summaryrefslogtreecommitdiffstats
path: root/dom/html
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/html
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/html')
-rw-r--r--dom/html/ConstraintValidation.cpp66
-rw-r--r--dom/html/ConstraintValidation.h48
-rw-r--r--dom/html/CustomStateSet.cpp82
-rw-r--r--dom/html/CustomStateSet.h60
-rw-r--r--dom/html/ElementInternals.cpp485
-rw-r--r--dom/html/ElementInternals.h220
-rw-r--r--dom/html/FetchPriority.cpp109
-rw-r--r--dom/html/FetchPriority.h36
-rw-r--r--dom/html/HTMLAllCollection.cpp195
-rw-r--r--dom/html/HTMLAllCollection.h90
-rw-r--r--dom/html/HTMLAnchorElement.cpp213
-rw-r--r--dom/html/HTMLAnchorElement.h201
-rw-r--r--dom/html/HTMLAreaElement.cpp115
-rw-r--r--dom/html/HTMLAreaElement.h170
-rw-r--r--dom/html/HTMLAudioElement.cpp109
-rw-r--r--dom/html/HTMLAudioElement.h50
-rw-r--r--dom/html/HTMLBRElement.cpp76
-rw-r--r--dom/html/HTMLBRElement.h73
-rw-r--r--dom/html/HTMLBodyElement.cpp329
-rw-r--r--dom/html/HTMLBodyElement.h117
-rw-r--r--dom/html/HTMLButtonElement.cpp438
-rw-r--r--dom/html/HTMLButtonElement.h149
-rw-r--r--dom/html/HTMLCanvasElement.cpp1443
-rw-r--r--dom/html/HTMLCanvasElement.h447
-rw-r--r--dom/html/HTMLDNSPrefetch.cpp647
-rw-r--r--dom/html/HTMLDNSPrefetch.h147
-rw-r--r--dom/html/HTMLDataElement.cpp28
-rw-r--r--dom/html/HTMLDataElement.h39
-rw-r--r--dom/html/HTMLDataListElement.cpp37
-rw-r--r--dom/html/HTMLDataListElement.h56
-rw-r--r--dom/html/HTMLDetailsElement.cpp162
-rw-r--r--dom/html/HTMLDetailsElement.h67
-rw-r--r--dom/html/HTMLDialogElement.cpp200
-rw-r--r--dom/html/HTMLDialogElement.h69
-rw-r--r--dom/html/HTMLDivElement.cpp54
-rw-r--r--dom/html/HTMLDivElement.h46
-rw-r--r--dom/html/HTMLElement.cpp467
-rw-r--r--dom/html/HTMLElement.h92
-rw-r--r--dom/html/HTMLEmbedElement.cpp244
-rw-r--r--dom/html/HTMLEmbedElement.h135
-rw-r--r--dom/html/HTMLFieldSetElement.cpp314
-rw-r--r--dom/html/HTMLFieldSetElement.h142
-rw-r--r--dom/html/HTMLFontElement.cpp106
-rw-r--r--dom/html/HTMLFontElement.h51
-rw-r--r--dom/html/HTMLFormControlsCollection.cpp305
-rw-r--r--dom/html/HTMLFormControlsCollection.h127
-rw-r--r--dom/html/HTMLFormElement.cpp2054
-rw-r--r--dom/html/HTMLFormElement.h596
-rw-r--r--dom/html/HTMLFormSubmission.cpp881
-rw-r--r--dom/html/HTMLFormSubmission.h291
-rw-r--r--dom/html/HTMLFormSubmissionConstants.h36
-rw-r--r--dom/html/HTMLFrameElement.cpp54
-rw-r--r--dom/html/HTMLFrameElement.h100
-rw-r--r--dom/html/HTMLFrameSetElement.cpp316
-rw-r--r--dom/html/HTMLFrameSetElement.h153
-rw-r--r--dom/html/HTMLHRElement.cpp193
-rw-r--r--dom/html/HTMLHRElement.h74
-rw-r--r--dom/html/HTMLHeadingElement.cpp58
-rw-r--r--dom/html/HTMLHeadingElement.h72
-rw-r--r--dom/html/HTMLIFrameElement.cpp381
-rw-r--r--dom/html/HTMLIFrameElement.h234
-rw-r--r--dom/html/HTMLImageElement.cpp1380
-rw-r--r--dom/html/HTMLImageElement.h428
-rw-r--r--dom/html/HTMLInputElement.cpp7407
-rw-r--r--dom/html/HTMLInputElement.h1679
-rw-r--r--dom/html/HTMLLIElement.cpp100
-rw-r--r--dom/html/HTMLLIElement.h56
-rw-r--r--dom/html/HTMLLabelElement.cpp246
-rw-r--r--dom/html/HTMLLabelElement.h73
-rw-r--r--dom/html/HTMLLegendElement.cpp140
-rw-r--r--dom/html/HTMLLegendElement.h95
-rw-r--r--dom/html/HTMLLinkElement.cpp706
-rw-r--r--dom/html/HTMLLinkElement.h223
-rw-r--r--dom/html/HTMLMapElement.cpp44
-rw-r--r--dom/html/HTMLMapElement.h45
-rw-r--r--dom/html/HTMLMarqueeElement.cpp173
-rw-r--r--dom/html/HTMLMarqueeElement.h130
-rw-r--r--dom/html/HTMLMediaElement.cpp7881
-rw-r--r--dom/html/HTMLMediaElement.h1941
-rw-r--r--dom/html/HTMLMenuElement.cpp28
-rw-r--r--dom/html/HTMLMenuElement.h42
-rw-r--r--dom/html/HTMLMetaElement.cpp178
-rw-r--r--dom/html/HTMLMetaElement.h74
-rw-r--r--dom/html/HTMLMeterElement.cpp259
-rw-r--r--dom/html/HTMLMeterElement.h99
-rw-r--r--dom/html/HTMLModElement.cpp28
-rw-r--r--dom/html/HTMLModElement.h42
-rw-r--r--dom/html/HTMLObjectElement.cpp273
-rw-r--r--dom/html/HTMLObjectElement.h205
-rw-r--r--dom/html/HTMLOptGroupElement.cpp115
-rw-r--r--dom/html/HTMLOptGroupElement.h74
-rw-r--r--dom/html/HTMLOptionElement.cpp348
-rw-r--r--dom/html/HTMLOptionElement.h134
-rw-r--r--dom/html/HTMLOptionsCollection.cpp191
-rw-r--r--dom/html/HTMLOptionsCollection.h150
-rw-r--r--dom/html/HTMLOutputElement.cpp137
-rw-r--r--dom/html/HTMLOutputElement.h100
-rw-r--r--dom/html/HTMLParagraphElement.cpp60
-rw-r--r--dom/html/HTMLParagraphElement.h52
-rw-r--r--dom/html/HTMLPictureElement.cpp79
-rw-r--r--dom/html/HTMLPictureElement.h40
-rw-r--r--dom/html/HTMLPreElement.cpp83
-rw-r--r--dom/html/HTMLPreElement.h50
-rw-r--r--dom/html/HTMLProgressElement.cpp87
-rw-r--r--dom/html/HTMLProgressElement.h57
-rw-r--r--dom/html/HTMLScriptElement.cpp254
-rw-r--r--dom/html/HTMLScriptElement.h167
-rw-r--r--dom/html/HTMLSelectElement.cpp1645
-rw-r--r--dom/html/HTMLSelectElement.h520
-rw-r--r--dom/html/HTMLSharedElement.cpp223
-rw-r--r--dom/html/HTMLSharedElement.h131
-rw-r--r--dom/html/HTMLSharedListElement.cpp148
-rw-r--r--dom/html/HTMLSharedListElement.h63
-rw-r--r--dom/html/HTMLSlotElement.cpp371
-rw-r--r--dom/html/HTMLSlotElement.h88
-rw-r--r--dom/html/HTMLSourceElement.cpp230
-rw-r--r--dom/html/HTMLSourceElement.h157
-rw-r--r--dom/html/HTMLSpanElement.cpp23
-rw-r--r--dom/html/HTMLSpanElement.h30
-rw-r--r--dom/html/HTMLStyleElement.cpp202
-rw-r--r--dom/html/HTMLStyleElement.h100
-rw-r--r--dom/html/HTMLSummaryElement.cpp118
-rw-r--r--dom/html/HTMLSummaryElement.h55
-rw-r--r--dom/html/HTMLTableCaptionElement.cpp73
-rw-r--r--dom/html/HTMLTableCaptionElement.h47
-rw-r--r--dom/html/HTMLTableCellElement.cpp219
-rw-r--r--dom/html/HTMLTableCellElement.h125
-rw-r--r--dom/html/HTMLTableColElement.cpp102
-rw-r--r--dom/html/HTMLTableColElement.h70
-rw-r--r--dom/html/HTMLTableElement.cpp995
-rw-r--r--dom/html/HTMLTableElement.h205
-rw-r--r--dom/html/HTMLTableRowElement.cpp249
-rw-r--r--dom/html/HTMLTableRowElement.h92
-rw-r--r--dom/html/HTMLTableSectionElement.cpp177
-rw-r--r--dom/html/HTMLTableSectionElement.h75
-rw-r--r--dom/html/HTMLTemplateElement.cpp110
-rw-r--r--dom/html/HTMLTemplateElement.h77
-rw-r--r--dom/html/HTMLTextAreaElement.cpp1164
-rw-r--r--dom/html/HTMLTextAreaElement.h382
-rw-r--r--dom/html/HTMLTimeElement.cpp30
-rw-r--r--dom/html/HTMLTimeElement.h40
-rw-r--r--dom/html/HTMLTitleElement.cpp95
-rw-r--r--dom/html/HTMLTitleElement.h60
-rw-r--r--dom/html/HTMLTrackElement.cpp517
-rw-r--r--dom/html/HTMLTrackElement.h137
-rw-r--r--dom/html/HTMLUnknownElement.cpp26
-rw-r--r--dom/html/HTMLUnknownElement.h43
-rw-r--r--dom/html/HTMLVideoElement.cpp681
-rw-r--r--dom/html/HTMLVideoElement.h203
-rw-r--r--dom/html/ImageDocument.cpp795
-rw-r--r--dom/html/ImageDocument.h160
-rw-r--r--dom/html/MediaDocument.cpp411
-rw-r--r--dom/html/MediaDocument.h122
-rw-r--r--dom/html/MediaError.cpp85
-rw-r--r--dom/html/MediaError.h48
-rw-r--r--dom/html/PlayPromise.cpp84
-rw-r--r--dom/html/PlayPromise.h37
-rw-r--r--dom/html/RadioNodeList.cpp60
-rw-r--r--dom/html/RadioNodeList.h44
-rw-r--r--dom/html/TextControlElement.h249
-rw-r--r--dom/html/TextControlState.cpp3087
-rw-r--r--dom/html/TextControlState.h550
-rw-r--r--dom/html/TextInputListener.h107
-rw-r--r--dom/html/TextTrackManager.cpp874
-rw-r--r--dom/html/TextTrackManager.h194
-rw-r--r--dom/html/TimeRanges.cpp183
-rw-r--r--dom/html/TimeRanges.h119
-rw-r--r--dom/html/ValidityState.cpp31
-rw-r--r--dom/html/ValidityState.h93
-rw-r--r--dom/html/VideoDocument.cpp158
-rw-r--r--dom/html/crashtests/1032654.html1
-rw-r--r--dom/html/crashtests/1141260.html4
-rw-r--r--dom/html/crashtests/1228876.html21
-rw-r--r--dom/html/crashtests/1230110.html19
-rw-r--r--dom/html/crashtests/1237633.html1
-rw-r--r--dom/html/crashtests/1281972-1.html5
-rw-r--r--dom/html/crashtests/1282894.html17
-rw-r--r--dom/html/crashtests/1290904.html37
-rw-r--r--dom/html/crashtests/1343886-1.html14
-rw-r--r--dom/html/crashtests/1343886-2.xml3
-rw-r--r--dom/html/crashtests/1343886-3.xml3
-rw-r--r--dom/html/crashtests/1350972.html22
-rw-r--r--dom/html/crashtests/1386905.html13
-rw-r--r--dom/html/crashtests/1401726.html17
-rw-r--r--dom/html/crashtests/1412173.html18
-rw-r--r--dom/html/crashtests/1429783.html19
-rw-r--r--dom/html/crashtests/1440523.html13
-rw-r--r--dom/html/crashtests/1440827.html6
-rw-r--r--dom/html/crashtests/1547057.html11
-rw-r--r--dom/html/crashtests/1550524.html7
-rw-r--r--dom/html/crashtests/1550881-1.html13
-rw-r--r--dom/html/crashtests/1550881-2.html14
-rw-r--r--dom/html/crashtests/1667493.html13
-rw-r--r--dom/html/crashtests/1667493_1.html7
-rw-r--r--dom/html/crashtests/1680418.html17
-rw-r--r--dom/html/crashtests/1704660.html17
-rw-r--r--dom/html/crashtests/1724816.html14
-rw-r--r--dom/html/crashtests/1785933-inner.html28
-rw-r--r--dom/html/crashtests/1785933.html10
-rw-r--r--dom/html/crashtests/1787671.html19
-rw-r--r--dom/html/crashtests/1789475.html10
-rw-r--r--dom/html/crashtests/1801380.html1
-rw-r--r--dom/html/crashtests/1840088.html2
-rw-r--r--dom/html/crashtests/257818-1.html82
-rw-r--r--dom/html/crashtests/285166-1.html3
-rw-r--r--dom/html/crashtests/294235-1.html14
-rw-r--r--dom/html/crashtests/307616-1.html8
-rw-r--r--dom/html/crashtests/324918-1.xhtml26
-rw-r--r--dom/html/crashtests/338649-1.xhtml22
-rw-r--r--dom/html/crashtests/339501-1.xhtml33
-rw-r--r--dom/html/crashtests/339501-2.xhtml33
-rw-r--r--dom/html/crashtests/378993-1.xhtml7
-rw-r--r--dom/html/crashtests/382568-1-inner.xhtml52
-rw-r--r--dom/html/crashtests/382568-1.html9
-rw-r--r--dom/html/crashtests/383137.xhtml13
-rw-r--r--dom/html/crashtests/388183-1.html8
-rw-r--r--dom/html/crashtests/395340-1.html28
-rw-r--r--dom/html/crashtests/399694-1.html20
-rw-r--r--dom/html/crashtests/407053.html6
-rw-r--r--dom/html/crashtests/423371-1.html9
-rw-r--r--dom/html/crashtests/448564.html7
-rw-r--r--dom/html/crashtests/451123-1.html7
-rw-r--r--dom/html/crashtests/453406-1.html34
-rw-r--r--dom/html/crashtests/464197-1.html23
-rw-r--r--dom/html/crashtests/468562-1.html6
-rw-r--r--dom/html/crashtests/468562-2.html6
-rw-r--r--dom/html/crashtests/494225.html10
-rw-r--r--dom/html/crashtests/495543.svg16
-rw-r--r--dom/html/crashtests/495546-1.html19
-rw-r--r--dom/html/crashtests/504183-1.html12
-rw-r--r--dom/html/crashtests/515829-1.html7
-rw-r--r--dom/html/crashtests/515829-2.html7
-rw-r--r--dom/html/crashtests/570566-1.html2
-rw-r--r--dom/html/crashtests/571428-1.html14
-rw-r--r--dom/html/crashtests/580507-1.xhtml18
-rw-r--r--dom/html/crashtests/590387.html8
-rw-r--r--dom/html/crashtests/596785-1.html9
-rw-r--r--dom/html/crashtests/596785-2.html9
-rw-r--r--dom/html/crashtests/602117.html8
-rw-r--r--dom/html/crashtests/604807.html9
-rw-r--r--dom/html/crashtests/605264.html8
-rw-r--r--dom/html/crashtests/606430-1.html31
-rw-r--r--dom/html/crashtests/613027.html21
-rw-r--r--dom/html/crashtests/614279.html18
-rw-r--r--dom/html/crashtests/614988-1.html5
-rw-r--r--dom/html/crashtests/616401.html8
-rw-r--r--dom/html/crashtests/620078-1.html20
-rw-r--r--dom/html/crashtests/620078-2.html6
-rw-r--r--dom/html/crashtests/631421.html34
-rw-r--r--dom/html/crashtests/631421.pngbin0 -> 38661 bytes
-rw-r--r--dom/html/crashtests/673853.html20
-rw-r--r--dom/html/crashtests/682058.xhtml11
-rw-r--r--dom/html/crashtests/682460.html21
-rw-r--r--dom/html/crashtests/68912-1.html24
-rw-r--r--dom/html/crashtests/738744.xhtml4
-rw-r--r--dom/html/crashtests/741218.json1
-rw-r--r--dom/html/crashtests/741218.json^headers^1
-rw-r--r--dom/html/crashtests/741250.xhtml9
-rw-r--r--dom/html/crashtests/768344.html22
-rw-r--r--dom/html/crashtests/795221-1.html7
-rw-r--r--dom/html/crashtests/795221-2.html9
-rw-r--r--dom/html/crashtests/795221-3.html14
-rw-r--r--dom/html/crashtests/795221-4.html9
-rw-r--r--dom/html/crashtests/795221-5.xml6
-rw-r--r--dom/html/crashtests/798802-1.html18
-rw-r--r--dom/html/crashtests/811226.html6
-rw-r--r--dom/html/crashtests/819745.html5
-rw-r--r--dom/html/crashtests/828180.html5
-rw-r--r--dom/html/crashtests/828472.html6
-rw-r--r--dom/html/crashtests/837033.html4
-rw-r--r--dom/html/crashtests/838256-1.html20
-rw-r--r--dom/html/crashtests/862084.html9
-rw-r--r--dom/html/crashtests/865147.html7
-rw-r--r--dom/html/crashtests/877910.html1
-rw-r--r--dom/html/crashtests/903106.html3
-rw-r--r--dom/html/crashtests/916322-1.html10
-rw-r--r--dom/html/crashtests/916322-2.html10
-rw-r--r--dom/html/crashtests/978644.xhtml11
-rw-r--r--dom/html/crashtests/crashtests.list99
-rw-r--r--dom/html/input/ButtonInputTypes.h73
-rw-r--r--dom/html/input/CheckableInputTypes.cpp36
-rw-r--r--dom/html/input/CheckableInputTypes.h55
-rw-r--r--dom/html/input/ColorInputType.h28
-rw-r--r--dom/html/input/DateTimeInputTypes.cpp501
-rw-r--r--dom/html/input/DateTimeInputTypes.h154
-rw-r--r--dom/html/input/FileInputType.cpp26
-rw-r--r--dom/html/input/FileInputType.h32
-rw-r--r--dom/html/input/HiddenInputType.h28
-rw-r--r--dom/html/input/InputType.cpp354
-rw-r--r--dom/html/input/InputType.h240
-rw-r--r--dom/html/input/NumericInputTypes.cpp166
-rw-r--r--dom/html/input/NumericInputTypes.h76
-rw-r--r--dom/html/input/SingleLineTextInputTypes.cpp289
-rw-r--r--dom/html/input/SingleLineTextInputTypes.h158
-rw-r--r--dom/html/input/moz.build36
-rw-r--r--dom/html/moz.build247
-rw-r--r--dom/html/nsBrowserElement.cpp57
-rw-r--r--dom/html/nsBrowserElement.h57
-rw-r--r--dom/html/nsDOMStringMap.cpp242
-rw-r--r--dom/html/nsDOMStringMap.h65
-rw-r--r--dom/html/nsGenericHTMLElement.cpp3623
-rw-r--r--dom/html/nsGenericHTMLElement.h1461
-rw-r--r--dom/html/nsGenericHTMLFrameElement.cpp363
-rw-r--r--dom/html/nsGenericHTMLFrameElement.h173
-rw-r--r--dom/html/nsHTMLContentSink.cpp937
-rw-r--r--dom/html/nsHTMLDocument.cpp747
-rw-r--r--dom/html/nsHTMLDocument.h213
-rw-r--r--dom/html/nsIConstraintValidation.cpp135
-rw-r--r--dom/html/nsIConstraintValidation.h111
-rw-r--r--dom/html/nsIFormControl.h288
-rw-r--r--dom/html/nsIHTMLCollection.h87
-rw-r--r--dom/html/nsIRadioVisitor.h48
-rw-r--r--dom/html/nsRadioVisitor.cpp49
-rw-r--r--dom/html/nsRadioVisitor.h96
-rw-r--r--dom/html/reftests/41464-1-ref.html5
-rw-r--r--dom/html/reftests/41464-1a.html8
-rw-r--r--dom/html/reftests/41464-1b.html8
-rw-r--r--dom/html/reftests/468263-1a.html6
-rw-r--r--dom/html/reftests/468263-1b.html6
-rw-r--r--dom/html/reftests/468263-1c.html6
-rw-r--r--dom/html/reftests/468263-1d.html6
-rw-r--r--dom/html/reftests/468263-2-alternate-ref.html8
-rw-r--r--dom/html/reftests/468263-2-ref.html10
-rw-r--r--dom/html/reftests/468263-2.html10
-rw-r--r--dom/html/reftests/484200-1-ref.html11
-rw-r--r--dom/html/reftests/484200-1.html11
-rw-r--r--dom/html/reftests/485377-ref.html3
-rw-r--r--dom/html/reftests/485377.html3
-rw-r--r--dom/html/reftests/52019-1-ref.html11
-rw-r--r--dom/html/reftests/52019-1.html11
-rw-r--r--dom/html/reftests/557840-ref.html3
-rw-r--r--dom/html/reftests/557840.html3
-rw-r--r--dom/html/reftests/560059-video-dimensions-ref.html3
-rw-r--r--dom/html/reftests/560059-video-dimensions.html3
-rw-r--r--dom/html/reftests/573322-no-quirks-ref.html28
-rw-r--r--dom/html/reftests/573322-no-quirks.html28
-rw-r--r--dom/html/reftests/573322-quirks-ref.html27
-rw-r--r--dom/html/reftests/573322-quirks.html27
-rw-r--r--dom/html/reftests/596455-1a.html14
-rw-r--r--dom/html/reftests/596455-1b.html14
-rw-r--r--dom/html/reftests/596455-2a.html14
-rw-r--r--dom/html/reftests/596455-2b.html14
-rw-r--r--dom/html/reftests/596455-ref-1.html6
-rw-r--r--dom/html/reftests/596455-ref-2.html6
-rw-r--r--dom/html/reftests/610935-ref.html12
-rw-r--r--dom/html/reftests/610935.html11
-rw-r--r--dom/html/reftests/649134-1.html31
-rw-r--r--dom/html/reftests/649134-2-ref.html25
-rw-r--r--dom/html/reftests/649134-2.html31
-rw-r--r--dom/html/reftests/649134-ref.html13
-rw-r--r--dom/html/reftests/741776-1-ref.html1
-rw-r--r--dom/html/reftests/741776-1.vtt1
-rw-r--r--dom/html/reftests/82711-1-ref.html15
-rw-r--r--dom/html/reftests/82711-1.html15
-rw-r--r--dom/html/reftests/82711-2-ref.html15
-rw-r--r--dom/html/reftests/82711-2.html8
-rw-r--r--dom/html/reftests/autofocus/autofocus-after-body-focus-ref.html7
-rw-r--r--dom/html/reftests/autofocus/autofocus-after-body-focus.html10
-rw-r--r--dom/html/reftests/autofocus/autofocus-after-load-ref.html7
-rw-r--r--dom/html/reftests/autofocus/autofocus-after-load.html21
-rw-r--r--dom/html/reftests/autofocus/autofocus-leaves-iframe-ref.html17
-rw-r--r--dom/html/reftests/autofocus/autofocus-leaves-iframe.html16
-rw-r--r--dom/html/reftests/autofocus/button-create.html23
-rw-r--r--dom/html/reftests/autofocus/button-load.html13
-rw-r--r--dom/html/reftests/autofocus/button-ref.html7
-rw-r--r--dom/html/reftests/autofocus/input-create.html23
-rw-r--r--dom/html/reftests/autofocus/input-load.html13
-rw-r--r--dom/html/reftests/autofocus/input-number-ref.html17
-rw-r--r--dom/html/reftests/autofocus/input-number.html26
-rw-r--r--dom/html/reftests/autofocus/input-ref.html7
-rw-r--r--dom/html/reftests/autofocus/input-time-ref.html22
-rw-r--r--dom/html/reftests/autofocus/input-time.html22
-rw-r--r--dom/html/reftests/autofocus/reftest.list13
-rw-r--r--dom/html/reftests/autofocus/select-create.html23
-rw-r--r--dom/html/reftests/autofocus/select-load.html13
-rw-r--r--dom/html/reftests/autofocus/select-ref.html7
-rw-r--r--dom/html/reftests/autofocus/style.css10
-rw-r--r--dom/html/reftests/autofocus/textarea-create.html23
-rw-r--r--dom/html/reftests/autofocus/textarea-load.html13
-rw-r--r--dom/html/reftests/autofocus/textarea-ref.html7
-rw-r--r--dom/html/reftests/body-frame-margin-remove-other-pres-hint-ref.html14
-rw-r--r--dom/html/reftests/body-frame-margin-remove-other-pres-hint.html16
-rw-r--r--dom/html/reftests/body-topmargin-dynamic.html12
-rw-r--r--dom/html/reftests/body-topmargin-ref.html7
-rw-r--r--dom/html/reftests/bug1106522-1.html11
-rw-r--r--dom/html/reftests/bug1106522-2.html11
-rw-r--r--dom/html/reftests/bug1106522-ref.html8
-rw-r--r--dom/html/reftests/bug1196784-no-srcset.html6
-rw-r--r--dom/html/reftests/bug1196784-with-srcset.html6
-rw-r--r--dom/html/reftests/bug1196784.pngbin0 -> 294 bytes
-rw-r--r--dom/html/reftests/bug1228601-video-rotated-ref.html13
-rw-r--r--dom/html/reftests/bug1228601-video-rotation-90.html13
-rw-r--r--dom/html/reftests/bug1423850-canvas-video-rotated-ref.html16
-rw-r--r--dom/html/reftests/bug1423850-canvas-video-rotation-90.html16
-rw-r--r--dom/html/reftests/bug1512297-ref.html7
-rw-r--r--dom/html/reftests/bug1512297.html14
-rw-r--r--dom/html/reftests/bug448564-1_ideal.html13
-rw-r--r--dom/html/reftests/bug448564-1_malformed.html19
-rw-r--r--dom/html/reftests/bug448564-1_well-formed.html11
-rw-r--r--dom/html/reftests/bug448564-4a.html10
-rw-r--r--dom/html/reftests/bug448564-4b.html6
-rw-r--r--dom/html/reftests/bug448564_forms.css2
-rw-r--r--dom/html/reftests/bug502168-1_malformed.html10
-rw-r--r--dom/html/reftests/bug502168-1_well-formed.html9
-rw-r--r--dom/html/reftests/bug917595-1-ref.html17
-rw-r--r--dom/html/reftests/bug917595-exif-rotated.jpgbin0 -> 90700 bytes
-rw-r--r--dom/html/reftests/bug917595-iframe-1.html17
-rw-r--r--dom/html/reftests/bug917595-pixel-rotated.jpgbin0 -> 91596 bytes
-rw-r--r--dom/html/reftests/bug917595-unrotated.jpgbin0 -> 90864 bytes
-rw-r--r--dom/html/reftests/figure-ref.html11
-rw-r--r--dom/html/reftests/figure.html8
-rw-r--r--dom/html/reftests/href-attr-change-restyles-ref.html33
-rw-r--r--dom/html/reftests/href-attr-change-restyles.html48
-rw-r--r--dom/html/reftests/iframe-with-image-src.html6
-rw-r--r--dom/html/reftests/image-load-shortcircuit-1.html8
-rw-r--r--dom/html/reftests/image-load-shortcircuit-2.html10
-rw-r--r--dom/html/reftests/image-load-shortcircuit-ref.html1
-rw-r--r--dom/html/reftests/lime100x100.svg4
-rw-r--r--dom/html/reftests/pass.pngbin0 -> 1036 bytes
-rw-r--r--dom/html/reftests/pre-1-ref.html22
-rw-r--r--dom/html/reftests/pre-1.html22
-rw-r--r--dom/html/reftests/red.pngbin0 -> 82 bytes
-rw-r--r--dom/html/reftests/reftest.list78
-rw-r--r--dom/html/reftests/responsive-image-load-shortcircuit-ref.html1
-rw-r--r--dom/html/reftests/responsive-image-load-shortcircuit.html15
-rw-r--r--dom/html/reftests/table-border-1-ref.html46
-rw-r--r--dom/html/reftests/table-border-1.html36
-rw-r--r--dom/html/reftests/table-border-2-notref.html40
-rw-r--r--dom/html/reftests/table-border-2-ref.html30
-rw-r--r--dom/html/reftests/table-border-2.html30
-rw-r--r--dom/html/reftests/toblob-todataurl/blob.js68
-rw-r--r--dom/html/reftests/toblob-todataurl/dataurl.js56
-rw-r--r--dom/html/reftests/toblob-todataurl/images/original.pngbin0 -> 50613 bytes
-rw-r--r--dom/html/reftests/toblob-todataurl/images/q0.jpgbin0 -> 2165 bytes
-rw-r--r--dom/html/reftests/toblob-todataurl/images/q100.jpgbin0 -> 54323 bytes
-rw-r--r--dom/html/reftests/toblob-todataurl/images/q25.jpgbin0 -> 3898 bytes
-rw-r--r--dom/html/reftests/toblob-todataurl/images/q50.jpgbin0 -> 4924 bytes
-rw-r--r--dom/html/reftests/toblob-todataurl/images/q75.jpgbin0 -> 6405 bytes
-rw-r--r--dom/html/reftests/toblob-todataurl/images/q92.jpgbin0 -> 13931 bytes
-rw-r--r--dom/html/reftests/toblob-todataurl/quality-0-ref.html2
-rw-r--r--dom/html/reftests/toblob-todataurl/quality-100-ref.html2
-rw-r--r--dom/html/reftests/toblob-todataurl/quality-25-ref.html2
-rw-r--r--dom/html/reftests/toblob-todataurl/quality-50-ref.html2
-rw-r--r--dom/html/reftests/toblob-todataurl/quality-75-ref.html2
-rw-r--r--dom/html/reftests/toblob-todataurl/quality-92-ref.html2
-rw-r--r--dom/html/reftests/toblob-todataurl/reftest.list16
-rw-r--r--dom/html/reftests/toblob-todataurl/sample.js2
-rw-r--r--dom/html/reftests/toblob-todataurl/toblob-quality-0.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/toblob-quality-100.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/toblob-quality-25.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/toblob-quality-50.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/toblob-quality-75.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/toblob-quality-92.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/toblob-quality-default.html7
-rw-r--r--dom/html/reftests/toblob-todataurl/toblob-quality-undefined.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/todataurl-quality-0.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/todataurl-quality-100.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/todataurl-quality-25.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/todataurl-quality-50.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/todataurl-quality-75.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/todataurl-quality-92.html10
-rw-r--r--dom/html/reftests/toblob-todataurl/todataurl-quality-default.html7
-rw-r--r--dom/html/reftests/toblob-todataurl/todataurl-quality-undefined.html10
-rw-r--r--dom/html/reftests/video_rotated.mp4bin0 -> 1543 bytes
-rw-r--r--dom/html/reftests/video_rotation_90.mp4bin0 -> 1541 bytes
-rw-r--r--dom/html/test/347174transform.xsl41
-rw-r--r--dom/html/test/347174transformable.xml3
-rw-r--r--dom/html/test/allowMedia.sjs12
-rw-r--r--dom/html/test/browser.toml46
-rw-r--r--dom/html/test/browser_DOMDocElementInserted.js23
-rw-r--r--dom/html/test/browser_ImageDocument_svg_zoom.js39
-rw-r--r--dom/html/test/browser_bug1081537.js11
-rw-r--r--dom/html/test/browser_bug1108547.js149
-rw-r--r--dom/html/test/browser_bug436200.js60
-rw-r--r--dom/html/test/browser_bug592641.js61
-rw-r--r--dom/html/test/browser_containerLoadingContent.js108
-rw-r--r--dom/html/test/browser_form_post_from_file_to_http.js181
-rw-r--r--dom/html/test/browser_refresh_after_document_write.js52
-rw-r--r--dom/html/test/browser_submission_flush.js97
-rw-r--r--dom/html/test/browser_targetBlankNoOpener.js121
-rw-r--r--dom/html/test/bug100533_iframe.html8
-rw-r--r--dom/html/test/bug100533_load.html14
-rw-r--r--dom/html/test/bug1260704_iframe.html38
-rw-r--r--dom/html/test/bug1260704_iframe_empty.html15
-rw-r--r--dom/html/test/bug1292522_iframe.html10
-rw-r--r--dom/html/test/bug1292522_page.html14
-rw-r--r--dom/html/test/bug1315146-iframe.html4
-rw-r--r--dom/html/test/bug1315146-main.html15
-rw-r--r--dom/html/test/bug196523-subframe.html37
-rw-r--r--dom/html/test/bug199692-nested-d2.html14
-rw-r--r--dom/html/test/bug199692-nested.html15
-rw-r--r--dom/html/test/bug199692-popup.html190
-rw-r--r--dom/html/test/bug199692-scrolled.html34
-rw-r--r--dom/html/test/bug242709_iframe.html20
-rw-r--r--dom/html/test/bug242709_load.html11
-rw-r--r--dom/html/test/bug277724_iframe1.html28
-rw-r--r--dom/html/test/bug277724_iframe2.xhtml27
-rw-r--r--dom/html/test/bug277890_iframe.html20
-rw-r--r--dom/html/test/bug277890_load.html11
-rw-r--r--dom/html/test/bug340800_iframe.txt4
-rw-r--r--dom/html/test/bug369370-popup.pngbin0 -> 4073 bytes
-rw-r--r--dom/html/test/bug372098-link-target.html7
-rw-r--r--dom/html/test/bug436200.html12
-rw-r--r--dom/html/test/bug441930_iframe.html27
-rw-r--r--dom/html/test/bug445004-inner.html14
-rw-r--r--dom/html/test/bug445004-inner.js27
-rw-r--r--dom/html/test/bug445004-outer-abs.html11
-rw-r--r--dom/html/test/bug445004-outer-rel.html11
-rw-r--r--dom/html/test/bug445004-outer-write.html11
-rw-r--r--dom/html/test/bug446483-iframe.html10
-rw-r--r--dom/html/test/bug448564-echo.sjs6
-rw-r--r--dom/html/test/bug448564-iframe-1.html16
-rw-r--r--dom/html/test/bug448564-iframe-2.html16
-rw-r--r--dom/html/test/bug448564-iframe-3.html16
-rw-r--r--dom/html/test/bug448564-submit.js6
-rw-r--r--dom/html/test/bug499092.html6
-rw-r--r--dom/html/test/bug499092.xml4
-rw-r--r--dom/html/test/bug514856_iframe.html21
-rw-r--r--dom/html/test/bug592641_img.jpgbin0 -> 42018 bytes
-rw-r--r--dom/html/test/bug649134/file_bug649134-1.sjs12
-rw-r--r--dom/html/test/bug649134/file_bug649134-2.sjs12
-rw-r--r--dom/html/test/bug649134/index.html3
-rw-r--r--dom/html/test/chrome.toml12
-rw-r--r--dom/html/test/dialog/mochitest.toml4
-rw-r--r--dom/html/test/dialog/test_bug1648877_dialog_fullscreen_denied.html52
-rw-r--r--dom/html/test/dummy_page.html10
-rw-r--r--dom/html/test/empty.html1
-rw-r--r--dom/html/test/file.webmbin0 -> 512 bytes
-rw-r--r--dom/html/test/file_anchor_ping.html13
-rw-r--r--dom/html/test/file_broadcast_load.html16
-rw-r--r--dom/html/test/file_bug1108547-1.html4
-rw-r--r--dom/html/test/file_bug1108547-2.html6
-rw-r--r--dom/html/test/file_bug1108547-3.html5
-rw-r--r--dom/html/test/file_bug1166138_1x.pngbin0 -> 91 bytes
-rw-r--r--dom/html/test/file_bug1166138_2x.pngbin0 -> 100 bytes
-rw-r--r--dom/html/test/file_bug1166138_def.pngbin0 -> 85 bytes
-rw-r--r--dom/html/test/file_bug1260704.pngbin0 -> 91 bytes
-rw-r--r--dom/html/test/file_bug209275_1.html28
-rw-r--r--dom/html/test/file_bug209275_2.html23
-rw-r--r--dom/html/test/file_bug209275_3.html23
-rw-r--r--dom/html/test/file_bug297761.html13
-rw-r--r--dom/html/test/file_bug417760.pngbin0 -> 1991 bytes
-rw-r--r--dom/html/test/file_bug871161-1.html16
-rw-r--r--dom/html/test/file_bug871161-2.html14
-rw-r--r--dom/html/test/file_bug893537.html9
-rw-r--r--dom/html/test/file_cookiemanager.js20
-rw-r--r--dom/html/test/file_formSubmission_img.jpgbin0 -> 2711 bytes
-rw-r--r--dom/html/test/file_formSubmission_text.txt1
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if1.html13
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if10.html12
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if11.html23
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if12.html23
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if13.html13
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if14.html34
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if15.html33
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if16.html25
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if17.html27
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if18.html26
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if19.html21
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if2.html21
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if3.html24
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if4.html30
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if5.html22
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if6.html21
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if7.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if8.html26
-rw-r--r--dom/html/test/file_iframe_sandbox_a_if9.html18
-rw-r--r--dom/html/test/file_iframe_sandbox_b_if1.html11
-rw-r--r--dom/html/test/file_iframe_sandbox_b_if2.html49
-rw-r--r--dom/html/test/file_iframe_sandbox_b_if3.html92
-rw-r--r--dom/html/test/file_iframe_sandbox_c_if1.html35
-rw-r--r--dom/html/test/file_iframe_sandbox_c_if2.html23
-rw-r--r--dom/html/test/file_iframe_sandbox_c_if3.html26
-rw-r--r--dom/html/test/file_iframe_sandbox_c_if4.html36
-rw-r--r--dom/html/test/file_iframe_sandbox_c_if5.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_c_if6.html24
-rw-r--r--dom/html/test/file_iframe_sandbox_c_if7.html27
-rw-r--r--dom/html/test/file_iframe_sandbox_c_if8.html27
-rw-r--r--dom/html/test/file_iframe_sandbox_c_if9.html17
-rw-r--r--dom/html/test/file_iframe_sandbox_close.html3
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if1.html19
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if10.html17
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if11.html30
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if12.html16
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if13.html34
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if14.html35
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if15.html14
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if16.html22
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if17.html24
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if18.html33
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if19.html13
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if2.html28
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if20.html25
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if21.html14
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if22.html25
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if23.html61
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if3.html13
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if4.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if5.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if6.html19
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if7.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if8.html18
-rw-r--r--dom/html/test/file_iframe_sandbox_d_if9.html17
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if1.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if10.html19
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if11.html22
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if12.html19
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if13.html19
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if14.html24
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if15.html17
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if16.html27
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if2.html12
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if3.html11
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if4.html11
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if5.html19
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if6.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if7.html17
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if8.html23
-rw-r--r--dom/html/test/file_iframe_sandbox_e_if9.html19
-rw-r--r--dom/html/test/file_iframe_sandbox_fail.js4
-rw-r--r--dom/html/test/file_iframe_sandbox_form_fail.html19
-rw-r--r--dom/html/test/file_iframe_sandbox_form_pass.html17
-rw-r--r--dom/html/test/file_iframe_sandbox_g_if1.html60
-rw-r--r--dom/html/test/file_iframe_sandbox_h_if1.html34
-rw-r--r--dom/html/test/file_iframe_sandbox_k_if1.html47
-rw-r--r--dom/html/test/file_iframe_sandbox_k_if2.html50
-rw-r--r--dom/html/test/file_iframe_sandbox_k_if3.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_k_if4.html34
-rw-r--r--dom/html/test/file_iframe_sandbox_k_if5.html33
-rw-r--r--dom/html/test/file_iframe_sandbox_k_if6.html21
-rw-r--r--dom/html/test/file_iframe_sandbox_k_if7.html26
-rw-r--r--dom/html/test/file_iframe_sandbox_k_if8.html36
-rw-r--r--dom/html/test/file_iframe_sandbox_k_if9.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_navigation_fail.html17
-rw-r--r--dom/html/test/file_iframe_sandbox_navigation_pass.html17
-rw-r--r--dom/html/test/file_iframe_sandbox_navigation_start.html11
-rw-r--r--dom/html/test/file_iframe_sandbox_open_window_fail.html19
-rw-r--r--dom/html/test/file_iframe_sandbox_open_window_pass.html25
-rw-r--r--dom/html/test/file_iframe_sandbox_pass.js4
-rw-r--r--dom/html/test/file_iframe_sandbox_redirect.html2
-rw-r--r--dom/html/test/file_iframe_sandbox_redirect.html^headers^2
-rw-r--r--dom/html/test/file_iframe_sandbox_redirect_target.html9
-rw-r--r--dom/html/test/file_iframe_sandbox_refresh.html2
-rw-r--r--dom/html/test/file_iframe_sandbox_refresh.html^headers^1
-rw-r--r--dom/html/test/file_iframe_sandbox_srcdoc_allow_scripts.html1
-rw-r--r--dom/html/test/file_iframe_sandbox_srcdoc_no_allow_scripts.html1
-rw-r--r--dom/html/test/file_iframe_sandbox_top_navigation_fail.html18
-rw-r--r--dom/html/test/file_iframe_sandbox_top_navigation_pass.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_window_form_fail.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_window_form_pass.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_window_navigation_fail.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_window_navigation_pass.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_window_top_navigation_fail.html24
-rw-r--r--dom/html/test/file_iframe_sandbox_window_top_navigation_pass.html20
-rw-r--r--dom/html/test/file_iframe_sandbox_worker.js3
-rw-r--r--dom/html/test/file_refresh_after_document_write.html15
-rw-r--r--dom/html/test/file_script_module.html42
-rw-r--r--dom/html/test/file_srcdoc-2.html10
-rw-r--r--dom/html/test/file_srcdoc.html16
-rw-r--r--dom/html/test/file_srcdoc_iframe3.html1
-rw-r--r--dom/html/test/file_window_close_and_open.html20
-rw-r--r--dom/html/test/file_window_open_close_inner.html7
-rw-r--r--dom/html/test/file_window_open_close_outer.html5
-rw-r--r--dom/html/test/formData_test.js289
-rw-r--r--dom/html/test/formData_worker.js23
-rw-r--r--dom/html/test/formSubmission_chrome.js20
-rw-r--r--dom/html/test/form_data_file.bin1
-rw-r--r--dom/html/test/form_data_file.txt1
-rw-r--r--dom/html/test/form_submit_server.sjs86
-rw-r--r--dom/html/test/forms/FAIL.html1
-rw-r--r--dom/html/test/forms/PASS.html1
-rw-r--r--dom/html/test/forms/chrome.toml6
-rw-r--r--dom/html/test/forms/file_double_submit.html11
-rw-r--r--dom/html/test/forms/file_login_fields.html16
-rw-r--r--dom/html/test/forms/mochitest.toml229
-rw-r--r--dom/html/test/forms/save_restore_custom_elements_sample.html43
-rw-r--r--dom/html/test/forms/save_restore_radio_groups.sjs48
-rw-r--r--dom/html/test/forms/submit_invalid_file.sjs13
-rw-r--r--dom/html/test/forms/test_MozEditableElement_setUserInput.html581
-rw-r--r--dom/html/test/forms/test_autocomplete.html164
-rw-r--r--dom/html/test/forms/test_autocompleteinfo.html206
-rw-r--r--dom/html/test/forms/test_bug1039548.html55
-rw-r--r--dom/html/test/forms/test_bug1283915.html67
-rw-r--r--dom/html/test/forms/test_bug1286509.html49
-rw-r--r--dom/html/test/forms/test_button_attributes_reflection.html144
-rw-r--r--dom/html/test/forms/test_change_event.html286
-rw-r--r--dom/html/test/forms/test_datalist_element.html118
-rw-r--r--dom/html/test/forms/test_double_submit.html33
-rw-r--r--dom/html/test/forms/test_form_attribute-1.html473
-rw-r--r--dom/html/test/forms/test_form_attribute-2.html53
-rw-r--r--dom/html/test/forms/test_form_attribute-3.html68
-rw-r--r--dom/html/test/forms/test_form_attribute-4.html48
-rw-r--r--dom/html/test/forms/test_form_attributes_reflection.html90
-rw-r--r--dom/html/test/forms/test_form_named_getter_dynamic.html54
-rw-r--r--dom/html/test/forms/test_formaction_attribute.html169
-rw-r--r--dom/html/test/forms/test_formnovalidate_attribute.html125
-rw-r--r--dom/html/test/forms/test_input_attributes_reflection.html271
-rw-r--r--dom/html/test/forms/test_input_color_input_change_events.html119
-rw-r--r--dom/html/test/forms/test_input_color_picker_datalist.html42
-rw-r--r--dom/html/test/forms/test_input_color_picker_initial.html78
-rw-r--r--dom/html/test/forms/test_input_color_picker_popup.html144
-rw-r--r--dom/html/test/forms/test_input_color_picker_update.html86
-rw-r--r--dom/html/test/forms/test_input_date_bad_input.html113
-rw-r--r--dom/html/test/forms/test_input_date_key_events.html270
-rw-r--r--dom/html/test/forms/test_input_datetime_calendar_button.html179
-rw-r--r--dom/html/test/forms/test_input_datetime_disabled_focus.html82
-rw-r--r--dom/html/test/forms/test_input_datetime_focus_blur.html64
-rw-r--r--dom/html/test/forms/test_input_datetime_focus_blur_events.html93
-rw-r--r--dom/html/test/forms/test_input_datetime_focus_state.html79
-rw-r--r--dom/html/test/forms/test_input_datetime_hidden.html32
-rw-r--r--dom/html/test/forms/test_input_datetime_input_change_events.html143
-rw-r--r--dom/html/test/forms/test_input_datetime_readonly.html20
-rw-r--r--dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html122
-rw-r--r--dom/html/test/forms/test_input_datetime_tabindex.html113
-rw-r--r--dom/html/test/forms/test_input_defaultValue.html81
-rw-r--r--dom/html/test/forms/test_input_email.html237
-rw-r--r--dom/html/test/forms/test_input_event.html409
-rw-r--r--dom/html/test/forms/test_input_file_picker.html280
-rw-r--r--dom/html/test/forms/test_input_hasBeenTypePassword.html67
-rw-r--r--dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html68
-rw-r--r--dom/html/test/forms/test_input_list_attribute.html253
-rw-r--r--dom/html/test/forms/test_input_number_data.js54
-rw-r--r--dom/html/test/forms/test_input_number_focus.html109
-rw-r--r--dom/html/test/forms/test_input_number_key_events.html238
-rw-r--r--dom/html/test/forms/test_input_number_l10n.html77
-rw-r--r--dom/html/test/forms/test_input_number_mouse_events.html272
-rw-r--r--dom/html/test/forms/test_input_number_placeholder_shown.html30
-rw-r--r--dom/html/test/forms/test_input_number_rounding.html120
-rw-r--r--dom/html/test/forms/test_input_number_validation.html139
-rw-r--r--dom/html/test/forms/test_input_password_click_show_password_button.html97
-rw-r--r--dom/html/test/forms/test_input_password_show_password_button.html81
-rw-r--r--dom/html/test/forms/test_input_radio_indeterminate.html109
-rw-r--r--dom/html/test/forms/test_input_radio_radiogroup.html75
-rw-r--r--dom/html/test/forms/test_input_radio_required.html31
-rw-r--r--dom/html/test/forms/test_input_range_attr_order.html48
-rw-r--r--dom/html/test/forms/test_input_range_key_events.html207
-rw-r--r--dom/html/test/forms/test_input_range_mouse_and_touch_events.html240
-rw-r--r--dom/html/test/forms/test_input_range_rounding.html103
-rw-r--r--dom/html/test/forms/test_input_sanitization.html585
-rw-r--r--dom/html/test/forms/test_input_setting_value.html619
-rw-r--r--dom/html/test/forms/test_input_textarea_set_value_no_scroll.html125
-rw-r--r--dom/html/test/forms/test_input_time_key_events.html221
-rw-r--r--dom/html/test/forms/test_input_time_sec_millisec_field.html134
-rw-r--r--dom/html/test/forms/test_input_types_pref.html77
-rw-r--r--dom/html/test/forms/test_input_typing_sanitization.html217
-rw-r--r--dom/html/test/forms/test_input_untrusted_key_events.html90
-rw-r--r--dom/html/test/forms/test_input_url.html91
-rw-r--r--dom/html/test/forms/test_interactive_content_in_label.html101
-rw-r--r--dom/html/test/forms/test_interactive_content_in_summary.html97
-rw-r--r--dom/html/test/forms/test_label_control_attribute.html100
-rw-r--r--dom/html/test/forms/test_label_input_controls.html84
-rw-r--r--dom/html/test/forms/test_max_attribute.html473
-rw-r--r--dom/html/test/forms/test_maxlength_attribute.html129
-rw-r--r--dom/html/test/forms/test_meter_element.html376
-rw-r--r--dom/html/test/forms/test_meter_pseudo-classes.html169
-rw-r--r--dom/html/test/forms/test_min_attribute.html473
-rw-r--r--dom/html/test/forms/test_minlength_attribute.html130
-rw-r--r--dom/html/test/forms/test_mozistextfield.html111
-rw-r--r--dom/html/test/forms/test_novalidate_attribute.html85
-rw-r--r--dom/html/test/forms/test_option_disabled.html123
-rw-r--r--dom/html/test/forms/test_option_index_attribute.html76
-rw-r--r--dom/html/test/forms/test_option_text.html57
-rw-r--r--dom/html/test/forms/test_output_element.html182
-rw-r--r--dom/html/test/forms/test_pattern_attribute.html324
-rw-r--r--dom/html/test/forms/test_preserving_metadata_between_reloads.html84
-rw-r--r--dom/html/test/forms/test_progress_element.html307
-rw-r--r--dom/html/test/forms/test_radio_in_label.html54
-rw-r--r--dom/html/test/forms/test_radio_radionodelist.html57
-rw-r--r--dom/html/test/forms/test_reportValidation_preventDefault.html89
-rw-r--r--dom/html/test/forms/test_required_attribute.html416
-rw-r--r--dom/html/test/forms/test_restore_form_elements.html174
-rw-r--r--dom/html/test/forms/test_save_restore_custom_elements.html90
-rw-r--r--dom/html/test/forms/test_save_restore_radio_groups.html70
-rw-r--r--dom/html/test/forms/test_select_change_event.html54
-rw-r--r--dom/html/test/forms/test_select_input_change_event.html122
-rw-r--r--dom/html/test/forms/test_select_selectedOptions.html119
-rw-r--r--dom/html/test/forms/test_select_validation.html39
-rw-r--r--dom/html/test/forms/test_set_range_text.html242
-rw-r--r--dom/html/test/forms/test_step_attribute.html1060
-rw-r--r--dom/html/test/forms/test_stepup_stepdown.html1137
-rw-r--r--dom/html/test/forms/test_submit_invalid_file.html55
-rw-r--r--dom/html/test/forms/test_textarea_attributes_reflection.html107
-rw-r--r--dom/html/test/forms/test_validation.html343
-rw-r--r--dom/html/test/forms/test_validation_not_in_doc.html19
-rw-r--r--dom/html/test/forms/test_valueasdate_attribute.html751
-rw-r--r--dom/html/test/forms/test_valueasnumber_attribute.html858
-rw-r--r--dom/html/test/forms/without_selectionchange/mochitest.toml5
-rw-r--r--dom/html/test/forms/without_selectionchange/test_select.html21
-rw-r--r--dom/html/test/head.js65
-rw-r--r--dom/html/test/image-allow-credentials.pngbin0 -> 844 bytes
-rw-r--r--dom/html/test/image-allow-credentials.png^headers^2
-rw-r--r--dom/html/test/image.pngbin0 -> 268 bytes
-rw-r--r--dom/html/test/image_yellow.pngbin0 -> 95 bytes
-rw-r--r--dom/html/test/mochitest.toml990
-rw-r--r--dom/html/test/nnc_lockup.gifbin0 -> 732 bytes
-rw-r--r--dom/html/test/object_bug287465_o1.html1
-rw-r--r--dom/html/test/object_bug287465_o2.html1
-rw-r--r--dom/html/test/object_bug556645.html1
-rw-r--r--dom/html/test/post_action_page.html10
-rw-r--r--dom/html/test/reflect.js1078
-rw-r--r--dom/html/test/script_fakepath.js16
-rw-r--r--dom/html/test/simpleFileOpener.js38
-rw-r--r--dom/html/test/submission_flush.html13
-rw-r--r--dom/html/test/sw_formSubmission.js36
-rw-r--r--dom/html/test/test_a_text.html44
-rw-r--r--dom/html/test/test_allowMedia.html97
-rw-r--r--dom/html/test/test_anchor_href_cache_invalidation.html30
-rw-r--r--dom/html/test/test_anchor_ping.html304
-rw-r--r--dom/html/test/test_base_attributes_reflection.html34
-rw-r--r--dom/html/test/test_bug1003539.html37
-rw-r--r--dom/html/test/test_bug100533.html47
-rw-r--r--dom/html/test/test_bug1013316.html46
-rw-r--r--dom/html/test/test_bug1045270.html46
-rw-r--r--dom/html/test/test_bug1089326.html108
-rw-r--r--dom/html/test/test_bug109445.html55
-rw-r--r--dom/html/test/test_bug109445.xhtml55
-rw-r--r--dom/html/test/test_bug1146116.html59
-rw-r--r--dom/html/test/test_bug1166138.html130
-rw-r--r--dom/html/test/test_bug1203668.html62
-rw-r--r--dom/html/test/test_bug1230665.html46
-rw-r--r--dom/html/test/test_bug1250401.html97
-rw-r--r--dom/html/test/test_bug1260664.html51
-rw-r--r--dom/html/test/test_bug1260704.html90
-rw-r--r--dom/html/test/test_bug1261673.html72
-rw-r--r--dom/html/test/test_bug1261674-1.html77
-rw-r--r--dom/html/test/test_bug1261674-2.html70
-rw-r--r--dom/html/test/test_bug1264157.html90
-rw-r--r--dom/html/test/test_bug1279218.html23
-rw-r--r--dom/html/test/test_bug1287321.html57
-rw-r--r--dom/html/test/test_bug1292522_same_domain_with_different_port_number.html43
-rw-r--r--dom/html/test/test_bug1295719_event_sequence_for_arrow_keys.html66
-rw-r--r--dom/html/test/test_bug1295719_event_sequence_for_number_keys.html65
-rw-r--r--dom/html/test/test_bug1297.html46
-rw-r--r--dom/html/test/test_bug1310865.html18
-rw-r--r--dom/html/test/test_bug1315146.html33
-rw-r--r--dom/html/test/test_bug1322678.html113
-rw-r--r--dom/html/test/test_bug1323815.html50
-rw-r--r--dom/html/test/test_bug1366.html35
-rw-r--r--dom/html/test/test_bug1400.html42
-rw-r--r--dom/html/test/test_bug1414077.html50
-rw-r--r--dom/html/test/test_bug143220.html72
-rw-r--r--dom/html/test/test_bug1472426.html120
-rw-r--r--dom/html/test/test_bug1682.html37
-rw-r--r--dom/html/test/test_bug1785739.html48
-rw-r--r--dom/html/test/test_bug182279.html35
-rw-r--r--dom/html/test/test_bug1823.html30
-rw-r--r--dom/html/test/test_bug196523.html41
-rw-r--r--dom/html/test/test_bug199692.html21
-rw-r--r--dom/html/test/test_bug2082.html30
-rw-r--r--dom/html/test/test_bug209275.xhtml258
-rw-r--r--dom/html/test/test_bug237071.html28
-rw-r--r--dom/html/test/test_bug242709.html33
-rw-r--r--dom/html/test/test_bug24958.html31
-rw-r--r--dom/html/test/test_bug255820.html99
-rw-r--r--dom/html/test/test_bug259332.html64
-rw-r--r--dom/html/test/test_bug274626.html97
-rw-r--r--dom/html/test/test_bug277724.html141
-rw-r--r--dom/html/test/test_bug277890.html33
-rw-r--r--dom/html/test/test_bug287465.html45
-rw-r--r--dom/html/test/test_bug295561.html86
-rw-r--r--dom/html/test/test_bug297761.html77
-rw-r--r--dom/html/test/test_bug300691-1.html126
-rw-r--r--dom/html/test/test_bug300691-2.html142
-rw-r--r--dom/html/test/test_bug300691-3.xhtml48
-rw-r--r--dom/html/test/test_bug311681.html99
-rw-r--r--dom/html/test/test_bug311681.xhtml102
-rw-r--r--dom/html/test/test_bug324378.html76
-rw-r--r--dom/html/test/test_bug330705-1.html41
-rw-r--r--dom/html/test/test_bug332246.html75
-rw-r--r--dom/html/test/test_bug332848.xhtml86
-rw-r--r--dom/html/test/test_bug332893-1.html38
-rw-r--r--dom/html/test/test_bug332893-2.html53
-rw-r--r--dom/html/test/test_bug332893-3.html58
-rw-r--r--dom/html/test/test_bug332893-4.html29
-rw-r--r--dom/html/test/test_bug332893-5.html29
-rw-r--r--dom/html/test/test_bug332893-6.html27
-rw-r--r--dom/html/test/test_bug332893-7.html69
-rw-r--r--dom/html/test/test_bug3348.html33
-rw-r--r--dom/html/test/test_bug340017.xhtml27
-rw-r--r--dom/html/test/test_bug340800.html55
-rw-r--r--dom/html/test/test_bug347174.html64
-rw-r--r--dom/html/test/test_bug347174_write.html71
-rw-r--r--dom/html/test/test_bug347174_xsl.html55
-rw-r--r--dom/html/test/test_bug347174_xslp.html61
-rw-r--r--dom/html/test/test_bug353415-1.html42
-rw-r--r--dom/html/test/test_bug353415-2.html67
-rw-r--r--dom/html/test/test_bug359657.html40
-rw-r--r--dom/html/test/test_bug369370.html153
-rw-r--r--dom/html/test/test_bug371375.html58
-rw-r--r--dom/html/test/test_bug372098.html68
-rw-r--r--dom/html/test/test_bug373589.html29
-rw-r--r--dom/html/test/test_bug375003-1.html157
-rw-r--r--dom/html/test/test_bug375003-2.html110
-rw-r--r--dom/html/test/test_bug377624.html25
-rw-r--r--dom/html/test/test_bug380383.html39
-rw-r--r--dom/html/test/test_bug383383.html41
-rw-r--r--dom/html/test/test_bug383383_2.xhtml20
-rw-r--r--dom/html/test/test_bug384419.html56
-rw-r--r--dom/html/test/test_bug386496.html53
-rw-r--r--dom/html/test/test_bug386728.html45
-rw-r--r--dom/html/test/test_bug386996.html43
-rw-r--r--dom/html/test/test_bug388558.html76
-rw-r--r--dom/html/test/test_bug388746.html62
-rw-r--r--dom/html/test/test_bug388794.html107
-rw-r--r--dom/html/test/test_bug389797.html243
-rw-r--r--dom/html/test/test_bug390975.html61
-rw-r--r--dom/html/test/test_bug391994.html184
-rw-r--r--dom/html/test/test_bug394700.html49
-rw-r--r--dom/html/test/test_bug395107.html108
-rw-r--r--dom/html/test/test_bug401160.xhtml27
-rw-r--r--dom/html/test/test_bug402680.html50
-rw-r--r--dom/html/test/test_bug403868.html87
-rw-r--r--dom/html/test/test_bug403868.xhtml86
-rw-r--r--dom/html/test/test_bug405242.html35
-rw-r--r--dom/html/test/test_bug406596.html83
-rw-r--r--dom/html/test/test_bug417760.html71
-rw-r--r--dom/html/test/test_bug421640.html56
-rw-r--r--dom/html/test/test_bug424698.html94
-rw-r--r--dom/html/test/test_bug428135.xhtml156
-rw-r--r--dom/html/test/test_bug430351.html523
-rw-r--r--dom/html/test/test_bug435128.html42
-rw-r--r--dom/html/test/test_bug441930.html29
-rw-r--r--dom/html/test/test_bug442801.html63
-rw-r--r--dom/html/test/test_bug445004.html138
-rw-r--r--dom/html/test/test_bug446483.html47
-rw-r--r--dom/html/test/test_bug448166.html39
-rw-r--r--dom/html/test/test_bug448564.html53
-rw-r--r--dom/html/test/test_bug456229.html30
-rw-r--r--dom/html/test/test_bug458037.xhtml112
-rw-r--r--dom/html/test/test_bug460568.html144
-rw-r--r--dom/html/test/test_bug463104.html25
-rw-r--r--dom/html/test/test_bug478251.html74
-rw-r--r--dom/html/test/test_bug481335.xhtml122
-rw-r--r--dom/html/test/test_bug481440.html30
-rw-r--r--dom/html/test/test_bug481647.html42
-rw-r--r--dom/html/test/test_bug482659.html64
-rw-r--r--dom/html/test/test_bug486741.html43
-rw-r--r--dom/html/test/test_bug489532.html33
-rw-r--r--dom/html/test/test_bug497242.xhtml41
-rw-r--r--dom/html/test/test_bug499092.html43
-rw-r--r--dom/html/test/test_bug500885.html67
-rw-r--r--dom/html/test/test_bug512367.html40
-rw-r--r--dom/html/test/test_bug514856.html61
-rw-r--r--dom/html/test/test_bug518122.html126
-rw-r--r--dom/html/test/test_bug519987.html33
-rw-r--r--dom/html/test/test_bug523771.html106
-rw-r--r--dom/html/test/test_bug529819.html32
-rw-r--r--dom/html/test/test_bug529859.html42
-rw-r--r--dom/html/test/test_bug535043.html90
-rw-r--r--dom/html/test/test_bug536891.html67
-rw-r--r--dom/html/test/test_bug536895.html54
-rw-r--r--dom/html/test/test_bug546995.html40
-rw-r--r--dom/html/test/test_bug547850.html45
-rw-r--r--dom/html/test/test_bug551846.html164
-rw-r--r--dom/html/test/test_bug555567.html42
-rw-r--r--dom/html/test/test_bug556645.html73
-rw-r--r--dom/html/test/test_bug557087-1.html129
-rw-r--r--dom/html/test/test_bug557087-2.html363
-rw-r--r--dom/html/test/test_bug557087-3.html215
-rw-r--r--dom/html/test/test_bug557087-4.html90
-rw-r--r--dom/html/test/test_bug557087-5.html94
-rw-r--r--dom/html/test/test_bug557087-6.html44
-rw-r--r--dom/html/test/test_bug557620.html30
-rw-r--r--dom/html/test/test_bug558788-1.html212
-rw-r--r--dom/html/test/test_bug558788-2.html174
-rw-r--r--dom/html/test/test_bug560112.html211
-rw-r--r--dom/html/test/test_bug561634.html126
-rw-r--r--dom/html/test/test_bug561636.html99
-rw-r--r--dom/html/test/test_bug561640.html72
-rw-r--r--dom/html/test/test_bug564001.html48
-rw-r--r--dom/html/test/test_bug566046.html200
-rw-r--r--dom/html/test/test_bug567938-1.html69
-rw-r--r--dom/html/test/test_bug567938-2.html70
-rw-r--r--dom/html/test/test_bug567938-3.html71
-rw-r--r--dom/html/test/test_bug567938-4.html43
-rw-r--r--dom/html/test/test_bug569955.html37
-rw-r--r--dom/html/test/test_bug573969.html37
-rw-r--r--dom/html/test/test_bug57600.html42
-rw-r--r--dom/html/test/test_bug579079.html40
-rw-r--r--dom/html/test/test_bug582412-1.html200
-rw-r--r--dom/html/test/test_bug582412-2.html199
-rw-r--r--dom/html/test/test_bug583514.html71
-rw-r--r--dom/html/test/test_bug583533.html81
-rw-r--r--dom/html/test/test_bug586763.html43
-rw-r--r--dom/html/test/test_bug586786.html57
-rw-r--r--dom/html/test/test_bug587469.html41
-rw-r--r--dom/html/test/test_bug589.html42
-rw-r--r--dom/html/test/test_bug590353-1.html36
-rw-r--r--dom/html/test/test_bug590353-2.html79
-rw-r--r--dom/html/test/test_bug590363.html133
-rw-r--r--dom/html/test/test_bug592802.html96
-rw-r--r--dom/html/test/test_bug593689.html50
-rw-r--r--dom/html/test/test_bug595429.html56
-rw-r--r--dom/html/test/test_bug595447.html29
-rw-r--r--dom/html/test/test_bug595449.html95
-rw-r--r--dom/html/test/test_bug596350.html65
-rw-r--r--dom/html/test/test_bug596511.html237
-rw-r--r--dom/html/test/test_bug598643.html80
-rw-r--r--dom/html/test/test_bug598833-1.html45
-rw-r--r--dom/html/test/test_bug600155.html44
-rw-r--r--dom/html/test/test_bug601030.html52
-rw-r--r--dom/html/test/test_bug605124-1.html98
-rw-r--r--dom/html/test/test_bug605124-2.html117
-rw-r--r--dom/html/test/test_bug605125-1.html105
-rw-r--r--dom/html/test/test_bug605125-2.html149
-rw-r--r--dom/html/test/test_bug606817.html59
-rw-r--r--dom/html/test/test_bug607145.html86
-rw-r--r--dom/html/test/test_bug610212.html42
-rw-r--r--dom/html/test/test_bug610687.html195
-rw-r--r--dom/html/test/test_bug611189.html45
-rw-r--r--dom/html/test/test_bug612730.html51
-rw-r--r--dom/html/test/test_bug613019.html84
-rw-r--r--dom/html/test/test_bug613113.html52
-rw-r--r--dom/html/test/test_bug613722.html32
-rw-r--r--dom/html/test/test_bug613979.html50
-rw-r--r--dom/html/test/test_bug615595.htmlbin0 -> 2706 bytes
-rw-r--r--dom/html/test/test_bug615833.html141
-rw-r--r--dom/html/test/test_bug618948.html88
-rw-r--r--dom/html/test/test_bug619278.html56
-rw-r--r--dom/html/test/test_bug622597.html105
-rw-r--r--dom/html/test/test_bug623291.html46
-rw-r--r--dom/html/test/test_bug6296.html31
-rw-r--r--dom/html/test/test_bug629801.html50
-rw-r--r--dom/html/test/test_bug633058.html66
-rw-r--r--dom/html/test/test_bug636336.html41
-rw-r--r--dom/html/test/test_bug641219.html34
-rw-r--r--dom/html/test/test_bug643051.html55
-rw-r--r--dom/html/test/test_bug646157.html95
-rw-r--r--dom/html/test/test_bug649134.html54
-rw-r--r--dom/html/test/test_bug651956.html48
-rw-r--r--dom/html/test/test_bug658746.html97
-rw-r--r--dom/html/test/test_bug659596.html96
-rw-r--r--dom/html/test/test_bug659743.xml55
-rw-r--r--dom/html/test/test_bug660663.html30
-rw-r--r--dom/html/test/test_bug660959-1.html25
-rw-r--r--dom/html/test/test_bug660959-2.html30
-rw-r--r--dom/html/test/test_bug660959-3.html28
-rw-r--r--dom/html/test/test_bug666200.html43
-rw-r--r--dom/html/test/test_bug666666.html32
-rw-r--r--dom/html/test/test_bug669012.html44
-rw-r--r--dom/html/test/test_bug674558.html287
-rw-r--r--dom/html/test/test_bug674927.html55
-rw-r--r--dom/html/test/test_bug677495-1.html34
-rw-r--r--dom/html/test/test_bug677495.html34
-rw-r--r--dom/html/test/test_bug677658.html41
-rw-r--r--dom/html/test/test_bug682886.html33
-rw-r--r--dom/html/test/test_bug691.html62
-rw-r--r--dom/html/test/test_bug694.html30
-rw-r--r--dom/html/test/test_bug694503.html75
-rw-r--r--dom/html/test/test_bug696.html28
-rw-r--r--dom/html/test/test_bug717819.html36
-rw-r--r--dom/html/test/test_bug741266.html44
-rw-r--r--dom/html/test/test_bug742030.html31
-rw-r--r--dom/html/test/test_bug742549.html47
-rw-r--r--dom/html/test/test_bug745685.html105
-rw-r--r--dom/html/test/test_bug763626.html29
-rw-r--r--dom/html/test/test_bug765780.html46
-rw-r--r--dom/html/test/test_bug780993.html39
-rw-r--r--dom/html/test/test_bug787134.html28
-rw-r--r--dom/html/test/test_bug797113.html39
-rw-r--r--dom/html/test/test_bug803677.html49
-rw-r--r--dom/html/test/test_bug821307.html41
-rw-r--r--dom/html/test/test_bug827126.html28
-rw-r--r--dom/html/test/test_bug838582.html35
-rw-r--r--dom/html/test/test_bug839371.html44
-rw-r--r--dom/html/test/test_bug839913.html14
-rw-r--r--dom/html/test/test_bug841466.html33
-rw-r--r--dom/html/test/test_bug845057.html59
-rw-r--r--dom/html/test/test_bug869040.html36
-rw-r--r--dom/html/test/test_bug870787.html84
-rw-r--r--dom/html/test/test_bug871161.html37
-rw-r--r--dom/html/test/test_bug874758.html31
-rw-r--r--dom/html/test/test_bug879319.html92
-rw-r--r--dom/html/test/test_bug885024.html46
-rw-r--r--dom/html/test/test_bug893537.html45
-rw-r--r--dom/html/test/test_bug95530.html38
-rw-r--r--dom/html/test/test_bug969346.html33
-rw-r--r--dom/html/test/test_bug982039.html46
-rw-r--r--dom/html/test/test_change_crossorigin.html89
-rw-r--r--dom/html/test/test_checked.html347
-rw-r--r--dom/html/test/test_dir_attributes_reflection.html27
-rw-r--r--dom/html/test/test_dl_attributes_reflection.html27
-rw-r--r--dom/html/test/test_document-element-inserted.html54
-rw-r--r--dom/html/test/test_documentAll.html167
-rw-r--r--dom/html/test/test_element_prototype.html32
-rw-r--r--dom/html/test/test_embed_attributes_reflection.html57
-rw-r--r--dom/html/test/test_external_protocol_iframe.html80
-rw-r--r--dom/html/test/test_fakepath.html40
-rw-r--r--dom/html/test/test_filepicker_default_directory.html81
-rw-r--r--dom/html/test/test_focusshift_button.html34
-rw-r--r--dom/html/test/test_form-parsing.html35
-rw-r--r--dom/html/test/test_formData.html50
-rw-r--r--dom/html/test/test_formSubmission.html910
-rw-r--r--dom/html/test/test_formSubmission2.html220
-rw-r--r--dom/html/test/test_formelements.html68
-rw-r--r--dom/html/test/test_fragment_form_pointer.html27
-rw-r--r--dom/html/test/test_frame_count_with_synthetic_doc.html36
-rw-r--r--dom/html/test/test_getElementsByName_after_mutation.html51
-rw-r--r--dom/html/test/test_hidden.html52
-rw-r--r--dom/html/test/test_html_attributes_reflection.html27
-rw-r--r--dom/html/test/test_htmlcollection.html55
-rw-r--r--dom/html/test/test_iframe_sandbox_general.html283
-rw-r--r--dom/html/test/test_iframe_sandbox_inheritance.html202
-rw-r--r--dom/html/test/test_iframe_sandbox_navigation.html285
-rw-r--r--dom/html/test/test_iframe_sandbox_navigation2.html216
-rw-r--r--dom/html/test/test_iframe_sandbox_popups.html78
-rw-r--r--dom/html/test/test_iframe_sandbox_popups_inheritance.html157
-rw-r--r--dom/html/test/test_iframe_sandbox_redirect.html45
-rw-r--r--dom/html/test/test_iframe_sandbox_refresh.html101
-rw-r--r--dom/html/test/test_iframe_sandbox_same_origin.html108
-rw-r--r--dom/html/test/test_iframe_sandbox_workers.html74
-rw-r--r--dom/html/test/test_imageSrcSet.html38
-rw-r--r--dom/html/test/test_image_clone_load.html21
-rw-r--r--dom/html/test/test_img_attributes_reflection.html103
-rw-r--r--dom/html/test/test_input_file_cancel_event.html43
-rw-r--r--dom/html/test/test_input_files_not_nsIFile.html48
-rw-r--r--dom/html/test/test_input_lastInteractiveValue.html134
-rw-r--r--dom/html/test/test_inputmode.html132
-rw-r--r--dom/html/test/test_li_attributes_reflection.html34
-rw-r--r--dom/html/test/test_link_attributes_reflection.html96
-rw-r--r--dom/html/test/test_link_sizes.html35
-rw-r--r--dom/html/test/test_map_attributes_reflection.html27
-rw-r--r--dom/html/test/test_meta_attributes_reflection.html45
-rw-r--r--dom/html/test/test_mod_attributes_reflection.html41
-rw-r--r--dom/html/test/test_multipleFilePicker.html79
-rw-r--r--dom/html/test/test_named_options.html61
-rw-r--r--dom/html/test/test_nested_invalid_fieldsets.html47
-rw-r--r--dom/html/test/test_nestediframe.html55
-rw-r--r--dom/html/test/test_non-ascii-cookie.html69
-rw-r--r--dom/html/test/test_non-ascii-cookie.html^headers^1
-rw-r--r--dom/html/test/test_object_attributes_reflection.html117
-rw-r--r--dom/html/test/test_ol_attributes_reflection.html65
-rw-r--r--dom/html/test/test_option_defaultSelected.html47
-rw-r--r--dom/html/test/test_option_selected_state.html61
-rw-r--r--dom/html/test/test_param_attributes_reflection.html45
-rw-r--r--dom/html/test/test_plugin.tst1
-rw-r--r--dom/html/test/test_q_attributes_reflection.html32
-rw-r--r--dom/html/test/test_restore_from_parser_fragment.html59
-rw-r--r--dom/html/test/test_rowscollection.html69
-rw-r--r--dom/html/test/test_script_module.html31
-rw-r--r--dom/html/test/test_set_input_files.html55
-rw-r--r--dom/html/test/test_srcdoc-2.html57
-rw-r--r--dom/html/test/test_srcdoc.html118
-rw-r--r--dom/html/test/test_style_attributes_reflection.html35
-rw-r--r--dom/html/test/test_track.html62
-rw-r--r--dom/html/test/test_ul_attributes_reflection.html33
-rw-r--r--dom/html/test/test_viewport_resize.html44
-rw-r--r--dom/html/test/test_window_open_close.html53
-rw-r--r--dom/html/test/test_window_open_from_closing.html43
1149 files changed, 125963 insertions, 0 deletions
diff --git a/dom/html/ConstraintValidation.cpp b/dom/html/ConstraintValidation.cpp
new file mode 100644
index 0000000000..1d256a3092
--- /dev/null
+++ b/dom/html/ConstraintValidation.cpp
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ConstraintValidation.h"
+
+#include "mozilla/ErrorResult.h"
+#include "nsAString.h"
+#include "nsIContent.h"
+
+namespace mozilla::dom {
+
+void ConstraintValidation::GetValidationMessage(nsAString& aValidationMessage,
+ ErrorResult& aError) {
+ aValidationMessage.Truncate();
+
+ if (IsCandidateForConstraintValidation() && !IsValid()) {
+ if (GetValidityState(VALIDITY_STATE_CUSTOM_ERROR)) {
+ aValidationMessage.Assign(mCustomValidity);
+ if (aValidationMessage.Length() > sContentSpecifiedMaxLengthMessage) {
+ aValidationMessage.Truncate(sContentSpecifiedMaxLengthMessage);
+ }
+ } else if (GetValidityState(VALIDITY_STATE_TOO_LONG)) {
+ GetValidationMessage(aValidationMessage, VALIDITY_STATE_TOO_LONG);
+ } else if (GetValidityState(VALIDITY_STATE_TOO_SHORT)) {
+ GetValidationMessage(aValidationMessage, VALIDITY_STATE_TOO_SHORT);
+ } else if (GetValidityState(VALIDITY_STATE_VALUE_MISSING)) {
+ GetValidationMessage(aValidationMessage, VALIDITY_STATE_VALUE_MISSING);
+ } else if (GetValidityState(VALIDITY_STATE_TYPE_MISMATCH)) {
+ GetValidationMessage(aValidationMessage, VALIDITY_STATE_TYPE_MISMATCH);
+ } else if (GetValidityState(VALIDITY_STATE_PATTERN_MISMATCH)) {
+ GetValidationMessage(aValidationMessage, VALIDITY_STATE_PATTERN_MISMATCH);
+ } else if (GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW)) {
+ GetValidationMessage(aValidationMessage, VALIDITY_STATE_RANGE_OVERFLOW);
+ } else if (GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW)) {
+ GetValidationMessage(aValidationMessage, VALIDITY_STATE_RANGE_UNDERFLOW);
+ } else if (GetValidityState(VALIDITY_STATE_STEP_MISMATCH)) {
+ GetValidationMessage(aValidationMessage, VALIDITY_STATE_STEP_MISMATCH);
+ } else if (GetValidityState(VALIDITY_STATE_BAD_INPUT)) {
+ GetValidationMessage(aValidationMessage, VALIDITY_STATE_BAD_INPUT);
+ } else {
+ // There should not be other validity states.
+ aError.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+ } else {
+ aValidationMessage.Truncate();
+ }
+}
+
+bool ConstraintValidation::CheckValidity() {
+ nsCOMPtr<nsIContent> content = do_QueryInterface(this);
+ MOZ_ASSERT(content, "This class should be inherited by HTML elements only!");
+ return nsIConstraintValidation::CheckValidity(*content);
+}
+
+ConstraintValidation::ConstraintValidation() = default;
+
+void ConstraintValidation::SetCustomValidity(const nsAString& aError) {
+ mCustomValidity.Assign(aError);
+ SetValidityState(VALIDITY_STATE_CUSTOM_ERROR, !mCustomValidity.IsEmpty());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/ConstraintValidation.h b/dom/html/ConstraintValidation.h
new file mode 100644
index 0000000000..5bd6c89dcb
--- /dev/null
+++ b/dom/html/ConstraintValidation.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ConstraintValidition_h___
+#define mozilla_dom_ConstraintValidition_h___
+
+#include "nsIConstraintValidation.h"
+#include "nsString.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class ConstraintValidation : public nsIConstraintValidation {
+ public:
+ // Web IDL binding methods
+ void GetValidationMessage(nsAString& aValidationMessage,
+ mozilla::ErrorResult& aError);
+ bool CheckValidity();
+
+ protected:
+ // You can't instantiate an object from that class.
+ ConstraintValidation();
+
+ virtual ~ConstraintValidation() = default;
+
+ void SetCustomValidity(const nsAString& aError);
+
+ virtual nsresult GetValidationMessage(nsAString& aValidationMessage,
+ ValidityStateType aType) {
+ return NS_OK;
+ }
+
+ private:
+ /**
+ * The string representing the custom error.
+ */
+ nsString mCustomValidity;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ConstraintValidition_h___
diff --git a/dom/html/CustomStateSet.cpp b/dom/html/CustomStateSet.cpp
new file mode 100644
index 0000000000..4e24551f5e
--- /dev/null
+++ b/dom/html/CustomStateSet.cpp
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CustomStateSet.h"
+#include "mozilla/dom/ElementInternalsBinding.h"
+#include "mozilla/dom/HTMLElement.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/PresShell.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CustomStateSet, mTarget);
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(CustomStateSet)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(CustomStateSet)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CustomStateSet)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+NS_INTERFACE_MAP_END
+
+CustomStateSet::CustomStateSet(HTMLElement* aTarget) : mTarget(aTarget) {}
+
+// WebIDL interface
+nsISupports* CustomStateSet::GetParentObject() const {
+ return ToSupports(mTarget);
+}
+
+JSObject* CustomStateSet::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return CustomStateSet_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void CustomStateSet::Clear(ErrorResult& aRv) {
+ CustomStateSet_Binding::SetlikeHelpers::Clear(this, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ mTarget->EnsureCustomStates().Clear();
+ InvalidateStyleFromCustomStateSetChange();
+}
+
+void CustomStateSet::InvalidateStyleFromCustomStateSetChange() const {
+ Document* doc = mTarget->OwnerDoc();
+
+ PresShell* presShell = doc->GetPresShell();
+ if (!presShell) {
+ return;
+ }
+
+ // TODO: make this more efficient?
+ presShell->DestroyFramesForAndRestyle(mTarget);
+}
+
+bool CustomStateSet::Delete(const nsAString& aState, ErrorResult& aRv) {
+ if (!CustomStateSet_Binding::SetlikeHelpers::Delete(this, aState, aRv) ||
+ aRv.Failed()) {
+ return false;
+ }
+
+ RefPtr<nsAtom> atom = NS_AtomizeMainThread(aState);
+ bool deleted = mTarget->EnsureCustomStates().RemoveElement(atom);
+ if (deleted) {
+ InvalidateStyleFromCustomStateSetChange();
+ }
+ return deleted;
+}
+
+void CustomStateSet::Add(const nsAString& aState, ErrorResult& aRv) {
+ CustomStateSet_Binding::SetlikeHelpers::Add(this, aState, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ RefPtr<nsAtom> atom = NS_AtomizeMainThread(aState);
+ mTarget->EnsureCustomStates().AppendElement(atom);
+ InvalidateStyleFromCustomStateSetChange();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/CustomStateSet.h b/dom/html/CustomStateSet.h
new file mode 100644
index 0000000000..095974b5f4
--- /dev/null
+++ b/dom/html/CustomStateSet.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CustomStateSet_h
+#define mozilla_dom_CustomStateSet_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/ErrorResult.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla::dom {
+
+class HTMLElement;
+class GlobalObject;
+
+class CustomStateSet final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(CustomStateSet)
+
+ explicit CustomStateSet(HTMLElement* aTarget);
+
+ // WebIDL interface
+ nsISupports* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static MOZ_CAN_RUN_SCRIPT_BOUNDARY already_AddRefed<CustomStateSet>
+ Constructor(const GlobalObject& aGlobal, ErrorResult& aRv);
+
+ void InvalidateStyleFromCustomStateSetChange() const;
+
+ MOZ_CAN_RUN_SCRIPT void Clear(ErrorResult& aRv);
+
+ /**
+ * @brief Removes a given string from the state set.
+ */
+ MOZ_CAN_RUN_SCRIPT bool Delete(const nsAString& aState, ErrorResult& aRv);
+
+ /**
+ * @brief Adds a string to this state set.
+ */
+ MOZ_CAN_RUN_SCRIPT void Add(const nsAString& aState, ErrorResult& aRv);
+
+ private:
+ virtual ~CustomStateSet() = default;
+
+ RefPtr<HTMLElement> mTarget;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_CustomStateSet_h
diff --git a/dom/html/ElementInternals.cpp b/dom/html/ElementInternals.cpp
new file mode 100644
index 0000000000..aaf58f818e
--- /dev/null
+++ b/dom/html/ElementInternals.cpp
@@ -0,0 +1,485 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ElementInternals.h"
+
+#include "mozAutoDocUpdate.h"
+#include "mozilla/dom/CustomElementRegistry.h"
+#include "mozilla/dom/CustomEvent.h"
+#include "mozilla/dom/CustomStateSet.h"
+#include "mozilla/dom/ElementInternalsBinding.h"
+#include "mozilla/dom/FormData.h"
+#include "mozilla/dom/HTMLElement.h"
+#include "mozilla/dom/HTMLFieldSetElement.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/dom/MutationObservers.h"
+#include "mozilla/dom/ShadowRoot.h"
+#include "mozilla/dom/ValidityState.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ElementInternals)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ElementInternals)
+ tmp->Unlink();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTarget, mSubmissionValue, mState, mValidity,
+ mValidationAnchor, mCustomStateSet);
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ElementInternals)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTarget, mSubmissionValue, mState,
+ mValidity, mValidationAnchor,
+ mCustomStateSet);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ElementInternals)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ElementInternals)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ElementInternals)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsIFormControl)
+ NS_INTERFACE_MAP_ENTRY(nsIConstraintValidation)
+NS_INTERFACE_MAP_END
+
+ElementInternals::ElementInternals(HTMLElement* aTarget)
+ : nsIFormControl(FormControlType::FormAssociatedCustomElement),
+ mTarget(aTarget),
+ mForm(nullptr),
+ mFieldSet(nullptr),
+ mControlNumber(-1) {}
+
+nsISupports* ElementInternals::GetParentObject() { return ToSupports(mTarget); }
+
+JSObject* ElementInternals::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ElementInternals_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// https://html.spec.whatwg.org/#dom-elementinternals-shadowroot
+ShadowRoot* ElementInternals::GetShadowRoot() const {
+ MOZ_ASSERT(mTarget);
+
+ ShadowRoot* shadowRoot = mTarget->GetShadowRoot();
+ if (shadowRoot && !shadowRoot->IsAvailableToElementInternals()) {
+ return nullptr;
+ }
+
+ return shadowRoot;
+}
+
+// https://html.spec.whatwg.org/commit-snapshots/912a3fe1f29649ccf8229de56f604b3c07ffd242/#dom-elementinternals-setformvalue
+void ElementInternals::SetFormValue(
+ const Nullable<FileOrUSVStringOrFormData>& aValue,
+ const Optional<Nullable<FileOrUSVStringOrFormData>>& aState,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mTarget);
+
+ /**
+ * 1. Let element be this's target element.
+ * 2. If element is not a form-associated custom element, then throw a
+ * "NotSupportedError" DOMException.
+ */
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return;
+ }
+
+ /**
+ * 3. Set target element's submission value to value if value is not a
+ * FormData object, or to a clone of the entry list associated with value
+ * otherwise.
+ */
+ mSubmissionValue.SetNull();
+ if (!aValue.IsNull()) {
+ const FileOrUSVStringOrFormData& value = aValue.Value();
+ OwningFileOrUSVStringOrFormData& owningValue = mSubmissionValue.SetValue();
+ if (value.IsFormData()) {
+ owningValue.SetAsFormData() = value.GetAsFormData().Clone();
+ } else if (value.IsFile()) {
+ owningValue.SetAsFile() = &value.GetAsFile();
+ } else {
+ owningValue.SetAsUSVString() = value.GetAsUSVString();
+ }
+ }
+
+ /**
+ * 4. If the state argument of the function is omitted, set element's state to
+ * its submission value.
+ */
+ if (!aState.WasPassed()) {
+ mState = mSubmissionValue;
+ return;
+ }
+
+ /**
+ * 5. Otherwise, if state is a FormData object, set element's state to clone
+ * of the entry list associated with state.
+ * 6. Otherwise, set element's state to state.
+ */
+ mState.SetNull();
+ if (!aState.Value().IsNull()) {
+ const FileOrUSVStringOrFormData& state = aState.Value().Value();
+ OwningFileOrUSVStringOrFormData& owningState = mState.SetValue();
+ if (state.IsFormData()) {
+ owningState.SetAsFormData() = state.GetAsFormData().Clone();
+ } else if (state.IsFile()) {
+ owningState.SetAsFile() = &state.GetAsFile();
+ } else {
+ owningState.SetAsUSVString() = state.GetAsUSVString();
+ }
+ }
+}
+
+// https://html.spec.whatwg.org/#dom-elementinternals-form
+HTMLFormElement* ElementInternals::GetForm(ErrorResult& aRv) const {
+ MOZ_ASSERT(mTarget);
+
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return nullptr;
+ }
+ return GetForm();
+}
+
+// https://html.spec.whatwg.org/commit-snapshots/3ad5159be8f27e110a70cefadcb50fc45ec21b05/#dom-elementinternals-setvalidity
+void ElementInternals::SetValidity(
+ const ValidityStateFlags& aFlags, const Optional<nsAString>& aMessage,
+ const Optional<NonNull<nsGenericHTMLElement>>& aAnchor, ErrorResult& aRv) {
+ MOZ_ASSERT(mTarget);
+
+ /**
+ * 1. Let element be this's target element.
+ * 2. If element is not a form-associated custom element, then throw a
+ * "NotSupportedError" DOMException.
+ */
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return;
+ }
+
+ /**
+ * 3. If flags contains one or more true values and message is not given or is
+ * the empty string, then throw a TypeError.
+ */
+ if ((aFlags.mBadInput || aFlags.mCustomError || aFlags.mPatternMismatch ||
+ aFlags.mRangeOverflow || aFlags.mRangeUnderflow ||
+ aFlags.mStepMismatch || aFlags.mTooLong || aFlags.mTooShort ||
+ aFlags.mTypeMismatch || aFlags.mValueMissing) &&
+ (!aMessage.WasPassed() || aMessage.Value().IsEmpty())) {
+ aRv.ThrowTypeError("Need to provide validation message");
+ return;
+ }
+
+ /**
+ * 4. For each entry flag → value of flags, set element's validity flag with
+ * the name flag to value.
+ */
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING, aFlags.mValueMissing);
+ SetValidityState(VALIDITY_STATE_TYPE_MISMATCH, aFlags.mTypeMismatch);
+ SetValidityState(VALIDITY_STATE_PATTERN_MISMATCH, aFlags.mPatternMismatch);
+ SetValidityState(VALIDITY_STATE_TOO_LONG, aFlags.mTooLong);
+ SetValidityState(VALIDITY_STATE_TOO_SHORT, aFlags.mTooShort);
+ SetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW, aFlags.mRangeUnderflow);
+ SetValidityState(VALIDITY_STATE_RANGE_OVERFLOW, aFlags.mRangeOverflow);
+ SetValidityState(VALIDITY_STATE_STEP_MISMATCH, aFlags.mStepMismatch);
+ SetValidityState(VALIDITY_STATE_BAD_INPUT, aFlags.mBadInput);
+ SetValidityState(VALIDITY_STATE_CUSTOM_ERROR, aFlags.mCustomError);
+ mTarget->UpdateValidityElementStates(true);
+
+ /**
+ * 5. Set element's validation message to the empty string if message is not
+ * given or all of element's validity flags are false, or to message
+ * otherwise.
+ * 6. If element's customError validity flag is true, then set element's
+ * custom validity error message to element's validation message.
+ * Otherwise, set element's custom validity error message to the empty
+ * string.
+ */
+ mValidationMessage =
+ (!aMessage.WasPassed() || IsValid()) ? EmptyString() : aMessage.Value();
+
+ /**
+ * 7. Set element's validation anchor to null if anchor is not given.
+ * Otherwise, if anchor is not a shadow-including descendant of element,
+ * then throw a "NotFoundError" DOMException. Otherwise, set element's
+ * validation anchor to anchor.
+ */
+ nsGenericHTMLElement* anchor =
+ aAnchor.WasPassed() ? &aAnchor.Value() : nullptr;
+ // TODO: maybe create something like IsShadowIncludingDescendantOf if there
+ // are other places also need such check.
+ if (anchor && (anchor == mTarget ||
+ !anchor->IsShadowIncludingInclusiveDescendantOf(mTarget))) {
+ aRv.ThrowNotFoundError(
+ "Validation anchor is not a shadow-including descendant of target"
+ "element");
+ return;
+ }
+ mValidationAnchor = anchor;
+}
+
+// https://html.spec.whatwg.org/#dom-elementinternals-willvalidate
+bool ElementInternals::GetWillValidate(ErrorResult& aRv) const {
+ MOZ_ASSERT(mTarget);
+
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return false;
+ }
+ return WillValidate();
+}
+
+// https://html.spec.whatwg.org/#dom-elementinternals-validity
+ValidityState* ElementInternals::GetValidity(ErrorResult& aRv) {
+ MOZ_ASSERT(mTarget);
+
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return nullptr;
+ }
+ return Validity();
+}
+
+// https://html.spec.whatwg.org/#dom-elementinternals-validationmessage
+void ElementInternals::GetValidationMessage(nsAString& aValidationMessage,
+ ErrorResult& aRv) const {
+ MOZ_ASSERT(mTarget);
+
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return;
+ }
+ aValidationMessage = mValidationMessage;
+}
+
+// https://html.spec.whatwg.org/#dom-elementinternals-checkvalidity
+bool ElementInternals::CheckValidity(ErrorResult& aRv) {
+ MOZ_ASSERT(mTarget);
+
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return false;
+ }
+ return nsIConstraintValidation::CheckValidity(*mTarget);
+}
+
+// https://html.spec.whatwg.org/#dom-elementinternals-reportvalidity
+bool ElementInternals::ReportValidity(ErrorResult& aRv) {
+ MOZ_ASSERT(mTarget);
+
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return false;
+ }
+
+ bool defaultAction = true;
+ if (nsIConstraintValidation::CheckValidity(*mTarget, &defaultAction)) {
+ return true;
+ }
+
+ if (!defaultAction) {
+ return false;
+ }
+
+ AutoTArray<RefPtr<Element>, 1> invalidElements;
+ invalidElements.AppendElement(mTarget);
+
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(mTarget->GetOwnerGlobal())) {
+ return false;
+ }
+ JS::Rooted<JS::Value> detail(jsapi.cx());
+ if (!ToJSValue(jsapi.cx(), invalidElements, &detail)) {
+ return false;
+ }
+
+ RefPtr<CustomEvent> event =
+ NS_NewDOMCustomEvent(mTarget->OwnerDoc(), nullptr, nullptr);
+ event->InitCustomEvent(jsapi.cx(), u"MozInvalidForm"_ns,
+ /* CanBubble */ true,
+ /* Cancelable */ true, detail);
+ event->SetTrusted(true);
+ event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
+ mTarget->DispatchEvent(*event);
+
+ return false;
+}
+
+// https://html.spec.whatwg.org/#dom-elementinternals-labels
+already_AddRefed<nsINodeList> ElementInternals::GetLabels(
+ ErrorResult& aRv) const {
+ MOZ_ASSERT(mTarget);
+
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return nullptr;
+ }
+ return mTarget->Labels();
+}
+
+nsGenericHTMLElement* ElementInternals::GetValidationAnchor(
+ ErrorResult& aRv) const {
+ MOZ_ASSERT(mTarget);
+
+ if (!mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return nullptr;
+ }
+ return mValidationAnchor;
+}
+
+CustomStateSet* ElementInternals::States() {
+ if (!mCustomStateSet) {
+ mCustomStateSet = new CustomStateSet(mTarget);
+ }
+ return mCustomStateSet;
+}
+
+void ElementInternals::SetForm(HTMLFormElement* aForm) { mForm = aForm; }
+
+void ElementInternals::ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) {
+ if (mTarget) {
+ mTarget->ClearForm(aRemoveFromForm, aUnbindOrDelete);
+ }
+}
+
+NS_IMETHODIMP ElementInternals::Reset() {
+ if (mTarget) {
+ MOZ_ASSERT(mTarget->IsFormAssociatedElement());
+ nsContentUtils::EnqueueLifecycleCallback(ElementCallbackType::eFormReset,
+ mTarget, {});
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP ElementInternals::SubmitNamesValues(FormData* aFormData) {
+ if (!mTarget) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ MOZ_ASSERT(mTarget->IsFormAssociatedElement());
+
+ // https://html.spec.whatwg.org/#face-entry-construction
+ if (!mSubmissionValue.IsNull()) {
+ if (mSubmissionValue.Value().IsFormData()) {
+ aFormData->Append(mSubmissionValue.Value().GetAsFormData());
+ return NS_OK;
+ }
+
+ // Get the name
+ nsAutoString name;
+ if (!mTarget->GetAttr(nsGkAtoms::name, name) || name.IsEmpty()) {
+ return NS_OK;
+ }
+
+ if (mSubmissionValue.Value().IsUSVString()) {
+ return aFormData->AddNameValuePair(
+ name, mSubmissionValue.Value().GetAsUSVString());
+ }
+
+ return aFormData->AddNameBlobPair(name,
+ mSubmissionValue.Value().GetAsFile());
+ }
+ return NS_OK;
+}
+
+void ElementInternals::UpdateFormOwner() {
+ if (mTarget) {
+ mTarget->UpdateFormOwner();
+ }
+}
+
+void ElementInternals::UpdateBarredFromConstraintValidation() {
+ if (mTarget) {
+ MOZ_ASSERT(mTarget->IsFormAssociatedElement());
+ SetBarredFromConstraintValidation(
+ mTarget->IsDisabled() || mTarget->HasAttr(nsGkAtoms::readonly) ||
+ mTarget->HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR));
+ }
+}
+
+void ElementInternals::Unlink() {
+ if (mForm) {
+ // Don't notify, since we're being destroyed in any case.
+ ClearForm(true, true);
+ MOZ_DIAGNOSTIC_ASSERT(!mForm);
+ }
+ if (mFieldSet) {
+ mFieldSet->RemoveElement(mTarget);
+ mFieldSet = nullptr;
+ }
+}
+
+void ElementInternals::GetAttr(const nsAtom* aName, nsAString& aResult) const {
+ MOZ_ASSERT(aResult.IsEmpty(), "Should have empty string coming in");
+
+ const nsAttrValue* val = mAttrs.GetAttr(aName);
+ if (val) {
+ val->ToString(aResult);
+ return;
+ }
+ SetDOMStringToNull(aResult);
+}
+
+nsresult ElementInternals::SetAttr(nsAtom* aName, const nsAString& aValue) {
+ Document* document = mTarget->GetComposedDoc();
+ mozAutoDocUpdate updateBatch(document, true);
+
+ uint8_t modType = mAttrs.HasAttr(aName) ? MutationEvent_Binding::MODIFICATION
+ : MutationEvent_Binding::ADDITION;
+
+ MutationObservers::NotifyARIAAttributeDefaultWillChange(mTarget, aName,
+ modType);
+
+ bool attrHadValue;
+ nsAttrValue attrValue(aValue);
+ nsresult rs = mAttrs.SetAndSwapAttr(aName, attrValue, &attrHadValue);
+ nsMutationGuard::DidMutate();
+
+ MutationObservers::NotifyARIAAttributeDefaultChanged(mTarget, aName, modType);
+
+ return rs;
+}
+
+DocGroup* ElementInternals::GetDocGroup() {
+ return mTarget->OwnerDoc()->GetDocGroup();
+}
+
+void ElementInternals::RestoreFormValue(
+ Nullable<OwningFileOrUSVStringOrFormData>&& aValue,
+ Nullable<OwningFileOrUSVStringOrFormData>&& aState) {
+ mSubmissionValue = aValue;
+ mState = aState;
+
+ if (!mState.IsNull()) {
+ LifecycleCallbackArgs args;
+ args.mState = mState;
+ args.mReason = RestoreReason::Restore;
+ nsContentUtils::EnqueueLifecycleCallback(
+ ElementCallbackType::eFormStateRestore, mTarget, args);
+ }
+}
+
+void ElementInternals::InitializeControlNumber() {
+ MOZ_ASSERT(mControlNumber == -1,
+ "FACE control number should only be initialized once!");
+ mControlNumber = mTarget->OwnerDoc()->GetNextControlNumber();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/ElementInternals.h b/dom/html/ElementInternals.h
new file mode 100644
index 0000000000..7ced3b9771
--- /dev/null
+++ b/dom/html/ElementInternals.h
@@ -0,0 +1,220 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ElementInternals_h
+#define mozilla_dom_ElementInternals_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/ElementInternalsBinding.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/CustomStateSet.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIConstraintValidation.h"
+#include "nsIFormControl.h"
+#include "nsWrapperCache.h"
+#include "AttrArray.h"
+#include "nsGkAtoms.h"
+
+#define ARIA_REFLECT_ATTR(method, attr) \
+ void Get##method(nsAString& aValue) const { \
+ GetAttr(nsGkAtoms::attr, aValue); \
+ } \
+ void Set##method(const nsAString& aValue, ErrorResult& aResult) { \
+ aResult = ErrorResult(SetAttr(nsGkAtoms::attr, aValue)); \
+ }
+
+class nsINodeList;
+class nsGenericHTMLElement;
+
+namespace mozilla::dom {
+
+class DocGroup;
+class HTMLElement;
+class HTMLFieldSetElement;
+class HTMLFormElement;
+class ShadowRoot;
+class ValidityState;
+
+class ElementInternals final : public nsIFormControl,
+ public nsIConstraintValidation,
+ public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(ElementInternals,
+ nsIFormControl)
+
+ explicit ElementInternals(HTMLElement* aTarget);
+
+ nsISupports* GetParentObject();
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL
+ ShadowRoot* GetShadowRoot() const;
+ void SetFormValue(const Nullable<FileOrUSVStringOrFormData>& aValue,
+ const Optional<Nullable<FileOrUSVStringOrFormData>>& aState,
+ ErrorResult& aRv);
+ mozilla::dom::HTMLFormElement* GetForm(ErrorResult& aRv) const;
+ void SetValidity(const ValidityStateFlags& aFlags,
+ const Optional<nsAString>& aMessage,
+ const Optional<NonNull<nsGenericHTMLElement>>& aAnchor,
+ ErrorResult& aRv);
+ bool GetWillValidate(ErrorResult& aRv) const;
+ ValidityState* GetValidity(ErrorResult& aRv);
+ void GetValidationMessage(nsAString& aValidationMessage,
+ ErrorResult& aRv) const;
+ bool CheckValidity(ErrorResult& aRv);
+ bool ReportValidity(ErrorResult& aRv);
+ already_AddRefed<nsINodeList> GetLabels(ErrorResult& aRv) const;
+ nsGenericHTMLElement* GetValidationAnchor(ErrorResult& aRv) const;
+ CustomStateSet* States();
+
+ // nsIFormControl
+ mozilla::dom::HTMLFieldSetElement* GetFieldSet() override {
+ return mFieldSet;
+ }
+ mozilla::dom::HTMLFormElement* GetForm() const override { return mForm; }
+ void SetForm(mozilla::dom::HTMLFormElement* aForm) override;
+ void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) override;
+ NS_IMETHOD Reset() override;
+ NS_IMETHOD SubmitNamesValues(mozilla::dom::FormData* aFormData) override;
+ int32_t GetParserInsertedControlNumberForStateKey() const override {
+ return mControlNumber;
+ }
+
+ void SetFieldSet(mozilla::dom::HTMLFieldSetElement* aFieldSet) {
+ mFieldSet = aFieldSet;
+ }
+
+ const Nullable<OwningFileOrUSVStringOrFormData>& GetFormSubmissionValue()
+ const {
+ return mSubmissionValue;
+ }
+
+ const Nullable<OwningFileOrUSVStringOrFormData>& GetFormState() const {
+ return mState;
+ }
+
+ void RestoreFormValue(Nullable<OwningFileOrUSVStringOrFormData>&& aValue,
+ Nullable<OwningFileOrUSVStringOrFormData>&& aState);
+
+ const nsCString& GetStateKey() const { return mStateKey; }
+ void SetStateKey(nsCString&& key) {
+ MOZ_ASSERT(mStateKey.IsEmpty(), "FACE state key should only be set once!");
+ mStateKey = key;
+ }
+ void InitializeControlNumber();
+
+ void UpdateFormOwner();
+ void UpdateBarredFromConstraintValidation();
+
+ void Unlink();
+
+ // AccessibilityRole
+ ARIA_REFLECT_ATTR(Role, role)
+
+ // AriaAttributes
+ ARIA_REFLECT_ATTR(AriaAtomic, aria_atomic)
+ ARIA_REFLECT_ATTR(AriaAutoComplete, aria_autocomplete)
+ ARIA_REFLECT_ATTR(AriaBusy, aria_busy)
+ ARIA_REFLECT_ATTR(AriaChecked, aria_checked)
+ ARIA_REFLECT_ATTR(AriaColCount, aria_colcount)
+ ARIA_REFLECT_ATTR(AriaColIndex, aria_colindex)
+ ARIA_REFLECT_ATTR(AriaColIndexText, aria_colindextext)
+ ARIA_REFLECT_ATTR(AriaColSpan, aria_colspan)
+ ARIA_REFLECT_ATTR(AriaCurrent, aria_current)
+ ARIA_REFLECT_ATTR(AriaDescription, aria_description)
+ ARIA_REFLECT_ATTR(AriaDisabled, aria_disabled)
+ ARIA_REFLECT_ATTR(AriaExpanded, aria_expanded)
+ ARIA_REFLECT_ATTR(AriaHasPopup, aria_haspopup)
+ ARIA_REFLECT_ATTR(AriaHidden, aria_hidden)
+ ARIA_REFLECT_ATTR(AriaInvalid, aria_invalid)
+ ARIA_REFLECT_ATTR(AriaKeyShortcuts, aria_keyshortcuts)
+ ARIA_REFLECT_ATTR(AriaLabel, aria_label)
+ ARIA_REFLECT_ATTR(AriaLevel, aria_level)
+ ARIA_REFLECT_ATTR(AriaLive, aria_live)
+ ARIA_REFLECT_ATTR(AriaModal, aria_modal)
+ ARIA_REFLECT_ATTR(AriaMultiLine, aria_multiline)
+ ARIA_REFLECT_ATTR(AriaMultiSelectable, aria_multiselectable)
+ ARIA_REFLECT_ATTR(AriaOrientation, aria_orientation)
+ ARIA_REFLECT_ATTR(AriaPlaceholder, aria_placeholder)
+ ARIA_REFLECT_ATTR(AriaPosInSet, aria_posinset)
+ ARIA_REFLECT_ATTR(AriaPressed, aria_pressed)
+ ARIA_REFLECT_ATTR(AriaReadOnly, aria_readonly)
+ ARIA_REFLECT_ATTR(AriaRelevant, aria_relevant)
+ ARIA_REFLECT_ATTR(AriaRequired, aria_required)
+ ARIA_REFLECT_ATTR(AriaRoleDescription, aria_roledescription)
+ ARIA_REFLECT_ATTR(AriaRowCount, aria_rowcount)
+ ARIA_REFLECT_ATTR(AriaRowIndex, aria_rowindex)
+ ARIA_REFLECT_ATTR(AriaRowIndexText, aria_rowindextext)
+ ARIA_REFLECT_ATTR(AriaRowSpan, aria_rowspan)
+ ARIA_REFLECT_ATTR(AriaSelected, aria_selected)
+ ARIA_REFLECT_ATTR(AriaSetSize, aria_setsize)
+ ARIA_REFLECT_ATTR(AriaSort, aria_sort)
+ ARIA_REFLECT_ATTR(AriaValueMax, aria_valuemax)
+ ARIA_REFLECT_ATTR(AriaValueMin, aria_valuemin)
+ ARIA_REFLECT_ATTR(AriaValueNow, aria_valuenow)
+ ARIA_REFLECT_ATTR(AriaValueText, aria_valuetext)
+
+ void GetAttr(const nsAtom* aName, nsAString& aResult) const;
+
+ nsresult SetAttr(nsAtom* aName, const nsAString& aValue);
+
+ const AttrArray& GetAttrs() const { return mAttrs; }
+
+ DocGroup* GetDocGroup();
+
+ private:
+ ~ElementInternals() = default;
+
+ // It's a target element which is a custom element.
+ RefPtr<HTMLElement> mTarget;
+
+ // The form that contains the target element.
+ // It's safe to use raw pointer because it will be reset via
+ // CustomElementData::Unlink when mTarget is released or unlinked.
+ HTMLFormElement* mForm;
+
+ // This is a pointer to the target element's closest fieldset parent if any.
+ // It's safe to use raw pointer because it will be reset via
+ // CustomElementData::Unlink when mTarget is released or unlinked.
+ HTMLFieldSetElement* mFieldSet;
+
+ // https://html.spec.whatwg.org/#face-submission-value
+ Nullable<OwningFileOrUSVStringOrFormData> mSubmissionValue;
+
+ // https://html.spec.whatwg.org/#face-state
+ // TODO: Bug 1734841 - Figure out how to support autocomplete for
+ // form-associated custom element.
+ Nullable<OwningFileOrUSVStringOrFormData> mState;
+
+ // https://html.spec.whatwg.org/#face-validation-message
+ nsString mValidationMessage;
+
+ // https://html.spec.whatwg.org/#face-validation-anchor
+ RefPtr<nsGenericHTMLElement> mValidationAnchor;
+
+ AttrArray mAttrs;
+
+ // Used to store the key to a form-associated custom element in the current
+ // session. Is empty until element has been upgraded.
+ nsCString mStateKey;
+
+ RefPtr<CustomStateSet> mCustomStateSet;
+
+ // A number for a form-associated custom element that is unique within its
+ // owner document. This is only set to a number for elements inserted into the
+ // document by the parser from the network. Otherwise, it is -1.
+ int32_t mControlNumber;
+};
+
+} // namespace mozilla::dom
+
+#undef ARIA_REFLECT_ATTR
+
+#endif // mozilla_dom_ElementInternals_h
diff --git a/dom/html/FetchPriority.cpp b/dom/html/FetchPriority.cpp
new file mode 100644
index 0000000000..259c05c6c3
--- /dev/null
+++ b/dom/html/FetchPriority.cpp
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/FetchPriority.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Logging.h"
+#include "mozilla/dom/RequestBinding.h"
+#include "nsISupportsPriority.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::dom {
+const char* kFetchPriorityAttributeValueHigh = "high";
+const char* kFetchPriorityAttributeValueLow = "low";
+const char* kFetchPriorityAttributeValueAuto = "auto";
+
+FetchPriority ToFetchPriority(RequestPriority aRequestPriority) {
+ switch (aRequestPriority) {
+ case RequestPriority::High: {
+ return FetchPriority::High;
+ }
+ case RequestPriority::Low: {
+ return FetchPriority::Low;
+ }
+ case RequestPriority::Auto: {
+ return FetchPriority::Auto;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE();
+ return FetchPriority::Auto;
+ }
+ }
+}
+
+#ifdef DEBUG
+constexpr auto kPriorityHighest = "PRIORITY_HIGHEST"_ns;
+constexpr auto kPriorityHigh = "PRIORITY_HIGH"_ns;
+constexpr auto kPriorityNormal = "PRIORITY_NORMAL"_ns;
+constexpr auto kPriorityLow = "PRIORITY_LOW"_ns;
+constexpr auto kPriorityLowest = "PRIORITY_LOWEST"_ns;
+constexpr auto kPriorityUnknown = "UNKNOWN"_ns;
+
+/**
+ * See <nsISupportsPriority.idl>.
+ */
+static void SupportsPriorityToString(int32_t aSupportsPriority,
+ nsACString& aResult) {
+ switch (aSupportsPriority) {
+ case nsISupportsPriority::PRIORITY_HIGHEST: {
+ aResult = kPriorityHighest;
+ break;
+ }
+ case nsISupportsPriority::PRIORITY_HIGH: {
+ aResult = kPriorityHigh;
+ break;
+ }
+ case nsISupportsPriority::PRIORITY_NORMAL: {
+ aResult = kPriorityNormal;
+ break;
+ }
+ case nsISupportsPriority::PRIORITY_LOW: {
+ aResult = kPriorityLow;
+ break;
+ }
+ case nsISupportsPriority::PRIORITY_LOWEST: {
+ aResult = kPriorityLowest;
+ break;
+ }
+ default: {
+ aResult = kPriorityUnknown;
+ break;
+ }
+ }
+}
+
+static const char* ToString(FetchPriority aFetchPriority) {
+ switch (aFetchPriority) {
+ case FetchPriority::Auto: {
+ return kFetchPriorityAttributeValueAuto;
+ }
+ case FetchPriority::Low: {
+ return kFetchPriorityAttributeValueLow;
+ }
+ case FetchPriority::High: {
+ return kFetchPriorityAttributeValueHigh;
+ }
+ default: {
+ MOZ_ASSERT_UNREACHABLE();
+ return kFetchPriorityAttributeValueAuto;
+ }
+ }
+}
+#endif // DEBUG
+
+void LogPriorityMapping(LazyLogModule& aLazyLogModule,
+ FetchPriority aFetchPriority,
+ int32_t aSupportsPriority) {
+#ifdef DEBUG
+ nsDependentCString supportsPriority;
+ SupportsPriorityToString(aSupportsPriority, supportsPriority);
+ MOZ_LOG(aLazyLogModule, LogLevel::Debug,
+ ("Mapping priority: %s -> %s", ToString(aFetchPriority),
+ supportsPriority.get()));
+#endif // DEBUG
+}
+} // namespace mozilla::dom
diff --git a/dom/html/FetchPriority.h b/dom/html/FetchPriority.h
new file mode 100644
index 0000000000..5719577771
--- /dev/null
+++ b/dom/html/FetchPriority.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FetchPriority_h
+#define mozilla_dom_FetchPriority_h
+
+#include <cstdint>
+
+namespace mozilla {
+class LazyLogModule;
+
+namespace dom {
+
+enum class RequestPriority : uint8_t;
+
+// <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>.
+enum class FetchPriority : uint8_t { High, Low, Auto };
+
+FetchPriority ToFetchPriority(RequestPriority aRequestPriority);
+
+// @param aSupportsPriority see <nsISupportsPriority.idl>.
+void LogPriorityMapping(LazyLogModule& aLazyLogModule,
+ FetchPriority aFetchPriority,
+ int32_t aSupportsPriority);
+
+extern const char* kFetchPriorityAttributeValueHigh;
+extern const char* kFetchPriorityAttributeValueLow;
+extern const char* kFetchPriorityAttributeValueAuto;
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_FetchPriority_h
diff --git a/dom/html/HTMLAllCollection.cpp b/dom/html/HTMLAllCollection.cpp
new file mode 100644
index 0000000000..b26c96b3c2
--- /dev/null
+++ b/dom/html/HTMLAllCollection.cpp
@@ -0,0 +1,195 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLAllCollection.h"
+
+#include "jsfriendapi.h"
+#include "mozilla/dom/HTMLAllCollectionBinding.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/Element.h"
+#include "nsContentList.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+HTMLAllCollection::HTMLAllCollection(mozilla::dom::Document* aDocument)
+ : mDocument(aDocument) {
+ MOZ_ASSERT(mDocument);
+}
+
+HTMLAllCollection::~HTMLAllCollection() = default;
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HTMLAllCollection, mDocument, mCollection,
+ mNamedMap)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLAllCollection)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLAllCollection)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLAllCollection)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+nsINode* HTMLAllCollection::GetParentObject() const { return mDocument; }
+
+uint32_t HTMLAllCollection::Length() { return Collection()->Length(true); }
+
+Element* HTMLAllCollection::Item(uint32_t aIndex) {
+ nsIContent* item = Collection()->Item(aIndex);
+ return item ? item->AsElement() : nullptr;
+}
+
+void HTMLAllCollection::Item(const Optional<nsAString>& aNameOrIndex,
+ Nullable<OwningHTMLCollectionOrElement>& aResult) {
+ if (!aNameOrIndex.WasPassed()) {
+ aResult.SetNull();
+ return;
+ }
+
+ const nsAString& nameOrIndex = aNameOrIndex.Value();
+ uint32_t indexVal;
+ if (js::StringIsArrayIndex(nameOrIndex.BeginReading(), nameOrIndex.Length(),
+ &indexVal)) {
+ Element* element = Item(indexVal);
+ if (element) {
+ aResult.SetValue().SetAsElement() = element;
+ } else {
+ aResult.SetNull();
+ }
+ return;
+ }
+
+ NamedItem(nameOrIndex, aResult);
+}
+
+nsContentList* HTMLAllCollection::Collection() {
+ if (!mCollection) {
+ Document* document = mDocument;
+ mCollection = document->GetElementsByTagName(u"*"_ns);
+ MOZ_ASSERT(mCollection);
+ }
+ return mCollection;
+}
+
+static bool IsAllNamedElement(nsIContent* aContent) {
+ return aContent->IsAnyOfHTMLElements(
+ nsGkAtoms::a, nsGkAtoms::button, nsGkAtoms::embed, nsGkAtoms::form,
+ nsGkAtoms::iframe, nsGkAtoms::img, nsGkAtoms::input, nsGkAtoms::map,
+ nsGkAtoms::meta, nsGkAtoms::object, nsGkAtoms::select,
+ nsGkAtoms::textarea, nsGkAtoms::frame, nsGkAtoms::frameset);
+}
+
+static bool DocAllResultMatch(Element* aElement, int32_t aNamespaceID,
+ nsAtom* aAtom, void* aData) {
+ if (aElement->GetID() == aAtom) {
+ return true;
+ }
+
+ nsGenericHTMLElement* elm = nsGenericHTMLElement::FromNode(aElement);
+ if (!elm) {
+ return false;
+ }
+
+ if (!IsAllNamedElement(elm)) {
+ return false;
+ }
+
+ const nsAttrValue* val = elm->GetParsedAttr(nsGkAtoms::name);
+ return val && val->Type() == nsAttrValue::eAtom &&
+ val->GetAtomValue() == aAtom;
+}
+
+nsContentList* HTMLAllCollection::GetDocumentAllList(const nsAString& aID) {
+ return mNamedMap
+ .LookupOrInsertWith(aID,
+ [this, &aID] {
+ RefPtr<nsAtom> id = NS_Atomize(aID);
+ return new nsContentList(mDocument,
+ DocAllResultMatch, nullptr,
+ nullptr, true, id);
+ })
+ .get();
+}
+
+void HTMLAllCollection::NamedGetter(
+ const nsAString& aID, bool& aFound,
+ Nullable<OwningHTMLCollectionOrElement>& aResult) {
+ if (aID.IsEmpty()) {
+ aFound = false;
+ aResult.SetNull();
+ return;
+ }
+
+ nsContentList* docAllList = GetDocumentAllList(aID);
+ if (!docAllList) {
+ aFound = false;
+ aResult.SetNull();
+ return;
+ }
+
+ // Check if there are more than 1 entries. Do this by getting the second one
+ // rather than the length since getting the length always requires walking
+ // the entire document.
+ if (docAllList->Item(1, true)) {
+ aFound = true;
+ aResult.SetValue().SetAsHTMLCollection() = docAllList;
+ return;
+ }
+
+ // There's only 0 or 1 items. Return the first one or null.
+ if (nsIContent* node = docAllList->Item(0, true)) {
+ aFound = true;
+ aResult.SetValue().SetAsElement() = node->AsElement();
+ return;
+ }
+
+ aFound = false;
+ aResult.SetNull();
+}
+
+void HTMLAllCollection::GetSupportedNames(nsTArray<nsString>& aNames) {
+ // XXXbz this is very similar to nsContentList::GetSupportedNames,
+ // but has to check IsAllNamedElement for the name case.
+ AutoTArray<nsAtom*, 8> atoms;
+ for (uint32_t i = 0; i < Length(); ++i) {
+ nsIContent* content = Item(i);
+ if (content->HasID()) {
+ nsAtom* id = content->GetID();
+ MOZ_ASSERT(id != nsGkAtoms::_empty, "Empty ids don't get atomized");
+ if (!atoms.Contains(id)) {
+ atoms.AppendElement(id);
+ }
+ }
+
+ nsGenericHTMLElement* el = nsGenericHTMLElement::FromNode(content);
+ if (el) {
+ // Note: nsINode::HasName means the name is exposed on the document,
+ // which is false for options, so we don't check it here.
+ const nsAttrValue* val = el->GetParsedAttr(nsGkAtoms::name);
+ if (val && val->Type() == nsAttrValue::eAtom &&
+ IsAllNamedElement(content)) {
+ nsAtom* name = val->GetAtomValue();
+ MOZ_ASSERT(name != nsGkAtoms::_empty, "Empty names don't get atomized");
+ if (!atoms.Contains(name)) {
+ atoms.AppendElement(name);
+ }
+ }
+ }
+ }
+
+ uint32_t atomsLen = atoms.Length();
+ nsString* names = aNames.AppendElements(atomsLen);
+ for (uint32_t i = 0; i < atomsLen; ++i) {
+ atoms[i]->ToString(names[i]);
+ }
+}
+
+JSObject* HTMLAllCollection::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLAllCollection_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLAllCollection.h b/dom/html/HTMLAllCollection.h
new file mode 100644
index 0000000000..6179951175
--- /dev/null
+++ b/dom/html/HTMLAllCollection.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLAllCollection_h
+#define mozilla_dom_HTMLAllCollection_h
+
+#include "mozilla/dom/Document.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupportsImpl.h"
+#include "nsRefPtrHashtable.h"
+#include "nsWrapperCache.h"
+
+#include <stdint.h>
+
+class nsContentList;
+class nsINode;
+
+namespace mozilla::dom {
+
+class Document;
+class Element;
+class OwningHTMLCollectionOrElement;
+template <typename>
+struct Nullable;
+template <typename>
+class Optional;
+
+class HTMLAllCollection final : public nsISupports, public nsWrapperCache {
+ ~HTMLAllCollection();
+
+ public:
+ explicit HTMLAllCollection(mozilla::dom::Document* aDocument);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(HTMLAllCollection)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsINode* GetParentObject() const;
+
+ uint32_t Length();
+ Element* IndexedGetter(uint32_t aIndex, bool& aFound) {
+ Element* result = Item(aIndex);
+ aFound = !!result;
+ return result;
+ }
+
+ void NamedItem(const nsAString& aName,
+ Nullable<OwningHTMLCollectionOrElement>& aResult) {
+ bool found = false;
+ NamedGetter(aName, found, aResult);
+ }
+ void NamedGetter(const nsAString& aName, bool& aFound,
+ Nullable<OwningHTMLCollectionOrElement>& aResult);
+ void GetSupportedNames(nsTArray<nsString>& aNames);
+
+ void Item(const Optional<nsAString>& aNameOrIndex,
+ Nullable<OwningHTMLCollectionOrElement>& aResult);
+
+ void LegacyCall(JS::Handle<JS::Value>,
+ const Optional<nsAString>& aNameOrIndex,
+ Nullable<OwningHTMLCollectionOrElement>& aResult) {
+ Item(aNameOrIndex, aResult);
+ }
+
+ private:
+ nsContentList* Collection();
+
+ /**
+ * Returns the HTMLCollection for document.all[aID], or null if there isn't
+ * one.
+ */
+ nsContentList* GetDocumentAllList(const nsAString& aID);
+
+ /**
+ * Helper for indexed getter and spec Item() method.
+ */
+ Element* Item(uint32_t aIndex);
+
+ RefPtr<mozilla::dom::Document> mDocument;
+ RefPtr<nsContentList> mCollection;
+ nsRefPtrHashtable<nsStringHashKey, nsContentList> mNamedMap;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLAllCollection_h
diff --git a/dom/html/HTMLAnchorElement.cpp b/dom/html/HTMLAnchorElement.cpp
new file mode 100644
index 0000000000..a5d958139e
--- /dev/null
+++ b/dom/html/HTMLAnchorElement.cpp
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLAnchorElement.h"
+
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/HTMLAnchorElementBinding.h"
+#include "mozilla/dom/HTMLDNSPrefetch.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MemoryReporting.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+#include "nsAttrValueOrString.h"
+#include "mozilla/dom/Document.h"
+#include "nsPresContext.h"
+#include "nsIURI.h"
+#include "nsWindowSizes.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Anchor)
+
+namespace mozilla::dom {
+
+HTMLAnchorElement::~HTMLAnchorElement() {
+ SupportsDNSPrefetch::Destroyed(*this);
+}
+
+bool HTMLAnchorElement::IsInteractiveHTMLContent() const {
+ return HasAttr(nsGkAtoms::href) ||
+ nsGenericHTMLElement::IsInteractiveHTMLContent();
+}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLAnchorElement,
+ nsGenericHTMLElement, Link)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLAnchorElement, nsGenericHTMLElement,
+ mRelList)
+
+NS_IMPL_ELEMENT_CLONE(HTMLAnchorElement)
+
+JSObject* HTMLAnchorElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLAnchorElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+int32_t HTMLAnchorElement::TabIndexDefault() { return 0; }
+
+bool HTMLAnchorElement::Draggable() const {
+ // links can be dragged as long as there is an href and the
+ // draggable attribute isn't false
+ if (!HasAttr(nsGkAtoms::href)) {
+ // no href, so just use the same behavior as other elements
+ return nsGenericHTMLElement::Draggable();
+ }
+
+ return !AttrValueIs(kNameSpaceID_None, nsGkAtoms::draggable,
+ nsGkAtoms::_false, eIgnoreCase);
+}
+
+nsresult HTMLAnchorElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ Link::BindToTree(aContext);
+
+ // Prefetch links
+ if (IsInComposedDoc()) {
+ TryDNSPrefetch(*this);
+ }
+
+ return rv;
+}
+
+void HTMLAnchorElement::UnbindFromTree(bool aNullParent) {
+ // Cancel any DNS prefetches
+ // Note: Must come before ResetLinkState. If called after, it will recreate
+ // mCachedURI based on data that is invalid - due to a call to Link::GetURI()
+ // via GetURIForDNSPrefetch().
+ CancelDNSPrefetch(*this);
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+
+ // Without removing the link state we risk a dangling pointer in the
+ // mStyledLinks hashtable
+ Link::UnbindFromTree();
+}
+
+bool HTMLAnchorElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ if (nsGenericHTMLElement::IsHTMLFocusable(aWithMouse, aIsFocusable,
+ aTabIndex)) {
+ return true;
+ }
+
+ // cannot focus links if there is no link handler
+ if (!OwnerDoc()->LinkHandlingEnabled()) {
+ *aTabIndex = -1;
+ *aIsFocusable = false;
+ return false;
+ }
+
+ // Links that are in an editable region should never be focusable, even if
+ // they are in a contenteditable="false" region.
+ if (nsContentUtils::IsNodeInEditableRegion(this)) {
+ *aTabIndex = -1;
+ *aIsFocusable = false;
+ return true;
+ }
+
+ if (GetTabIndexAttrValue().isNothing()) {
+ // check whether we're actually a link
+ if (!IsLink()) {
+ // Not tabbable or focusable without href (bug 17605), unless
+ // forced to be via presence of nonnegative tabindex attribute
+ *aTabIndex = -1;
+ *aIsFocusable = false;
+ return false;
+ }
+ }
+
+ if ((sTabFocusModel & eTabFocus_linksMask) == 0) {
+ *aTabIndex = -1;
+ }
+ *aIsFocusable = true;
+ return false;
+}
+
+void HTMLAnchorElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ GetEventTargetParentForAnchors(aVisitor);
+}
+
+nsresult HTMLAnchorElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
+ return PostHandleEventForAnchors(aVisitor);
+}
+
+void HTMLAnchorElement::GetLinkTarget(nsAString& aTarget) {
+ GetAttr(nsGkAtoms::target, aTarget);
+ if (aTarget.IsEmpty()) {
+ GetBaseTarget(aTarget);
+ }
+}
+
+void HTMLAnchorElement::GetTarget(nsAString& aValue) const {
+ if (!GetAttr(nsGkAtoms::target, aValue)) {
+ GetBaseTarget(aValue);
+ }
+}
+
+nsDOMTokenList* HTMLAnchorElement::RelList() {
+ if (!mRelList) {
+ mRelList =
+ new nsDOMTokenList(this, nsGkAtoms::rel, sAnchorAndFormRelValues);
+ }
+ return mRelList;
+}
+
+void HTMLAnchorElement::GetText(nsAString& aText,
+ mozilla::ErrorResult& aRv) const {
+ if (NS_WARN_IF(
+ !nsContentUtils::GetNodeTextContent(this, true, aText, fallible))) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ }
+}
+
+void HTMLAnchorElement::SetText(const nsAString& aText, ErrorResult& aRv) {
+ aRv = nsContentUtils::SetNodeTextContent(this, aText, false);
+}
+
+already_AddRefed<nsIURI> HTMLAnchorElement::GetHrefURI() const {
+ if (nsCOMPtr<nsIURI> uri = GetCachedURI()) {
+ return uri.forget();
+ }
+ return GetHrefURIForAnchors();
+}
+
+void HTMLAnchorElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None && aName == nsGkAtoms::href) {
+ CancelDNSPrefetch(*this);
+ }
+ return nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue,
+ aNotify);
+}
+
+void HTMLAnchorElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::href) {
+ Link::ResetLinkState(aNotify, !!aValue);
+ if (aValue && IsInComposedDoc()) {
+ TryDNSPrefetch(*this);
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLAnchorElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes,
+ size_t* aNodeSize) const {
+ nsGenericHTMLElement::AddSizeOfExcludingThis(aSizes, aNodeSize);
+ *aNodeSize += Link::SizeOfExcludingThis(aSizes.mState);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLAnchorElement.h b/dom/html/HTMLAnchorElement.h
new file mode 100644
index 0000000000..2a524b96c2
--- /dev/null
+++ b/dom/html/HTMLAnchorElement.h
@@ -0,0 +1,201 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLAnchorElement_h
+#define mozilla_dom_HTMLAnchorElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/Link.h"
+#include "mozilla/dom/HTMLDNSPrefetch.h"
+#include "nsGenericHTMLElement.h"
+#include "nsDOMTokenList.h"
+
+namespace mozilla {
+class EventChainPostVisitor;
+class EventChainPreVisitor;
+namespace dom {
+
+class HTMLAnchorElement final : public nsGenericHTMLElement,
+ public Link,
+ public SupportsDNSPrefetch {
+ public:
+ using Element::GetText;
+
+ explicit HTMLAnchorElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)), Link(this) {}
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // CC
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLAnchorElement,
+ nsGenericHTMLElement)
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLAnchorElement, a);
+
+ int32_t TabIndexDefault() override;
+ bool Draggable() const override;
+
+ // Element
+ bool IsInteractiveHTMLContent() const override;
+
+ // DOM memory reporter participant
+ NS_DECL_ADDSIZEOFEXCLUDINGTHIS
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+ MOZ_CAN_RUN_SCRIPT
+ nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+
+ void GetLinkTarget(nsAString& aTarget) override;
+ already_AddRefed<nsIURI> GetHrefURI() const override;
+
+ void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+ void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // WebIDL API
+
+ void GetHref(nsAString& aValue) const {
+ GetURIAttr(nsGkAtoms::href, nullptr, aValue);
+ }
+ void SetHref(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::href, aValue, rv);
+ }
+ void GetTarget(nsAString& aValue) const;
+ void SetTarget(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::target, aValue, rv);
+ }
+ void GetDownload(DOMString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::download, aValue);
+ }
+ void SetDownload(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::download, aValue, rv);
+ }
+ void GetPing(DOMString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::ping, aValue);
+ }
+ void SetPing(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::ping, aValue, rv);
+ }
+ void GetRel(DOMString& aValue) const { GetHTMLAttr(nsGkAtoms::rel, aValue); }
+ void SetRel(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::rel, aValue, rv);
+ }
+ void SetReferrerPolicy(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::referrerpolicy, aValue, rv);
+ }
+ void GetReferrerPolicy(DOMString& aPolicy) const {
+ GetEnumAttr(nsGkAtoms::referrerpolicy, "", aPolicy);
+ }
+ nsDOMTokenList* RelList();
+ void GetHreflang(DOMString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::hreflang, aValue);
+ }
+ void SetHreflang(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::hreflang, aValue, rv);
+ }
+ // Needed for docshell
+ void GetType(nsAString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::type, aValue);
+ }
+ void GetType(DOMString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::type, aValue);
+ }
+ void SetType(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::type, aValue, rv);
+ }
+ void GetText(nsAString& aText, mozilla::ErrorResult& aRv) const;
+ void SetText(const nsAString& aText, mozilla::ErrorResult& aRv);
+
+ // Link::GetOrigin is OK for us
+
+ // Link::GetProtocol is OK for us
+ // Link::SetProtocol is OK for us
+
+ // Link::GetUsername is OK for us
+ // Link::SetUsername is OK for us
+
+ // Link::GetPassword is OK for us
+ // Link::SetPassword is OK for us
+
+ // Link::Link::GetHost is OK for us
+ // Link::Link::SetHost is OK for us
+
+ // Link::Link::GetHostname is OK for us
+ // Link::Link::SetHostname is OK for us
+
+ // Link::Link::GetPort is OK for us
+ // Link::Link::SetPort is OK for us
+
+ // Link::Link::GetPathname is OK for us
+ // Link::Link::SetPathname is OK for us
+
+ // Link::Link::GetSearch is OK for us
+ // Link::Link::SetSearch is OK for us
+
+ // Link::Link::GetHash is OK for us
+ // Link::Link::SetHash is OK for us
+
+ void GetCoords(DOMString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::coords, aValue);
+ }
+ void SetCoords(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::coords, aValue, rv);
+ }
+ void GetCharset(DOMString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::charset, aValue);
+ }
+ void SetCharset(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::charset, aValue, rv);
+ }
+ void GetName(DOMString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::name, aValue);
+ }
+ void GetName(nsAString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::name, aValue);
+ }
+ void SetName(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::name, aValue, rv);
+ }
+ void GetRev(DOMString& aValue) const { GetHTMLAttr(nsGkAtoms::rev, aValue); }
+ void SetRev(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::rev, aValue, rv);
+ }
+ void GetShape(DOMString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::shape, aValue);
+ }
+ void SetShape(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::shape, aValue, rv);
+ }
+ void Stringify(nsAString& aResult) const { GetHref(aResult); }
+ void ToString(nsAString& aSource) const { GetHref(aSource); }
+
+ void NodeInfoChanged(Document* aOldDoc) final {
+ ClearHasPendingLinkUpdate();
+ nsGenericHTMLElement::NodeInfoChanged(aOldDoc);
+ }
+
+ protected:
+ virtual ~HTMLAnchorElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+ RefPtr<nsDOMTokenList> mRelList;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_HTMLAnchorElement_h
diff --git a/dom/html/HTMLAreaElement.cpp b/dom/html/HTMLAreaElement.cpp
new file mode 100644
index 0000000000..81389a4d14
--- /dev/null
+++ b/dom/html/HTMLAreaElement.cpp
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLAreaElement.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLAnchorElement.h"
+#include "mozilla/dom/HTMLAreaElementBinding.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MemoryReporting.h"
+#include "nsWindowSizes.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Area)
+
+namespace mozilla::dom {
+
+HTMLAreaElement::HTMLAreaElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)), Link(this) {}
+
+HTMLAreaElement::~HTMLAreaElement() = default;
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLAreaElement,
+ nsGenericHTMLElement, Link)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLAreaElement, nsGenericHTMLElement,
+ mRelList)
+
+NS_IMPL_ELEMENT_CLONE(HTMLAreaElement)
+
+int32_t HTMLAreaElement::TabIndexDefault() { return 0; }
+
+void HTMLAreaElement::GetTarget(DOMString& aValue) {
+ if (!GetAttr(nsGkAtoms::target, aValue)) {
+ GetBaseTarget(aValue);
+ }
+}
+
+void HTMLAreaElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ GetEventTargetParentForAnchors(aVisitor);
+}
+
+nsresult HTMLAreaElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
+ return PostHandleEventForAnchors(aVisitor);
+}
+
+void HTMLAreaElement::GetLinkTarget(nsAString& aTarget) {
+ GetAttr(nsGkAtoms::target, aTarget);
+ if (aTarget.IsEmpty()) {
+ GetBaseTarget(aTarget);
+ }
+}
+
+nsDOMTokenList* HTMLAreaElement::RelList() {
+ if (!mRelList) {
+ mRelList =
+ new nsDOMTokenList(this, nsGkAtoms::rel, sAnchorAndFormRelValues);
+ }
+ return mRelList;
+}
+
+nsresult HTMLAreaElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ Link::BindToTree(aContext);
+ return rv;
+}
+
+void HTMLAreaElement::UnbindFromTree(bool aNullParent) {
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+ // Without removing the link state we risk a dangling pointer in the
+ // mStyledLinks hashtable
+ Link::UnbindFromTree();
+}
+
+void HTMLAreaElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None && aName == nsGkAtoms::href) {
+ Link::ResetLinkState(aNotify, !!aValue);
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLAreaElement::ToString(nsAString& aSource) { GetHref(aSource); }
+
+already_AddRefed<nsIURI> HTMLAreaElement::GetHrefURI() const {
+ if (nsCOMPtr<nsIURI> uri = GetCachedURI()) {
+ return uri.forget();
+ }
+ return GetHrefURIForAnchors();
+}
+
+void HTMLAreaElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes,
+ size_t* aNodeSize) const {
+ nsGenericHTMLElement::AddSizeOfExcludingThis(aSizes, aNodeSize);
+ *aNodeSize += Link::SizeOfExcludingThis(aSizes.mState);
+}
+
+JSObject* HTMLAreaElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLAreaElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLAreaElement.h b/dom/html/HTMLAreaElement.h
new file mode 100644
index 0000000000..3cba9dd833
--- /dev/null
+++ b/dom/html/HTMLAreaElement.h
@@ -0,0 +1,170 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLAreaElement_h
+#define mozilla_dom_HTMLAreaElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/Link.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+
+namespace mozilla {
+class EventChainPostVisitor;
+class EventChainPreVisitor;
+namespace dom {
+
+class HTMLAreaElement final : public nsGenericHTMLElement, public Link {
+ public:
+ explicit HTMLAreaElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // CC
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLAreaElement,
+ nsGenericHTMLElement)
+
+ NS_DECL_ADDSIZEOFEXCLUDINGTHIS
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLAreaElement, area)
+
+ virtual int32_t TabIndexDefault() override;
+
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+ MOZ_CAN_RUN_SCRIPT
+ nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+
+ void GetLinkTarget(nsAString& aTarget) override;
+ already_AddRefed<nsIURI> GetHrefURI() const override;
+
+ virtual nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ virtual void UnbindFromTree(bool aNullParent = true) override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // WebIDL
+ void GetAlt(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::alt, aValue); }
+ void SetAlt(const nsAString& aAlt, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::alt, aAlt, aError);
+ }
+
+ void GetCoords(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::coords, aValue); }
+ void SetCoords(const nsAString& aCoords, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::coords, aCoords, aError);
+ }
+
+ // argument type nsAString for HTMLImageMapAccessible
+ void GetShape(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::shape, aValue); }
+ void SetShape(const nsAString& aShape, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::shape, aShape, aError);
+ }
+
+ // argument type nsAString for nsContextMenuInfo
+ void GetHref(nsAString& aValue) {
+ GetURIAttr(nsGkAtoms::href, nullptr, aValue);
+ }
+ void SetHref(const nsAString& aHref, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::href, aHref, aError);
+ }
+
+ void GetTarget(DOMString& aValue);
+ void SetTarget(const nsAString& aTarget, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::target, aTarget, aError);
+ }
+
+ void GetDownload(DOMString& aValue) {
+ GetHTMLAttr(nsGkAtoms::download, aValue);
+ }
+ void SetDownload(const nsAString& aDownload, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::download, aDownload, aError);
+ }
+
+ void GetPing(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::ping, aValue); }
+
+ void SetPing(const nsAString& aPing, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::ping, aPing, aError);
+ }
+
+ void GetRel(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::rel, aValue); }
+
+ void SetRel(const nsAString& aRel, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::rel, aRel, aError);
+ }
+ nsDOMTokenList* RelList();
+
+ void SetReferrerPolicy(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::referrerpolicy, aValue, rv);
+ }
+ void GetReferrerPolicy(nsAString& aReferrer) {
+ GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrer);
+ }
+
+ // The Link::GetOrigin is OK for us
+
+ // Link::Link::GetProtocol is OK for us
+ // Link::Link::SetProtocol is OK for us
+
+ // The Link::GetUsername is OK for us
+ // The Link::SetUsername is OK for us
+
+ // The Link::GetPassword is OK for us
+ // The Link::SetPassword is OK for us
+
+ // Link::Link::GetHost is OK for us
+ // Link::Link::SetHost is OK for us
+
+ // Link::Link::GetHostname is OK for us
+ // Link::Link::SetHostname is OK for us
+
+ // Link::Link::GetPort is OK for us
+ // Link::Link::SetPort is OK for us
+
+ // Link::Link::GetPathname is OK for us
+ // Link::Link::SetPathname is OK for us
+
+ // Link::Link::GetSearch is OK for us
+ // Link::Link::SetSearch is OK for us
+
+ // Link::Link::GetHash is OK for us
+ // Link::Link::SetHash is OK for us
+
+ // The Link::GetSearchParams is OK for us
+
+ bool NoHref() const { return GetBoolAttr(nsGkAtoms::nohref); }
+
+ void SetNoHref(bool aValue, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::nohref, aValue, aError);
+ }
+
+ void ToString(nsAString& aSource);
+ void Stringify(nsAString& aResult) { GetHref(aResult); }
+
+ void NodeInfoChanged(Document* aOldDoc) final {
+ ClearHasPendingLinkUpdate();
+ nsGenericHTMLElement::NodeInfoChanged(aOldDoc);
+ }
+
+ protected:
+ virtual ~HTMLAreaElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) override;
+
+ RefPtr<nsDOMTokenList> mRelList;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_HTMLAreaElement_h */
diff --git a/dom/html/HTMLAudioElement.cpp b/dom/html/HTMLAudioElement.cpp
new file mode 100644
index 0000000000..766df668b8
--- /dev/null
+++ b/dom/html/HTMLAudioElement.cpp
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLAudioElement.h"
+#include "mozilla/dom/HTMLAudioElementBinding.h"
+#include "nsError.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "mozilla/dom/Document.h"
+#include "jsfriendapi.h"
+#include "nsContentUtils.h"
+#include "nsJSUtils.h"
+#include "AudioSampleFormat.h"
+#include <algorithm>
+#include "nsComponentManagerUtils.h"
+#include "nsIHttpChannel.h"
+#include "mozilla/dom/TimeRanges.h"
+#include "AudioStream.h"
+
+nsGenericHTMLElement* NS_NewHTMLAudioElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
+ auto* nim = nodeInfo->NodeInfoManager();
+ mozilla::dom::HTMLAudioElement* element =
+ new (nim) mozilla::dom::HTMLAudioElement(nodeInfo.forget());
+ element->Init();
+ return element;
+}
+
+namespace mozilla::dom {
+
+nsresult HTMLAudioElement::Clone(mozilla::dom::NodeInfo* aNodeInfo,
+ nsINode** aResult) const {
+ *aResult = nullptr;
+ RefPtr<mozilla::dom::NodeInfo> ni(aNodeInfo);
+ auto* nim = ni->NodeInfoManager();
+ HTMLAudioElement* it = new (nim) HTMLAudioElement(ni.forget());
+ it->Init();
+ nsCOMPtr<nsINode> kungFuDeathGrip = it;
+ nsresult rv = const_cast<HTMLAudioElement*>(this)->CopyInnerTo(it);
+ if (NS_SUCCEEDED(rv)) {
+ kungFuDeathGrip.swap(*aResult);
+ }
+ return rv;
+}
+
+HTMLAudioElement::HTMLAudioElement(already_AddRefed<NodeInfo>&& aNodeInfo)
+ : HTMLMediaElement(std::move(aNodeInfo)) {
+ DecoderDoctorLogger::LogConstruction(this);
+}
+
+HTMLAudioElement::~HTMLAudioElement() {
+ DecoderDoctorLogger::LogDestruction(this);
+}
+
+bool HTMLAudioElement::IsInteractiveHTMLContent() const {
+ return HasAttr(nsGkAtoms::controls) ||
+ HTMLMediaElement::IsInteractiveHTMLContent();
+}
+
+already_AddRefed<HTMLAudioElement> HTMLAudioElement::Audio(
+ const GlobalObject& aGlobal, const Optional<nsAString>& aSrc,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
+ Document* doc;
+ if (!win || !(doc = win->GetExtantDoc())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo = doc->NodeInfoManager()->GetNodeInfo(
+ nsGkAtoms::audio, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE);
+
+ RefPtr<HTMLAudioElement> audio =
+ static_cast<HTMLAudioElement*>(NS_NewHTMLAudioElement(nodeInfo.forget()));
+ audio->SetHTMLAttr(nsGkAtoms::preload, u"auto"_ns, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (aSrc.WasPassed()) {
+ audio->SetSrc(aSrc.Value(), aRv);
+ }
+
+ return audio.forget();
+}
+
+nsresult HTMLAudioElement::SetAcceptHeader(nsIHttpChannel* aChannel) {
+ nsAutoCString value(
+ "audio/webm,"
+ "audio/ogg,"
+ "audio/wav,"
+ "audio/*;q=0.9,"
+ "application/ogg;q=0.7,"
+ "video/*;q=0.6,*/*;q=0.5");
+
+ return aChannel->SetRequestHeader("Accept"_ns, value, false);
+}
+
+JSObject* HTMLAudioElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLAudioElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLAudioElement.h b/dom/html/HTMLAudioElement.h
new file mode 100644
index 0000000000..75f7105c6f
--- /dev/null
+++ b/dom/html/HTMLAudioElement.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLAudioElement_h
+#define mozilla_dom_HTMLAudioElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/TypedArray.h"
+
+typedef uint16_t nsMediaNetworkState;
+typedef uint16_t nsMediaReadyState;
+
+namespace mozilla::dom {
+
+class HTMLAudioElement final : public HTMLMediaElement {
+ public:
+ typedef mozilla::dom::NodeInfo NodeInfo;
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLAudioElement, audio)
+
+ explicit HTMLAudioElement(already_AddRefed<NodeInfo>&& aNodeInfo);
+
+ // Element
+ virtual bool IsInteractiveHTMLContent() const override;
+
+ // nsIDOMHTMLMediaElement
+ using HTMLMediaElement::GetPaused;
+
+ virtual nsresult Clone(NodeInfo*, nsINode** aResult) const override;
+ virtual nsresult SetAcceptHeader(nsIHttpChannel* aChannel) override;
+
+ // WebIDL
+
+ static already_AddRefed<HTMLAudioElement> Audio(
+ const GlobalObject& aGlobal, const Optional<nsAString>& aSrc,
+ ErrorResult& aRv);
+
+ protected:
+ virtual ~HTMLAudioElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLAudioElement_h
diff --git a/dom/html/HTMLBRElement.cpp b/dom/html/HTMLBRElement.cpp
new file mode 100644
index 0000000000..5145c9ea3b
--- /dev/null
+++ b/dom/html/HTMLBRElement.cpp
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/HTMLBRElementBinding.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsAttrValueInlines.h"
+#include "nsStyleConsts.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(BR)
+
+namespace mozilla::dom {
+
+HTMLBRElement::HTMLBRElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLBRElement::~HTMLBRElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLBRElement)
+
+static const nsAttrValue::EnumTable kClearTable[] = {
+ {"left", StyleClear::Left},
+ {"right", StyleClear::Right},
+ {"all", StyleClear::Both},
+ {"both", StyleClear::Both},
+ {nullptr, 0}};
+
+bool HTMLBRElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aAttribute == nsGkAtoms::clear && aNamespaceID == kNameSpaceID_None) {
+ return aResult.ParseEnumValue(aValue, kClearTable, false);
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLBRElement::MapAttributesIntoRule(MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty_clear)) {
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::clear);
+ if (value && value->Type() == nsAttrValue::eEnum) {
+ aBuilder.SetKeywordValue(eCSSProperty_clear, value->GetEnumValue());
+ }
+ }
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLBRElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {{nsGkAtoms::clear},
+ {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLBRElement::GetAttributeMappingFunction() const {
+ return &MapAttributesIntoRule;
+}
+
+JSObject* HTMLBRElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLBRElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLBRElement.h b/dom/html/HTMLBRElement.h
new file mode 100644
index 0000000000..e98210dcd2
--- /dev/null
+++ b/dom/html/HTMLBRElement.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLBRElement_h
+#define mozilla_dom_HTMLBRElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+
+namespace mozilla::dom {
+
+#define BR_ELEMENT_FLAG_BIT(n_) \
+ NODE_FLAG_BIT(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + (n_))
+
+// BR element specific bits
+enum {
+ // NS_PADDING_FOR_EMPTY_EDITOR is set if the <br> element is created by
+ // editor for placing caret at proper position in empty editor.
+ NS_PADDING_FOR_EMPTY_EDITOR = BR_ELEMENT_FLAG_BIT(0),
+
+ // NS_PADDING_FOR_EMPTY_LAST_LINE is set if the <br> element is created by
+ // editor for placing caret at proper position for making empty last line
+ // in a block or <textarea> element visible.
+ NS_PADDING_FOR_EMPTY_LAST_LINE = BR_ELEMENT_FLAG_BIT(1),
+};
+
+ASSERT_NODE_FLAGS_SPACE(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 2);
+
+class HTMLBRElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLBRElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLBRElement, br)
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ bool Clear() const { return GetBoolAttr(nsGkAtoms::clear); }
+ void SetClear(const nsAString& aClear, ErrorResult& aError) {
+ return SetHTMLAttr(nsGkAtoms::clear, aClear, aError);
+ }
+ void GetClear(DOMString& aClear) const {
+ return GetHTMLAttr(nsGkAtoms::clear, aClear);
+ }
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ bool IsPaddingForEmptyEditor() const {
+ return HasFlag(NS_PADDING_FOR_EMPTY_EDITOR);
+ }
+ bool IsPaddingForEmptyLastLine() const {
+ return HasFlag(NS_PADDING_FOR_EMPTY_LAST_LINE);
+ }
+
+ private:
+ virtual ~HTMLBRElement();
+
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/html/HTMLBodyElement.cpp b/dom/html/HTMLBodyElement.cpp
new file mode 100644
index 0000000000..9fcf3e0eb6
--- /dev/null
+++ b/dom/html/HTMLBodyElement.cpp
@@ -0,0 +1,329 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLBodyElement.h"
+#include "mozilla/dom/HTMLBodyElementBinding.h"
+
+#include "mozilla/AttributeStyles.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/Document.h"
+#include "nsAttrValueInlines.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsPresContext.h"
+#include "DocumentInlines.h"
+#include "nsDocShell.h"
+#include "nsIDocShell.h"
+#include "nsGlobalWindowInner.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Body)
+
+namespace mozilla::dom {
+
+//----------------------------------------------------------------------
+
+HTMLBodyElement::~HTMLBodyElement() = default;
+
+JSObject* HTMLBodyElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLBodyElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLBodyElement)
+
+bool HTMLBodyElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::bgcolor || aAttribute == nsGkAtoms::text ||
+ aAttribute == nsGkAtoms::link || aAttribute == nsGkAtoms::alink ||
+ aAttribute == nsGkAtoms::vlink) {
+ return aResult.ParseColor(aValue);
+ }
+ if (aAttribute == nsGkAtoms::marginwidth ||
+ aAttribute == nsGkAtoms::marginheight ||
+ aAttribute == nsGkAtoms::topmargin ||
+ aAttribute == nsGkAtoms::bottommargin ||
+ aAttribute == nsGkAtoms::leftmargin ||
+ aAttribute == nsGkAtoms::rightmargin) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseBackgroundAttribute(
+ aNamespaceID, aAttribute, aValue, aResult) ||
+ nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLBodyElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ // This is the one place where we try to set the same property
+ // multiple times in presentation attributes. Servo does not support
+ // querying if a property is set (because that is O(n) behavior
+ // in ServoSpecifiedValues). Instead, we use the below values to keep
+ // track of whether we have already set a property, and if so, what value
+ // we set it to (which is used when handling margin
+ // attributes from the containing frame element)
+
+ int32_t bodyMarginWidth = -1;
+ int32_t bodyMarginHeight = -1;
+ int32_t bodyTopMargin = -1;
+ int32_t bodyBottomMargin = -1;
+ int32_t bodyLeftMargin = -1;
+ int32_t bodyRightMargin = -1;
+
+ const nsAttrValue* value;
+ // if marginwidth/marginheight are set, reflect them as 'margin'
+ value = aBuilder.GetAttr(nsGkAtoms::marginwidth);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ bodyMarginWidth = value->GetIntegerValue();
+ if (bodyMarginWidth < 0) {
+ bodyMarginWidth = 0;
+ }
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_left,
+ (float)bodyMarginWidth);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_right,
+ (float)bodyMarginWidth);
+ }
+
+ value = aBuilder.GetAttr(nsGkAtoms::marginheight);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ bodyMarginHeight = value->GetIntegerValue();
+ if (bodyMarginHeight < 0) {
+ bodyMarginHeight = 0;
+ }
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_top,
+ (float)bodyMarginHeight);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_bottom,
+ (float)bodyMarginHeight);
+ }
+
+ // topmargin (IE-attribute)
+ if (bodyMarginHeight == -1) {
+ value = aBuilder.GetAttr(nsGkAtoms::topmargin);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ bodyTopMargin = value->GetIntegerValue();
+ if (bodyTopMargin < 0) {
+ bodyTopMargin = 0;
+ }
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_top,
+ (float)bodyTopMargin);
+ }
+ }
+ // bottommargin (IE-attribute)
+
+ if (bodyMarginHeight == -1) {
+ value = aBuilder.GetAttr(nsGkAtoms::bottommargin);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ bodyBottomMargin = value->GetIntegerValue();
+ if (bodyBottomMargin < 0) {
+ bodyBottomMargin = 0;
+ }
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_bottom,
+ (float)bodyBottomMargin);
+ }
+ }
+
+ // leftmargin (IE-attribute)
+ if (bodyMarginWidth == -1) {
+ value = aBuilder.GetAttr(nsGkAtoms::leftmargin);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ bodyLeftMargin = value->GetIntegerValue();
+ if (bodyLeftMargin < 0) {
+ bodyLeftMargin = 0;
+ }
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_left,
+ (float)bodyLeftMargin);
+ }
+ }
+ // rightmargin (IE-attribute)
+ if (bodyMarginWidth == -1) {
+ value = aBuilder.GetAttr(nsGkAtoms::rightmargin);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ bodyRightMargin = value->GetIntegerValue();
+ if (bodyRightMargin < 0) {
+ bodyRightMargin = 0;
+ }
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_right,
+ (float)bodyRightMargin);
+ }
+ }
+
+ // if marginwidth or marginheight is set in the <frame> and not set in the
+ // <body> reflect them as margin in the <body>
+ if (bodyMarginWidth == -1 || bodyMarginHeight == -1) {
+ if (nsDocShell* ds = nsDocShell::Cast(aBuilder.Document().GetDocShell())) {
+ CSSIntSize margins = ds->GetFrameMargins();
+ int32_t frameMarginWidth = margins.width;
+ int32_t frameMarginHeight = margins.height;
+
+ if (bodyMarginWidth == -1 && frameMarginWidth >= 0) {
+ if (bodyLeftMargin == -1) {
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_left,
+ (float)frameMarginWidth);
+ }
+ if (bodyRightMargin == -1) {
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_right,
+ (float)frameMarginWidth);
+ }
+ }
+
+ if (bodyMarginHeight == -1 && frameMarginHeight >= 0) {
+ if (bodyTopMargin == -1) {
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_top,
+ (float)frameMarginHeight);
+ }
+ if (bodyBottomMargin == -1) {
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_bottom,
+ (float)frameMarginHeight);
+ }
+ }
+ }
+ }
+
+ // When display if first asked for, go ahead and get our colors set up.
+ if (AttributeStyles* attrStyles = aBuilder.Document().GetAttributeStyles()) {
+ nscolor color;
+ value = aBuilder.GetAttr(nsGkAtoms::link);
+ if (value && value->GetColorValue(color)) {
+ attrStyles->SetLinkColor(color);
+ }
+
+ value = aBuilder.GetAttr(nsGkAtoms::alink);
+ if (value && value->GetColorValue(color)) {
+ attrStyles->SetActiveLinkColor(color);
+ }
+
+ value = aBuilder.GetAttr(nsGkAtoms::vlink);
+ if (value && value->GetColorValue(color)) {
+ attrStyles->SetVisitedLinkColor(color);
+ }
+ }
+
+ if (!aBuilder.PropertyIsSet(eCSSProperty_color)) {
+ // color: color
+ nscolor color;
+ value = aBuilder.GetAttr(nsGkAtoms::text);
+ if (value && value->GetColorValue(color)) {
+ aBuilder.SetColorValue(eCSSProperty_color, color);
+ }
+ }
+
+ nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+nsMapRuleToAttributesFunc HTMLBodyElement::GetAttributeMappingFunction() const {
+ return &MapAttributesIntoRule;
+}
+
+NS_IMETHODIMP_(bool)
+HTMLBodyElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::link},
+ {nsGkAtoms::vlink},
+ {nsGkAtoms::alink},
+ {nsGkAtoms::text},
+ {nsGkAtoms::marginwidth},
+ {nsGkAtoms::marginheight},
+ {nsGkAtoms::topmargin},
+ {nsGkAtoms::rightmargin},
+ {nsGkAtoms::bottommargin},
+ {nsGkAtoms::leftmargin},
+ {nullptr},
+ };
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ sBackgroundAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+already_AddRefed<EditorBase> HTMLBodyElement::GetAssociatedEditor() {
+ MOZ_ASSERT(!GetTextEditorInternal());
+
+ // Make sure this is the actual body of the document
+ if (this != OwnerDoc()->GetBodyElement()) {
+ return nullptr;
+ }
+
+ // For designmode, try to get document's editor
+ nsPresContext* presContext = GetPresContext(eForComposedDoc);
+ if (!presContext) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = presContext->GetDocShell();
+ if (!docShell) {
+ return nullptr;
+ }
+
+ RefPtr<HTMLEditor> htmlEditor = docShell->GetHTMLEditor();
+ return htmlEditor.forget();
+}
+
+bool HTMLBodyElement::IsEventAttributeNameInternal(nsAtom* aName) {
+ return nsContentUtils::IsEventAttributeName(
+ aName, EventNameType_HTML | EventNameType_HTMLBodyOrFramesetOnly);
+}
+
+nsresult HTMLBodyElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ mAttrs.MarkAsPendingPresAttributeEvaluation();
+ return nsGenericHTMLElement::BindToTree(aContext, aParent);
+}
+
+void HTMLBodyElement::FrameMarginsChanged() {
+ MOZ_ASSERT(IsInComposedDoc());
+ if (IsPendingMappedAttributeEvaluation()) {
+ return;
+ }
+ if (mAttrs.MarkAsPendingPresAttributeEvaluation()) {
+ OwnerDoc()->ScheduleForPresAttrEvaluation(this);
+ }
+}
+
+#define EVENT(name_, id_, type_, \
+ struct_) /* nothing; handled by the superclass */
+// nsGenericHTMLElement::GetOnError returns
+// already_AddRefed<EventHandlerNonNull> while other getters return
+// EventHandlerNonNull*, so allow passing in the type to use here.
+#define WINDOW_EVENT_HELPER(name_, type_) \
+ type_* HTMLBodyElement::GetOn##name_() { \
+ if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \
+ nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \
+ return globalWin->GetOn##name_(); \
+ } \
+ return nullptr; \
+ } \
+ void HTMLBodyElement::SetOn##name_(type_* handler) { \
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \
+ if (!win) { \
+ return; \
+ } \
+ \
+ nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \
+ return globalWin->SetOn##name_(handler); \
+ }
+#define WINDOW_EVENT(name_, id_, type_, struct_) \
+ WINDOW_EVENT_HELPER(name_, EventHandlerNonNull)
+#define BEFOREUNLOAD_EVENT(name_, id_, type_, struct_) \
+ WINDOW_EVENT_HELPER(name_, OnBeforeUnloadEventHandlerNonNull)
+#include "mozilla/EventNameList.h" // IWYU pragma: keep
+#undef BEFOREUNLOAD_EVENT
+#undef WINDOW_EVENT
+#undef WINDOW_EVENT_HELPER
+#undef EVENT
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLBodyElement.h b/dom/html/HTMLBodyElement.h
new file mode 100644
index 0000000000..3c836f2ed0
--- /dev/null
+++ b/dom/html/HTMLBodyElement.h
@@ -0,0 +1,117 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef HTMLBodyElement_h___
+#define HTMLBodyElement_h___
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla {
+
+class EditorBase;
+
+namespace dom {
+
+class OnBeforeUnloadEventHandlerNonNull;
+
+class HTMLBodyElement final : public nsGenericHTMLElement {
+ public:
+ using Element::GetText;
+
+ explicit HTMLBodyElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLBodyElement, nsGenericHTMLElement)
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLBodyElement, body);
+
+ // Event listener stuff; we need to declare only the ones we need to
+ // forward to window that don't come from nsIDOMHTMLBodyElement.
+#define EVENT(name_, id_, type_, struct_) /* nothing; handled by the shim */
+#define WINDOW_EVENT_HELPER(name_, type_) \
+ type_* GetOn##name_(); \
+ void SetOn##name_(type_* handler);
+#define WINDOW_EVENT(name_, id_, type_, struct_) \
+ WINDOW_EVENT_HELPER(name_, EventHandlerNonNull)
+#define BEFOREUNLOAD_EVENT(name_, id_, type_, struct_) \
+ WINDOW_EVENT_HELPER(name_, OnBeforeUnloadEventHandlerNonNull)
+#include "mozilla/EventNameList.h" // IWYU pragma: keep
+#undef BEFOREUNLOAD_EVENT
+#undef WINDOW_EVENT
+#undef WINDOW_EVENT_HELPER
+#undef EVENT
+
+ void GetText(nsAString& aText) { GetHTMLAttr(nsGkAtoms::text, aText); }
+ void SetText(const nsAString& aText) { SetHTMLAttr(nsGkAtoms::text, aText); }
+ void SetText(const nsAString& aText, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::text, aText, aError);
+ }
+ void GetLink(nsAString& aLink) { GetHTMLAttr(nsGkAtoms::link, aLink); }
+ void SetLink(const nsAString& aLink) { SetHTMLAttr(nsGkAtoms::link, aLink); }
+ void SetLink(const nsAString& aLink, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::link, aLink, aError);
+ }
+ void GetVLink(nsAString& aVLink) { GetHTMLAttr(nsGkAtoms::vlink, aVLink); }
+ void SetVLink(const nsAString& aVLink) {
+ SetHTMLAttr(nsGkAtoms::vlink, aVLink);
+ }
+ void SetVLink(const nsAString& aVLink, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::vlink, aVLink, aError);
+ }
+ void GetALink(nsAString& aALink) { GetHTMLAttr(nsGkAtoms::alink, aALink); }
+ void SetALink(const nsAString& aALink) {
+ SetHTMLAttr(nsGkAtoms::alink, aALink);
+ }
+ void SetALink(const nsAString& aALink, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::alink, aALink, aError);
+ }
+ void GetBgColor(nsAString& aBgColor) {
+ GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor);
+ }
+ void SetBgColor(const nsAString& aBgColor) {
+ SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor);
+ }
+ void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError);
+ }
+ void GetBackground(DOMString& aBackground) {
+ GetHTMLAttr(nsGkAtoms::background, aBackground);
+ }
+ void GetBackground(nsAString& aBackground) {
+ GetHTMLAttr(nsGkAtoms::background, aBackground);
+ }
+ void SetBackground(const nsAString& aBackground, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::background, aBackground, aError);
+ }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ already_AddRefed<EditorBase> GetAssociatedEditor() override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ bool IsEventAttributeNameInternal(nsAtom* aName) override;
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+
+ void FrameMarginsChanged();
+
+ protected:
+ virtual ~HTMLBodyElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* HTMLBodyElement_h___ */
diff --git a/dom/html/HTMLButtonElement.cpp b/dom/html/HTMLButtonElement.cpp
new file mode 100644
index 0000000000..7aa7428a26
--- /dev/null
+++ b/dom/html/HTMLButtonElement.cpp
@@ -0,0 +1,438 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLButtonElement.h"
+
+#include "HTMLFormSubmissionConstants.h"
+#include "mozilla/dom/FormData.h"
+#include "mozilla/dom/HTMLButtonElementBinding.h"
+#include "nsAttrValueInlines.h"
+#include "nsIContentInlines.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsPresContext.h"
+#include "nsIFormControl.h"
+#include "nsIFrame.h"
+#include "nsIFormControlFrame.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/ContentEvents.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/TextEvents.h"
+#include "nsUnicharUtils.h"
+#include "nsLayoutUtils.h"
+#include "mozilla/PresState.h"
+#include "nsError.h"
+#include "nsFocusManager.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "mozAutoDocUpdate.h"
+
+#define NS_IN_SUBMIT_CLICK (1 << 0)
+#define NS_OUTER_ACTIVATE_EVENT (1 << 1)
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Button)
+
+namespace mozilla::dom {
+
+static const nsAttrValue::EnumTable kButtonTypeTable[] = {
+ {"button", FormControlType::ButtonButton},
+ {"reset", FormControlType::ButtonReset},
+ {"submit", FormControlType::ButtonSubmit},
+ {nullptr, 0}};
+
+// Default type is 'submit'.
+static const nsAttrValue::EnumTable* kButtonDefaultType = &kButtonTypeTable[2];
+
+// Construction, destruction
+HTMLButtonElement::HTMLButtonElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : nsGenericHTMLFormControlElementWithState(
+ std::move(aNodeInfo), aFromParser,
+ FormControlType(kButtonDefaultType->value)),
+ mDisabledChanged(false),
+ mInInternalActivate(false),
+ mInhibitStateRestoration(aFromParser & FROM_PARSER_FRAGMENT) {
+ // Set up our default state: enabled
+ AddStatesSilently(ElementState::ENABLED);
+}
+
+HTMLButtonElement::~HTMLButtonElement() = default;
+
+// nsISupports
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLButtonElement,
+ nsGenericHTMLFormControlElementWithState,
+ mValidity)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(
+ HTMLButtonElement, nsGenericHTMLFormControlElementWithState,
+ nsIConstraintValidation)
+
+void HTMLButtonElement::SetCustomValidity(const nsAString& aError) {
+ ConstraintValidation::SetCustomValidity(aError);
+ UpdateValidityElementStates(true);
+}
+
+void HTMLButtonElement::UpdateBarredFromConstraintValidation() {
+ SetBarredFromConstraintValidation(
+ mType == FormControlType::ButtonButton ||
+ mType == FormControlType::ButtonReset ||
+ HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR) || IsDisabled());
+}
+
+void HTMLButtonElement::FieldSetDisabledChanged(bool aNotify) {
+ // FieldSetDisabledChanged *has* to be called *before*
+ // UpdateBarredFromConstraintValidation, because the latter depends on our
+ // disabled state.
+ nsGenericHTMLFormControlElementWithState::FieldSetDisabledChanged(aNotify);
+
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(aNotify);
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLButtonElement)
+
+void HTMLButtonElement::GetFormEnctype(nsAString& aFormEncType) {
+ GetEnumAttr(nsGkAtoms::formenctype, "", kFormDefaultEnctype->tag,
+ aFormEncType);
+}
+
+void HTMLButtonElement::GetFormMethod(nsAString& aFormMethod) {
+ GetEnumAttr(nsGkAtoms::formmethod, "", kFormDefaultMethod->tag, aFormMethod);
+}
+
+void HTMLButtonElement::GetType(nsAString& aType) {
+ GetEnumAttr(nsGkAtoms::type, kButtonDefaultType->tag, aType);
+}
+
+int32_t HTMLButtonElement::TabIndexDefault() { return 0; }
+
+bool HTMLButtonElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable(
+ aWithMouse, aIsFocusable, aTabIndex)) {
+ return true;
+ }
+
+ *aIsFocusable = IsFormControlDefaultFocusable(aWithMouse) && !IsDisabled();
+
+ return false;
+}
+
+bool HTMLButtonElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::type) {
+ return aResult.ParseEnumValue(aValue, kButtonTypeTable, false,
+ kButtonDefaultType);
+ }
+
+ if (aAttribute == nsGkAtoms::formmethod) {
+ return aResult.ParseEnumValue(aValue, kFormMethodTable, false);
+ }
+ if (aAttribute == nsGkAtoms::formenctype) {
+ return aResult.ParseEnumValue(aValue, kFormEnctypeTable, false);
+ }
+ }
+
+ return nsGenericHTMLFormControlElementWithState::ParseAttribute(
+ aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult);
+}
+
+bool HTMLButtonElement::IsDisabledForEvents(WidgetEvent* aEvent) {
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(false);
+ nsIFrame* formFrame = do_QueryFrame(formControlFrame);
+ return IsElementDisabledForEvents(aEvent, formFrame);
+}
+
+void HTMLButtonElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ aVisitor.mCanHandle = false;
+
+ if (IsDisabledForEvents(aVisitor.mEvent)) {
+ return;
+ }
+
+ // Track whether we're in the outermost Dispatch invocation that will
+ // cause activation of the input. That is, if we're a click event, or a
+ // DOMActivate that was dispatched directly, this will be set, but if we're
+ // a DOMActivate dispatched from click handling, it will not be set.
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ bool outerActivateEvent =
+ ((mouseEvent && mouseEvent->IsLeftClickEvent()) ||
+ (aVisitor.mEvent->mMessage == eLegacyDOMActivate &&
+ !mInInternalActivate && aVisitor.mEvent->mOriginalTarget == this));
+
+ if (outerActivateEvent) {
+ aVisitor.mItemFlags |= NS_OUTER_ACTIVATE_EVENT;
+ aVisitor.mWantsActivationBehavior = true;
+ }
+
+ nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+}
+
+void HTMLButtonElement::LegacyPreActivationBehavior(
+ EventChainVisitor& aVisitor) {
+ // out-of-spec legacy pre-activation behavior needed because of bug 1803805
+ if (mType == FormControlType::ButtonSubmit && mForm) {
+ aVisitor.mItemFlags |= NS_IN_SUBMIT_CLICK;
+ aVisitor.mItemData = static_cast<Element*>(mForm);
+ // tell the form that we are about to enter a click handler.
+ // that means that if there are scripted submissions, the
+ // latest one will be deferred until after the exit point of the handler.
+ mForm->OnSubmitClickBegin(this);
+ }
+}
+
+nsresult HTMLButtonElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
+ nsresult rv = NS_OK;
+ if (!aVisitor.mPresContext) {
+ return rv;
+ }
+
+ if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) {
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ if (mouseEvent && mouseEvent->IsLeftClickEvent() &&
+ OwnerDoc()->MayHaveDOMActivateListeners()) {
+ // DOMActive event should be trusted since the activation is actually
+ // occurred even if the cause is an untrusted click event.
+ InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent);
+ actEvent.mDetail = 1;
+
+ if (RefPtr<PresShell> presShell = aVisitor.mPresContext->GetPresShell()) {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mInInternalActivate = true;
+ presShell->HandleDOMEventWithTarget(this, &actEvent, &status);
+ mInInternalActivate = false;
+
+ // If activate is cancelled, we must do the same as when click is
+ // cancelled (revert the checkbox to its original value).
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ aVisitor.mEventStatus = status;
+ }
+ }
+ }
+ }
+
+ if (nsEventStatus_eIgnore == aVisitor.mEventStatus) {
+ WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent();
+ if (keyEvent && keyEvent->IsTrusted()) {
+ HandleKeyboardActivation(aVisitor);
+ }
+
+ // Bug 1459231: Temporarily needed till links respect activation target
+ // Then also remove NS_OUTER_ACTIVATE_EVENT
+ if ((aVisitor.mItemFlags & NS_OUTER_ACTIVATE_EVENT) && mForm &&
+ (mType == FormControlType::ButtonReset ||
+ mType == FormControlType::ButtonSubmit)) {
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ }
+ }
+
+ return rv;
+}
+
+void EndSubmitClick(EventChainVisitor& aVisitor) {
+ if ((aVisitor.mItemFlags & NS_IN_SUBMIT_CLICK)) {
+ nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mItemData));
+ RefPtr<HTMLFormElement> form = HTMLFormElement::FromNodeOrNull(content);
+ MOZ_ASSERT(form);
+ // Tell the form that we are about to exit a click handler,
+ // so the form knows not to defer subsequent submissions.
+ // The pending ones that were created during the handler
+ // will be flushed or forgotten.
+ form->OnSubmitClickEnd();
+ // Tell the form to flush a possible pending submission.
+ // the reason is that the script returned false (the event was
+ // not ignored) so if there is a stored submission, it needs to
+ // be submitted immediatelly.
+ // Note, NS_IN_SUBMIT_CLICK is set only when we're in outer activate event.
+ form->FlushPendingSubmission();
+ }
+}
+
+void HTMLButtonElement::ActivationBehavior(EventChainPostVisitor& aVisitor) {
+ if (!aVisitor.mPresContext) {
+ // Should check whether EndSubmitClick is needed here.
+ return;
+ }
+
+ if (!IsDisabled()) {
+ if (mForm) {
+ // Hold a strong ref while dispatching
+ RefPtr<mozilla::dom::HTMLFormElement> form(mForm);
+ if (mType == FormControlType::ButtonReset) {
+ form->MaybeReset(this);
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ } else if (mType == FormControlType::ButtonSubmit) {
+ form->MaybeSubmit(this);
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ // https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type-button-state
+ // NS_FORM_BUTTON_BUTTON do nothing.
+ }
+ if (!GetInvokeTargetElement()) {
+ HandlePopoverTargetAction();
+ } else {
+ HandleInvokeTargetAction();
+ }
+ }
+
+ EndSubmitClick(aVisitor);
+}
+
+void HTMLButtonElement::LegacyCanceledActivationBehavior(
+ EventChainPostVisitor& aVisitor) {
+ // still need to end submission, see bug 1803805
+ // e.g. when parent element of button has event handler preventing default
+ // legacy canceled instead of activation behavior will be run
+ EndSubmitClick(aVisitor);
+}
+
+nsresult HTMLButtonElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv =
+ nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(false);
+
+ return NS_OK;
+}
+
+void HTMLButtonElement::UnbindFromTree(bool aNullParent) {
+ nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent);
+
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(false);
+}
+
+NS_IMETHODIMP
+HTMLButtonElement::Reset() { return NS_OK; }
+
+NS_IMETHODIMP
+HTMLButtonElement::SubmitNamesValues(FormData* aFormData) {
+ //
+ // We only submit if we were the button pressed
+ //
+ if (aFormData->GetSubmitterElement() != this) {
+ return NS_OK;
+ }
+
+ //
+ // Get the name (if no name, no submit)
+ //
+ nsAutoString name;
+ GetHTMLAttr(nsGkAtoms::name, name);
+ if (name.IsEmpty()) {
+ return NS_OK;
+ }
+
+ //
+ // Get the value
+ //
+ nsAutoString value;
+ GetHTMLAttr(nsGkAtoms::value, value);
+
+ //
+ // Submit
+ //
+ return aFormData->AddNameValuePair(name, value);
+}
+
+void HTMLButtonElement::DoneCreatingElement() {
+ if (!mInhibitStateRestoration) {
+ GenerateStateKey();
+ RestoreFormControlState();
+ }
+}
+
+void HTMLButtonElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aNotify && aName == nsGkAtoms::disabled &&
+ aNameSpaceID == kNameSpaceID_None) {
+ mDisabledChanged = true;
+ }
+
+ return nsGenericHTMLFormControlElementWithState::BeforeSetAttr(
+ aNameSpaceID, aName, aValue, aNotify);
+}
+
+void HTMLButtonElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::type) {
+ if (aValue) {
+ mType = FormControlType(aValue->GetEnumValue());
+ } else {
+ mType = FormControlType(kButtonDefaultType->value);
+ }
+ }
+
+ if (aName == nsGkAtoms::type || aName == nsGkAtoms::disabled) {
+ if (aName == nsGkAtoms::disabled) {
+ // This *has* to be called *before* validity state check because
+ // UpdateBarredFromConstraintValidation depends on our disabled state.
+ UpdateDisabledState(aNotify);
+ }
+
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(aNotify);
+ }
+ }
+
+ return nsGenericHTMLFormControlElementWithState::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLButtonElement::SaveState() {
+ if (!mDisabledChanged) {
+ return;
+ }
+
+ PresState* state = GetPrimaryPresState();
+ if (state) {
+ // We do not want to save the real disabled state but the disabled
+ // attribute.
+ state->disabled() = HasAttr(nsGkAtoms::disabled);
+ state->disabledSet() = true;
+ }
+}
+
+bool HTMLButtonElement::RestoreState(PresState* aState) {
+ if (aState && aState->disabledSet() && !aState->disabled()) {
+ SetDisabled(false, IgnoreErrors());
+ }
+ return false;
+}
+
+void HTMLButtonElement::UpdateValidityElementStates(bool aNotify) {
+ AutoStateChangeNotifier notifier(*this, aNotify);
+ RemoveStatesSilently(ElementState::VALIDITY_STATES);
+ if (!IsCandidateForConstraintValidation()) {
+ return;
+ }
+ if (IsValid()) {
+ AddStatesSilently(ElementState::VALID | ElementState::USER_VALID);
+ } else {
+ AddStatesSilently(ElementState::INVALID | ElementState::USER_INVALID);
+ }
+}
+
+JSObject* HTMLButtonElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLButtonElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLButtonElement.h b/dom/html/HTMLButtonElement.h
new file mode 100644
index 0000000000..57bc51c05c
--- /dev/null
+++ b/dom/html/HTMLButtonElement.h
@@ -0,0 +1,149 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLButtonElement_h
+#define mozilla_dom_HTMLButtonElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/ConstraintValidation.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla {
+class EventChainPostVisitor;
+class EventChainPreVisitor;
+namespace dom {
+class FormData;
+
+class HTMLButtonElement final : public nsGenericHTMLFormControlElementWithState,
+ public ConstraintValidation {
+ public:
+ using ConstraintValidation::GetValidationMessage;
+
+ explicit HTMLButtonElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser = NOT_FROM_PARSER);
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
+ HTMLButtonElement, nsGenericHTMLFormControlElementWithState)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ int32_t TabIndexDefault() override;
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLButtonElement, button)
+
+ // Element
+ bool IsInteractiveHTMLContent() const override { return true; }
+
+ // nsGenericHTMLFormElement
+ void SaveState() override;
+ bool RestoreState(PresState* aState) override;
+
+ // overriden nsIFormControl methods
+ NS_IMETHOD Reset() override;
+ NS_IMETHOD SubmitNamesValues(FormData* aFormData) override;
+
+ void FieldSetDisabledChanged(bool aNotify) override;
+
+ // EventTarget
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+ void LegacyPreActivationBehavior(EventChainVisitor& aVisitor) override;
+ MOZ_CAN_RUN_SCRIPT
+ void ActivationBehavior(EventChainPostVisitor& aVisitor) override;
+ void LegacyCanceledActivationBehavior(
+ EventChainPostVisitor& aVisitor) override;
+
+ // nsINode
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ // nsIContent
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+ void DoneCreatingElement() override;
+
+ void UpdateBarredFromConstraintValidation();
+ void UpdateValidityElementStates(bool aNotify);
+ /**
+ * Called when an attribute is about to be changed
+ */
+ void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+ /**
+ * Called when an attribute has just been changed
+ */
+ void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ // nsGenericHTMLElement
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+ bool IsDisabledForEvents(WidgetEvent* aEvent) override;
+
+ // WebIDL
+ bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); }
+ void SetDisabled(bool aDisabled, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::disabled, aDisabled, aError);
+ }
+ // GetFormAction implemented in superclass
+ void SetFormAction(const nsAString& aFormAction, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::formaction, aFormAction, aRv);
+ }
+ void GetFormEnctype(nsAString& aFormEncType);
+ void SetFormEnctype(const nsAString& aFormEnctype, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::formenctype, aFormEnctype, aRv);
+ }
+ void GetFormMethod(nsAString& aFormMethod);
+ void SetFormMethod(const nsAString& aFormMethod, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::formmethod, aFormMethod, aRv);
+ }
+ bool FormNoValidate() const { return GetBoolAttr(nsGkAtoms::formnovalidate); }
+ void SetFormNoValidate(bool aFormNoValidate, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::formnovalidate, aFormNoValidate, aError);
+ }
+ void GetFormTarget(DOMString& aFormTarget) {
+ GetHTMLAttr(nsGkAtoms::formtarget, aFormTarget);
+ }
+ void SetFormTarget(const nsAString& aFormTarget, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::formtarget, aFormTarget, aRv);
+ }
+ void GetName(DOMString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); }
+ void SetName(const nsAString& aName, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aRv);
+ }
+ void GetType(nsAString& aType);
+ void SetType(const nsAString& aType, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::type, aType, aRv);
+ }
+ void GetValue(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::value, aValue); }
+ void SetValue(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::value, aValue, aRv);
+ }
+
+ // Override SetCustomValidity so we update our state properly when it's called
+ // via bindings.
+ void SetCustomValidity(const nsAString& aError);
+
+ protected:
+ virtual ~HTMLButtonElement();
+
+ bool mDisabledChanged : 1;
+ bool mInInternalActivate : 1;
+ bool mInhibitStateRestoration : 1;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_HTMLButtonElement_h
diff --git a/dom/html/HTMLCanvasElement.cpp b/dom/html/HTMLCanvasElement.cpp
new file mode 100644
index 0000000000..93a7bb3787
--- /dev/null
+++ b/dom/html/HTMLCanvasElement.cpp
@@ -0,0 +1,1443 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLCanvasElement.h"
+
+#include "ImageEncoder.h"
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "MediaTrackGraph.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Base64.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/CanvasCaptureMediaStream.h"
+#include "mozilla/dom/CanvasRenderingContext2D.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/GeneratePlaceholderCanvasData.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/HTMLCanvasElementBinding.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/OffscreenCanvas.h"
+#include "mozilla/dom/OffscreenCanvasDisplayHelper.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/layers/CanvasRenderer.h"
+#include "mozilla/layers/WebRenderCanvasRenderer.h"
+#include "mozilla/layers/WebRenderUserData.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/webgpu/CanvasContext.h"
+#include "nsAttrValueInlines.h"
+#include "nsContentUtils.h"
+#include "nsDisplayList.h"
+#include "nsDOMJSUtils.h"
+#include "nsITimer.h"
+#include "nsJSUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsMathUtils.h"
+#include "nsNetUtil.h"
+#include "nsRefreshDriver.h"
+#include "nsStreamUtils.h"
+#include "ActiveLayerTracker.h"
+#include "CanvasUtils.h"
+#include "VRManagerChild.h"
+#include "ClientWebGLContext.h"
+#include "WindowRenderer.h"
+
+using namespace mozilla::layers;
+using namespace mozilla::gfx;
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Canvas)
+
+namespace mozilla::dom {
+
+class RequestedFrameRefreshObserver : public nsARefreshObserver {
+ NS_INLINE_DECL_REFCOUNTING(RequestedFrameRefreshObserver, override)
+
+ public:
+ RequestedFrameRefreshObserver(HTMLCanvasElement* const aOwningElement,
+ nsRefreshDriver* aRefreshDriver,
+ bool aReturnPlaceholderData)
+ : mRegistered(false),
+ mWatching(false),
+ mReturnPlaceholderData(aReturnPlaceholderData),
+ mOwningElement(aOwningElement),
+ mRefreshDriver(aRefreshDriver),
+ mWatchManager(this, AbstractThread::MainThread()),
+ mPendingThrottledCapture(false) {
+ MOZ_ASSERT(mOwningElement);
+ }
+
+ static already_AddRefed<DataSourceSurface> CopySurface(
+ const RefPtr<SourceSurface>& aSurface, bool aReturnPlaceholderData) {
+ RefPtr<DataSourceSurface> data = aSurface->GetDataSurface();
+ if (!data) {
+ return nullptr;
+ }
+
+ DataSourceSurface::ScopedMap read(data, DataSourceSurface::READ);
+ if (!read.IsMapped()) {
+ return nullptr;
+ }
+
+ RefPtr<DataSourceSurface> copy = Factory::CreateDataSourceSurfaceWithStride(
+ data->GetSize(), data->GetFormat(), read.GetStride());
+ if (!copy) {
+ return nullptr;
+ }
+
+ DataSourceSurface::ScopedMap write(copy, DataSourceSurface::WRITE);
+ if (!write.IsMapped()) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(read.GetStride() == write.GetStride());
+ MOZ_ASSERT(data->GetSize() == copy->GetSize());
+ MOZ_ASSERT(data->GetFormat() == copy->GetFormat());
+
+ if (aReturnPlaceholderData) {
+ auto size = write.GetStride() * copy->GetSize().height;
+ auto* data = write.GetData();
+ GeneratePlaceholderCanvasData(size, data);
+ } else {
+ memcpy(write.GetData(), read.GetData(),
+ write.GetStride() * copy->GetSize().height);
+ }
+
+ return copy.forget();
+ }
+
+ void SetReturnPlaceholderData(bool aReturnPlaceholderData) {
+ mReturnPlaceholderData = aReturnPlaceholderData;
+ }
+
+ void NotifyCaptureStateChange() {
+ if (mPendingThrottledCapture) {
+ return;
+ }
+
+ if (!mOwningElement) {
+ return;
+ }
+
+ Watchable<FrameCaptureState>* captureState =
+ mOwningElement->GetFrameCaptureState();
+ if (!captureState) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: No capture state"_ns);
+ return;
+ }
+
+ if (captureState->Ref() == FrameCaptureState::CLEAN) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: CLEAN"_ns);
+ return;
+ }
+
+ if (!mRefreshDriver) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: no refresh driver"_ns);
+ return;
+ }
+
+ if (!mRefreshDriver->IsThrottled()) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: not throttled"_ns);
+ return;
+ }
+
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp next =
+ mLastCaptureTime.IsNull()
+ ? now
+ : mLastCaptureTime + TimeDuration::FromMilliseconds(
+ nsRefreshDriver::DefaultInterval());
+ if (mLastCaptureTime.IsNull() || next <= now) {
+ AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "CaptureFrame direct while throttled"_ns);
+ CaptureFrame(now);
+ return;
+ }
+
+ nsCString str;
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ str.AppendPrintf("Delaying CaptureFrame by %.2fms",
+ (next - now).ToMilliseconds());
+ }
+ AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, str);
+
+ mPendingThrottledCapture = true;
+ AbstractThread::MainThread()->DelayedDispatch(
+ NS_NewRunnableFunction(
+ __func__,
+ [this, self = RefPtr<RequestedFrameRefreshObserver>(this), next] {
+ mPendingThrottledCapture = false;
+ AUTO_PROFILER_MARKER_TEXT(
+ "Canvas CaptureStream", MEDIA_RT, {},
+ "CaptureFrame after delay while throttled"_ns);
+ CaptureFrame(next);
+ }),
+ // next >= now, so this is a guard for (next - now) flooring to 0.
+ std::max<uint32_t>(
+ 1, static_cast<uint32_t>((next - now).ToMilliseconds())));
+ }
+
+ void WillRefresh(TimeStamp aTime) override {
+ AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "CaptureFrame by refresh driver"_ns);
+
+ CaptureFrame(aTime);
+ }
+
+ void CaptureFrame(TimeStamp aTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mOwningElement) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: no owning element"_ns);
+ return;
+ }
+
+ if (mOwningElement->IsWriteOnly()) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: write only"_ns);
+ return;
+ }
+
+ if (auto* captureStateWatchable = mOwningElement->GetFrameCaptureState();
+ captureStateWatchable &&
+ *captureStateWatchable == FrameCaptureState::CLEAN) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: CLEAN"_ns);
+ return;
+ }
+
+ // Mark the context already now, since if the frame capture state is DIRTY
+ // and we catch an early return below (not marking it CLEAN), the next draw
+ // will not trigger a capture state change from the
+ // Watchable<FrameCaptureState>.
+ mOwningElement->MarkContextCleanForFrameCapture();
+
+ mOwningElement->ProcessDestroyedFrameListeners();
+
+ if (!mOwningElement->IsFrameCaptureRequested(aTime)) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: no capture requested"_ns);
+ return;
+ }
+
+ RefPtr<SourceSurface> snapshot;
+ {
+ AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "GetSnapshot"_ns);
+ snapshot = mOwningElement->GetSurfaceSnapshot(nullptr);
+ if (!snapshot) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: snapshot failed"_ns);
+ return;
+ }
+ }
+
+ RefPtr<DataSourceSurface> copy;
+ {
+ AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "CopySurface"_ns);
+ copy = CopySurface(snapshot, mReturnPlaceholderData);
+ if (!copy) {
+ PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "Abort: copy failed"_ns);
+ return;
+ }
+ }
+
+ nsCString str;
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ TimeDuration sinceLast =
+ aTime - (mLastCaptureTime.IsNull() ? aTime : mLastCaptureTime);
+ str.AppendPrintf("Forwarding captured frame %.2fms after last",
+ sinceLast.ToMilliseconds());
+ }
+ AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, str);
+
+ if (!mLastCaptureTime.IsNull() && aTime <= mLastCaptureTime) {
+ aTime = mLastCaptureTime + TimeDuration::FromMilliseconds(1);
+ }
+ mLastCaptureTime = aTime;
+
+ mOwningElement->SetFrameCapture(copy.forget(), aTime);
+ }
+
+ void DetachFromRefreshDriver() {
+ MOZ_ASSERT(mOwningElement);
+ MOZ_ASSERT(mRefreshDriver);
+
+ Unregister();
+ mRefreshDriver = nullptr;
+ mWatchManager.Shutdown();
+ }
+
+ bool IsRegisteredAndWatching() { return mRegistered && mWatching; }
+
+ void Register() {
+ if (!mRegistered) {
+ MOZ_ASSERT(mRefreshDriver);
+ if (mRefreshDriver) {
+ mRefreshDriver->AddRefreshObserver(this, FlushType::Display,
+ "Canvas frame capture listeners");
+ mRegistered = true;
+ }
+ }
+
+ if (mWatching) {
+ return;
+ }
+
+ if (!mOwningElement) {
+ return;
+ }
+
+ if (Watchable<FrameCaptureState>* captureState =
+ mOwningElement->GetFrameCaptureState()) {
+ mWatchManager.Watch(
+ *captureState,
+ &RequestedFrameRefreshObserver::NotifyCaptureStateChange);
+ mWatching = true;
+ }
+ }
+
+ void Unregister() {
+ if (mRegistered) {
+ MOZ_ASSERT(mRefreshDriver);
+ if (mRefreshDriver) {
+ mRefreshDriver->RemoveRefreshObserver(this, FlushType::Display);
+ mRegistered = false;
+ }
+ }
+
+ if (!mWatching) {
+ return;
+ }
+
+ if (!mOwningElement) {
+ return;
+ }
+
+ if (Watchable<FrameCaptureState>* captureState =
+ mOwningElement->GetFrameCaptureState()) {
+ mWatchManager.Unwatch(
+ *captureState,
+ &RequestedFrameRefreshObserver::NotifyCaptureStateChange);
+ mWatching = false;
+ }
+ }
+
+ private:
+ virtual ~RequestedFrameRefreshObserver() {
+ MOZ_ASSERT(!mRefreshDriver);
+ MOZ_ASSERT(!mRegistered);
+ MOZ_ASSERT(!mWatching);
+ }
+
+ bool mRegistered;
+ bool mWatching;
+ bool mReturnPlaceholderData;
+ const WeakPtr<HTMLCanvasElement> mOwningElement;
+ RefPtr<nsRefreshDriver> mRefreshDriver;
+ WatchManager<RequestedFrameRefreshObserver> mWatchManager;
+ TimeStamp mLastCaptureTime;
+ bool mPendingThrottledCapture;
+};
+
+// ---------------------------------------------------------------------------
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HTMLCanvasPrintState, mCanvas, mContext,
+ mCallback)
+
+HTMLCanvasPrintState::HTMLCanvasPrintState(
+ HTMLCanvasElement* aCanvas, nsICanvasRenderingContextInternal* aContext,
+ nsITimerCallback* aCallback)
+ : mIsDone(false),
+ mPendingNotify(false),
+ mCanvas(aCanvas),
+ mContext(aContext),
+ mCallback(aCallback) {}
+
+HTMLCanvasPrintState::~HTMLCanvasPrintState() = default;
+
+/* virtual */
+JSObject* HTMLCanvasPrintState::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MozCanvasPrintState_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* HTMLCanvasPrintState::Context() const { return mContext; }
+
+void HTMLCanvasPrintState::Done() {
+ if (!mPendingNotify && !mIsDone) {
+ // The canvas needs to be invalidated for printing reftests on linux to
+ // work.
+ if (mCanvas) {
+ mCanvas->InvalidateCanvas();
+ }
+ RefPtr<nsRunnableMethod<HTMLCanvasPrintState>> doneEvent =
+ NewRunnableMethod("dom::HTMLCanvasPrintState::NotifyDone", this,
+ &HTMLCanvasPrintState::NotifyDone);
+ if (NS_SUCCEEDED(NS_DispatchToCurrentThread(doneEvent))) {
+ mPendingNotify = true;
+ }
+ }
+}
+
+void HTMLCanvasPrintState::NotifyDone() {
+ mIsDone = true;
+ mPendingNotify = false;
+ if (mCallback) {
+ mCallback->Notify(nullptr);
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+HTMLCanvasElementObserver::HTMLCanvasElementObserver(
+ HTMLCanvasElement* aElement)
+ : mElement(aElement) {
+ RegisterObserverEvents();
+}
+
+HTMLCanvasElementObserver::~HTMLCanvasElementObserver() { Destroy(); }
+
+void HTMLCanvasElementObserver::Destroy() {
+ UnregisterObserverEvents();
+ mElement = nullptr;
+}
+
+void HTMLCanvasElementObserver::RegisterObserverEvents() {
+ if (!mElement) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ MOZ_ASSERT(observerService);
+
+ if (observerService) {
+ observerService->AddObserver(this, "memory-pressure", false);
+ observerService->AddObserver(this, "canvas-device-reset", false);
+ }
+}
+
+void HTMLCanvasElementObserver::UnregisterObserverEvents() {
+ if (!mElement) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ // Do not assert on observerService here. This might be triggered by
+ // the cycle collector at a late enough time, that XPCOM services are
+ // no longer available. See bug 1029504.
+ if (observerService) {
+ observerService->RemoveObserver(this, "memory-pressure");
+ observerService->RemoveObserver(this, "canvas-device-reset");
+ }
+}
+
+NS_IMETHODIMP
+HTMLCanvasElementObserver::Observe(nsISupports*, const char* aTopic,
+ const char16_t*) {
+ if (!mElement) {
+ return NS_OK;
+ }
+
+ if (strcmp(aTopic, "memory-pressure") == 0) {
+ mElement->OnMemoryPressure();
+ } else if (strcmp(aTopic, "canvas-device-reset") == 0) {
+ mElement->OnDeviceReset();
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(HTMLCanvasElementObserver, nsIObserver)
+
+// ---------------------------------------------------------------------------
+
+HTMLCanvasElement::HTMLCanvasElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)),
+ mResetLayer(true),
+ mMaybeModified(false),
+ mWriteOnly(false) {}
+
+HTMLCanvasElement::~HTMLCanvasElement() { Destroy(); }
+
+void HTMLCanvasElement::Destroy() {
+ if (mOffscreenDisplay) {
+ mOffscreenDisplay->DestroyElement();
+ mOffscreenDisplay = nullptr;
+ mImageContainer = nullptr;
+ }
+
+ if (mContextObserver) {
+ mContextObserver->Destroy();
+ mContextObserver = nullptr;
+ }
+
+ ResetPrintCallback();
+ if (mRequestedFrameRefreshObserver) {
+ mRequestedFrameRefreshObserver->DetachFromRefreshDriver();
+ mRequestedFrameRefreshObserver = nullptr;
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLCanvasElement)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLCanvasElement,
+ nsGenericHTMLElement)
+ tmp->Destroy();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mCurrentContext, mPrintCallback, mPrintState,
+ mOriginalCanvas, mOffscreenCanvas)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLCanvasElement,
+ nsGenericHTMLElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCurrentContext, mPrintCallback,
+ mPrintState, mOriginalCanvas,
+ mOffscreenCanvas)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLCanvasElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLCanvasElement)
+
+/* virtual */
+JSObject* HTMLCanvasElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLCanvasElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<nsICanvasRenderingContextInternal>
+HTMLCanvasElement::CreateContext(CanvasContextType aContextType) {
+ // Note that the compositor backend will be LAYERS_NONE if there is no widget.
+ RefPtr<nsICanvasRenderingContextInternal> ret =
+ CreateContextHelper(aContextType, GetCompositorBackendType());
+ if (NS_WARN_IF(!ret)) {
+ return nullptr;
+ }
+
+ // Add Observer for webgl canvas.
+ if (aContextType == CanvasContextType::WebGL1 ||
+ aContextType == CanvasContextType::WebGL2 ||
+ aContextType == CanvasContextType::Canvas2D) {
+ if (!mContextObserver) {
+ mContextObserver = new HTMLCanvasElementObserver(this);
+ }
+ }
+
+ ret->SetCanvasElement(this);
+ return ret.forget();
+}
+
+nsresult HTMLCanvasElement::UpdateContext(
+ JSContext* aCx, JS::Handle<JS::Value> aNewContextOptions,
+ ErrorResult& aRvForDictionaryInit) {
+ nsresult rv = CanvasRenderingContextHelper::UpdateContext(
+ aCx, aNewContextOptions, aRvForDictionaryInit);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // If we have a mRequestedFrameRefreshObserver that wasn't fully registered,
+ // retry that now.
+ if (mRequestedFrameRefreshObserver.get() &&
+ !mRequestedFrameRefreshObserver->IsRegisteredAndWatching()) {
+ mRequestedFrameRefreshObserver->Register();
+ }
+
+ return NS_OK;
+}
+
+nsIntSize HTMLCanvasElement::GetWidthHeight() {
+ nsIntSize size(DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT);
+ const nsAttrValue* value;
+
+ if ((value = GetParsedAttr(nsGkAtoms::width)) &&
+ value->Type() == nsAttrValue::eInteger) {
+ size.width = value->GetIntegerValue();
+ }
+
+ if ((value = GetParsedAttr(nsGkAtoms::height)) &&
+ value->Type() == nsAttrValue::eInteger) {
+ size.height = value->GetIntegerValue();
+ }
+
+ MOZ_ASSERT(size.width >= 0 && size.height >= 0,
+ "we should've required <canvas> width/height attrs to be "
+ "unsigned (non-negative) values");
+
+ return size;
+}
+
+void HTMLCanvasElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLCanvasElement::OnAttrSetButNotChanged(
+ int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+
+ return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName,
+ aValue, aNotify);
+}
+
+void HTMLCanvasElement::AfterMaybeChangeAttr(int32_t aNamespaceID,
+ nsAtom* aName, bool aNotify) {
+ if (mCurrentContext && aNamespaceID == kNameSpaceID_None &&
+ (aName == nsGkAtoms::width || aName == nsGkAtoms::height ||
+ aName == nsGkAtoms::moz_opaque)) {
+ ErrorResult dummy;
+ UpdateContext(nullptr, JS::NullHandleValue, dummy);
+ }
+}
+
+void HTMLCanvasElement::HandlePrintCallback(nsPresContext* aPresContext) {
+ // Only call the print callback here if 1) we're in a print testing mode or
+ // print preview mode, 2) the canvas has a print callback and 3) the callback
+ // hasn't already been called. For real printing the callback is handled in
+ // nsPageSequenceFrame::PrePrintNextSheet.
+ if ((aPresContext->Type() == nsPresContext::eContext_PageLayout ||
+ aPresContext->Type() == nsPresContext::eContext_PrintPreview) &&
+ !mPrintState && GetMozPrintCallback()) {
+ DispatchPrintCallback(nullptr);
+ }
+}
+
+nsresult HTMLCanvasElement::DispatchPrintCallback(nsITimerCallback* aCallback) {
+ // For print reftests the context may not be initialized yet, so get a context
+ // so mCurrentContext is set.
+ if (!mCurrentContext) {
+ nsresult rv;
+ nsCOMPtr<nsISupports> context;
+ rv = GetContext(u"2d"_ns, getter_AddRefs(context));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ mPrintState = new HTMLCanvasPrintState(this, mCurrentContext, aCallback);
+
+ RefPtr<nsRunnableMethod<HTMLCanvasElement>> renderEvent =
+ NewRunnableMethod("dom::HTMLCanvasElement::CallPrintCallback", this,
+ &HTMLCanvasElement::CallPrintCallback);
+ return OwnerDoc()->Dispatch(renderEvent.forget());
+}
+
+void HTMLCanvasElement::CallPrintCallback() {
+ AUTO_PROFILER_MARKER_TEXT("HTMLCanvasElement Printing", LAYOUT_Printing, {},
+ "HTMLCanvasElement::CallPrintCallback"_ns);
+ if (!mPrintState) {
+ // `mPrintState` might have been destroyed by cancelling the previous
+ // printing (especially the canvas frame destruction) during processing
+ // event loops in the printing.
+ return;
+ }
+ RefPtr<PrintCallback> callback = GetMozPrintCallback();
+ RefPtr<HTMLCanvasPrintState> state = mPrintState;
+ callback->Call(*state);
+}
+
+void HTMLCanvasElement::ResetPrintCallback() {
+ if (mPrintState) {
+ mPrintState = nullptr;
+ }
+}
+
+bool HTMLCanvasElement::IsPrintCallbackDone() {
+ if (mPrintState == nullptr) {
+ return true;
+ }
+
+ return mPrintState->mIsDone;
+}
+
+HTMLCanvasElement* HTMLCanvasElement::GetOriginalCanvas() {
+ return mOriginalCanvas ? mOriginalCanvas.get() : this;
+}
+
+nsresult HTMLCanvasElement::CopyInnerTo(HTMLCanvasElement* aDest) {
+ nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest);
+ NS_ENSURE_SUCCESS(rv, rv);
+ Document* destDoc = aDest->OwnerDoc();
+ if (destDoc->IsStaticDocument()) {
+ // The Firefox print preview code can create a static clone from an
+ // existing static clone, so we may not be the original 'canvas' element.
+ aDest->mOriginalCanvas = GetOriginalCanvas();
+
+ if (GetMozPrintCallback()) {
+ destDoc->SetHasPrintCallbacks();
+ }
+
+ // We make sure that the canvas is not zero sized since that would cause
+ // the DrawImage call below to return an error, which would cause printing
+ // to fail.
+ nsIntSize size = GetWidthHeight();
+ if (size.height > 0 && size.width > 0) {
+ nsCOMPtr<nsISupports> cxt;
+ aDest->GetContext(u"2d"_ns, getter_AddRefs(cxt));
+ RefPtr<CanvasRenderingContext2D> context2d =
+ static_cast<CanvasRenderingContext2D*>(cxt.get());
+ if (context2d && !mPrintCallback) {
+ CanvasImageSource source;
+ source.SetAsHTMLCanvasElement() = this;
+ ErrorResult err;
+ context2d->DrawImage(source, 0.0, 0.0, err);
+ rv = err.StealNSResult();
+ }
+ }
+ }
+ return rv;
+}
+
+nsChangeHint HTMLCanvasElement::GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const {
+ nsChangeHint retval =
+ nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType);
+ if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height) {
+ retval |= NS_STYLE_HINT_REFLOW;
+ } else if (aAttribute == nsGkAtoms::moz_opaque) {
+ retval |= NS_STYLE_HINT_VISUAL;
+ }
+ return retval;
+}
+
+void HTMLCanvasElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapAspectRatioInto(aBuilder);
+ MapCommonAttributesInto(aBuilder);
+}
+
+nsMapRuleToAttributesFunc HTMLCanvasElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+NS_IMETHODIMP_(bool)
+HTMLCanvasElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::width}, {nsGkAtoms::height}, {nullptr}};
+ static const MappedAttributeEntry* const map[] = {attributes,
+ sCommonAttributeMap};
+ return FindAttributeDependence(aAttribute, map);
+}
+
+bool HTMLCanvasElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None &&
+ (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height)) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLCanvasElement::ToDataURL(JSContext* aCx, const nsAString& aType,
+ JS::Handle<JS::Value> aParams,
+ nsAString& aDataURL,
+ nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aRv) {
+ // mWriteOnly check is redundant, but optimizes for the common case.
+ if (mWriteOnly && !CallerCanRead(aSubjectPrincipal)) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsresult rv = ToDataURLImpl(aCx, aSubjectPrincipal, aType, aParams, aDataURL);
+ if (NS_FAILED(rv)) {
+ aDataURL.AssignLiteral("data:,");
+ }
+}
+
+void HTMLCanvasElement::SetMozPrintCallback(PrintCallback* aCallback) {
+ mPrintCallback = aCallback;
+}
+
+PrintCallback* HTMLCanvasElement::GetMozPrintCallback() const {
+ if (mOriginalCanvas) {
+ return mOriginalCanvas->GetMozPrintCallback();
+ }
+ return mPrintCallback;
+}
+
+static uint32_t sCaptureSourceId = 0;
+class CanvasCaptureTrackSource : public MediaStreamTrackSource {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CanvasCaptureTrackSource,
+ MediaStreamTrackSource)
+
+ CanvasCaptureTrackSource(nsIPrincipal* aPrincipal,
+ CanvasCaptureMediaStream* aCaptureStream)
+ : MediaStreamTrackSource(
+ aPrincipal, nsString(),
+ TrackingId(TrackingId::Source::Canvas, sCaptureSourceId++,
+ TrackingId::TrackAcrossProcesses::Yes)),
+ mCaptureStream(aCaptureStream) {}
+
+ MediaSourceEnum GetMediaSource() const override {
+ return MediaSourceEnum::Other;
+ }
+
+ bool HasAlpha() const override {
+ if (!mCaptureStream || !mCaptureStream->Canvas()) {
+ // In cycle-collection
+ return false;
+ }
+ return !mCaptureStream->Canvas()->GetIsOpaque();
+ }
+
+ void Stop() override {
+ if (!mCaptureStream) {
+ return;
+ }
+
+ mCaptureStream->StopCapture();
+ }
+
+ void Disable() override {}
+
+ void Enable() override {}
+
+ private:
+ virtual ~CanvasCaptureTrackSource() = default;
+
+ RefPtr<CanvasCaptureMediaStream> mCaptureStream;
+};
+
+NS_IMPL_ADDREF_INHERITED(CanvasCaptureTrackSource, MediaStreamTrackSource)
+NS_IMPL_RELEASE_INHERITED(CanvasCaptureTrackSource, MediaStreamTrackSource)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanvasCaptureTrackSource)
+NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(CanvasCaptureTrackSource,
+ MediaStreamTrackSource, mCaptureStream)
+
+already_AddRefed<CanvasCaptureMediaStream> HTMLCanvasElement::CaptureStream(
+ const Optional<double>& aFrameRate, nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aRv) {
+ if (IsWriteOnly()) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return nullptr;
+ }
+
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ if (!window) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ auto stream = MakeRefPtr<CanvasCaptureMediaStream>(window, this);
+
+ nsCOMPtr<nsIPrincipal> principal = NodePrincipal();
+ nsresult rv = stream->Init(aFrameRate, principal);
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+
+ RefPtr<MediaStreamTrack> track =
+ new VideoStreamTrack(window, stream->GetSourceStream(),
+ new CanvasCaptureTrackSource(principal, stream));
+ stream->AddTrackInternal(track);
+
+ // Check site-specific permission and display prompt if appropriate.
+ // If no permission, arrange for the frame capture listener to return
+ // all-white, opaque image data.
+ bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(
+ OwnerDoc(), nsContentUtils::GetCurrentJSContext(), aSubjectPrincipal);
+
+ rv = RegisterFrameCaptureListener(stream->FrameCaptureListener(),
+ usePlaceholder);
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+
+ return stream.forget();
+}
+
+nsresult HTMLCanvasElement::ExtractData(JSContext* aCx,
+ nsIPrincipal& aSubjectPrincipal,
+ nsAString& aType,
+ const nsAString& aOptions,
+ nsIInputStream** aStream) {
+ // Check site-specific permission and display prompt if appropriate.
+ // If no permission, return all-white, opaque image data.
+ bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(
+ OwnerDoc(), aCx, aSubjectPrincipal);
+
+ if (!usePlaceholder) {
+ auto size = GetWidthHeight();
+ CanvasContextType type = GetCurrentContextType();
+ CanvasFeatureUsage featureUsage = CanvasFeatureUsage::None;
+ if (type == CanvasContextType::Canvas2D) {
+ if (auto ctx =
+ static_cast<CanvasRenderingContext2D*>(GetCurrentContext())) {
+ featureUsage = ctx->FeatureUsage();
+ }
+ }
+
+ CanvasUsage usage(size, type, featureUsage);
+ OwnerDoc()->RecordCanvasUsage(usage);
+ }
+
+ return ImageEncoder::ExtractData(aType, aOptions, GetSize(), usePlaceholder,
+ mCurrentContext, mOffscreenDisplay, aStream);
+}
+
+nsresult HTMLCanvasElement::ToDataURLImpl(JSContext* aCx,
+ nsIPrincipal& aSubjectPrincipal,
+ const nsAString& aMimeType,
+ const JS::Value& aEncoderOptions,
+ nsAString& aDataURL) {
+ nsIntSize size = GetWidthHeight();
+ if (size.height == 0 || size.width == 0) {
+ aDataURL = u"data:,"_ns;
+ return NS_OK;
+ }
+
+ nsAutoString type;
+ nsContentUtils::ASCIIToLower(aMimeType, type);
+
+ nsAutoString params;
+ bool usingCustomParseOptions;
+ nsresult rv =
+ ParseParams(aCx, type, aEncoderOptions, params, &usingCustomParseOptions);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIInputStream> stream;
+ rv =
+ ExtractData(aCx, aSubjectPrincipal, type, params, getter_AddRefs(stream));
+
+ // If there are unrecognized custom parse options, we should fall back to
+ // the default values for the encoder without any options at all.
+ if (rv == NS_ERROR_INVALID_ARG && usingCustomParseOptions) {
+ rv = ExtractData(aCx, aSubjectPrincipal, type, u""_ns,
+ getter_AddRefs(stream));
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // build data URL string
+ aDataURL = u"data:"_ns + type + u";base64,"_ns;
+
+ uint64_t count;
+ rv = stream->Available(&count);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(count <= UINT32_MAX, NS_ERROR_FILE_TOO_BIG);
+
+ return Base64EncodeInputStream(stream, aDataURL, (uint32_t)count,
+ aDataURL.Length());
+}
+
+UniquePtr<uint8_t[]> HTMLCanvasElement::GetImageBuffer(
+ int32_t* aOutFormat, gfx::IntSize* aOutImageSize) {
+ if (mCurrentContext) {
+ return mCurrentContext->GetImageBuffer(aOutFormat, aOutImageSize);
+ }
+ if (mOffscreenDisplay) {
+ return mOffscreenDisplay->GetImageBuffer(aOutFormat, aOutImageSize);
+ }
+ return nullptr;
+}
+
+void HTMLCanvasElement::ToBlob(JSContext* aCx, BlobCallback& aCallback,
+ const nsAString& aType,
+ JS::Handle<JS::Value> aParams,
+ nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aRv) {
+ // mWriteOnly check is redundant, but optimizes for the common case.
+ if (mWriteOnly && !CallerCanRead(aSubjectPrincipal)) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+ MOZ_ASSERT(global);
+
+ nsIntSize elemSize = GetWidthHeight();
+ if (elemSize.width == 0 || elemSize.height == 0) {
+ // According to spec, blob should return null if either its horizontal
+ // dimension or its vertical dimension is zero. See link below.
+ // https://html.spec.whatwg.org/multipage/scripting.html#dom-canvas-toblob
+ OwnerDoc()->Dispatch(NewRunnableMethod<Blob*, const char*>(
+ "dom::HTMLCanvasElement::ToBlob", &aCallback,
+ static_cast<void (BlobCallback::*)(Blob*, const char*)>(
+ &BlobCallback::Call),
+ nullptr, nullptr));
+ return;
+ }
+
+ // Check site-specific permission and display prompt if appropriate.
+ // If no permission, return all-white, opaque image data.
+ bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(
+ OwnerDoc(), aCx, aSubjectPrincipal);
+ CanvasRenderingContextHelper::ToBlob(aCx, global, aCallback, aType, aParams,
+ usePlaceholder, aRv);
+}
+
+OffscreenCanvas* HTMLCanvasElement::TransferControlToOffscreen(
+ ErrorResult& aRv) {
+ if (mCurrentContext || mOffscreenCanvas) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ MOZ_ASSERT(!mOffscreenDisplay);
+
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
+ if (!win) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ LayersBackend backend = LayersBackend::LAYERS_NONE;
+ TextureType textureType = TextureType::Unknown;
+ nsIWidget* docWidget = nsContentUtils::WidgetForDocument(OwnerDoc());
+ if (docWidget) {
+ WindowRenderer* renderer = docWidget->GetWindowRenderer();
+ if (renderer) {
+ backend = renderer->GetCompositorBackendType();
+ textureType = TexTypeForWebgl(renderer->AsKnowsCompositor());
+ }
+ }
+
+ nsIntSize sz = GetWidthHeight();
+ mOffscreenDisplay =
+ MakeRefPtr<OffscreenCanvasDisplayHelper>(this, sz.width, sz.height);
+ mOffscreenCanvas =
+ new OffscreenCanvas(win->AsGlobal(), sz.width, sz.height, backend,
+ textureType, do_AddRef(mOffscreenDisplay));
+ if (mWriteOnly) {
+ mOffscreenCanvas->SetWriteOnly(mExpandedReader);
+ }
+
+ if (!mContextObserver) {
+ mContextObserver = new HTMLCanvasElementObserver(this);
+ }
+
+ return mOffscreenCanvas;
+}
+
+nsresult HTMLCanvasElement::GetContext(const nsAString& aContextId,
+ nsISupports** aContext) {
+ ErrorResult rv;
+ mMaybeModified = true; // For FirstContentfulPaint
+ *aContext = GetContext(nullptr, aContextId, JS::NullHandleValue, rv).take();
+ return rv.StealNSResult();
+}
+
+already_AddRefed<nsISupports> HTMLCanvasElement::GetContext(
+ JSContext* aCx, const nsAString& aContextId,
+ JS::Handle<JS::Value> aContextOptions, ErrorResult& aRv) {
+ if (mOffscreenCanvas) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ mMaybeModified = true; // For FirstContentfulPaint
+ return CanvasRenderingContextHelper::GetOrCreateContext(
+ aCx, aContextId,
+ aContextOptions.isObject() ? aContextOptions : JS::NullHandleValue, aRv);
+}
+
+nsIntSize HTMLCanvasElement::GetSize() { return GetWidthHeight(); }
+
+bool HTMLCanvasElement::IsWriteOnly() const { return mWriteOnly; }
+
+void HTMLCanvasElement::SetWriteOnly(
+ nsIPrincipal* aExpandedReader /* = nullptr */) {
+ mExpandedReader = aExpandedReader;
+ mWriteOnly = true;
+ if (mOffscreenCanvas) {
+ mOffscreenCanvas->SetWriteOnly(aExpandedReader);
+ }
+}
+
+bool HTMLCanvasElement::CallerCanRead(nsIPrincipal& aPrincipal) const {
+ if (!mWriteOnly) {
+ return true;
+ }
+
+ // If mExpandedReader is set, this canvas was tainted only by
+ // mExpandedReader's resources. So allow reading if the subject
+ // principal subsumes mExpandedReader.
+ if (mExpandedReader && aPrincipal.Subsumes(mExpandedReader)) {
+ return true;
+ }
+
+ return nsContentUtils::PrincipalHasPermission(aPrincipal,
+ nsGkAtoms::all_urlsPermission);
+}
+
+void HTMLCanvasElement::SetWidth(uint32_t aWidth, ErrorResult& aRv) {
+ if (mOffscreenCanvas) {
+ aRv.ThrowInvalidStateError(
+ "Cannot set width of placeholder canvas transferred to "
+ "OffscreenCanvas.");
+ return;
+ }
+
+ SetUnsignedIntAttr(nsGkAtoms::width, aWidth, DEFAULT_CANVAS_WIDTH, aRv);
+}
+
+void HTMLCanvasElement::SetHeight(uint32_t aHeight, ErrorResult& aRv) {
+ if (mOffscreenCanvas) {
+ aRv.ThrowInvalidStateError(
+ "Cannot set height of placeholder canvas transferred to "
+ "OffscreenCanvas.");
+ return;
+ }
+
+ SetUnsignedIntAttr(nsGkAtoms::height, aHeight, DEFAULT_CANVAS_HEIGHT, aRv);
+}
+
+void HTMLCanvasElement::SetSize(const nsIntSize& aSize, ErrorResult& aRv) {
+ if (mOffscreenCanvas) {
+ aRv.ThrowInvalidStateError(
+ "Cannot set width of placeholder canvas transferred to "
+ "OffscreenCanvas.");
+ return;
+ }
+
+ if (NS_WARN_IF(aSize.IsEmpty())) {
+ aRv.ThrowRangeError("Canvas size is empty, must be non-empty.");
+ return;
+ }
+
+ SetUnsignedIntAttr(nsGkAtoms::width, aSize.width, DEFAULT_CANVAS_WIDTH, aRv);
+ MOZ_ASSERT(!aRv.Failed());
+ SetUnsignedIntAttr(nsGkAtoms::height, aSize.height, DEFAULT_CANVAS_HEIGHT,
+ aRv);
+ MOZ_ASSERT(!aRv.Failed());
+}
+
+void HTMLCanvasElement::FlushOffscreenCanvas() {
+ if (mOffscreenDisplay) {
+ mOffscreenDisplay->FlushForDisplay();
+ }
+}
+
+void HTMLCanvasElement::InvalidateCanvasPlaceholder(uint32_t aWidth,
+ uint32_t aHeight) {
+ ErrorResult rv;
+ SetUnsignedIntAttr(nsGkAtoms::width, aWidth, DEFAULT_CANVAS_WIDTH, rv);
+ MOZ_ASSERT(!rv.Failed());
+ SetUnsignedIntAttr(nsGkAtoms::height, aHeight, DEFAULT_CANVAS_HEIGHT, rv);
+ MOZ_ASSERT(!rv.Failed());
+}
+
+void HTMLCanvasElement::InvalidateCanvasContent(const gfx::Rect* damageRect) {
+ // Cache the current ImageContainer to avoid contention on the mutex.
+ if (mOffscreenDisplay) {
+ mImageContainer = mOffscreenDisplay->GetImageContainer();
+ }
+
+ // We don't need to flush anything here; if there's no frame or if
+ // we plan to reframe we don't need to invalidate it anyway.
+ nsIFrame* frame = GetPrimaryFrame();
+ if (!frame) return;
+
+ // When using layers-free WebRender, we cannot invalidate the layer (because
+ // there isn't one). Instead, we mark the CanvasRenderer dirty and scheduling
+ // an empty transaction which is effectively equivalent.
+ CanvasRenderer* renderer = nullptr;
+ const auto key = static_cast<uint32_t>(DisplayItemType::TYPE_CANVAS);
+ RefPtr<WebRenderCanvasData> data =
+ GetWebRenderUserData<WebRenderCanvasData>(frame, key);
+ if (data) {
+ renderer = data->GetCanvasRenderer();
+ }
+
+ if (renderer) {
+ renderer->SetDirty();
+ frame->SchedulePaint(nsIFrame::PAINT_COMPOSITE_ONLY);
+ } else {
+ if (damageRect) {
+ nsIntSize size = GetWidthHeight();
+ if (size.width != 0 && size.height != 0) {
+ gfx::IntRect invalRect = gfx::IntRect::Truncate(*damageRect);
+ frame->InvalidateLayer(DisplayItemType::TYPE_CANVAS, &invalRect);
+ }
+ } else {
+ frame->InvalidateLayer(DisplayItemType::TYPE_CANVAS);
+ }
+
+ // This path is taken in two situations:
+ // 1) WebRender is enabled and has not yet processed a display list.
+ // 2) WebRender is disabled and layer invalidation failed.
+ // In both cases, schedule a full paint to properly update canvas.
+ frame->SchedulePaint(nsIFrame::PAINT_DEFAULT, false);
+ }
+
+ /*
+ * Treat canvas invalidations as animation activity for JS. Frequently
+ * invalidating a canvas will feed into heuristics and cause JIT code to be
+ * kept around longer, for smoother animations.
+ */
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
+
+ if (win) {
+ if (JSObject* obj = win->AsGlobal()->GetGlobalJSObject()) {
+ js::NotifyAnimationActivity(obj);
+ }
+ }
+}
+
+void HTMLCanvasElement::InvalidateCanvas() {
+ // We don't need to flush anything here; if there's no frame or if
+ // we plan to reframe we don't need to invalidate it anyway.
+ nsIFrame* frame = GetPrimaryFrame();
+ if (!frame) return;
+
+ frame->InvalidateFrame();
+}
+
+bool HTMLCanvasElement::GetIsOpaque() {
+ if (mCurrentContext) {
+ return mCurrentContext->GetIsOpaque();
+ }
+
+ return GetOpaqueAttr();
+}
+
+bool HTMLCanvasElement::GetOpaqueAttr() {
+ return HasAttr(nsGkAtoms::moz_opaque);
+}
+
+CanvasContextType HTMLCanvasElement::GetCurrentContextType() {
+ if (mCurrentContextType == CanvasContextType::NoContext &&
+ mOffscreenDisplay) {
+ mCurrentContextType = mOffscreenDisplay->GetContextType();
+ }
+ return mCurrentContextType;
+}
+
+already_AddRefed<Image> HTMLCanvasElement::GetAsImage() {
+ if (mOffscreenDisplay) {
+ return mOffscreenDisplay->GetAsImage();
+ }
+
+ if (mCurrentContext) {
+ return mCurrentContext->GetAsImage();
+ }
+
+ return nullptr;
+}
+
+bool HTMLCanvasElement::UpdateWebRenderCanvasData(
+ nsDisplayListBuilder* aBuilder, WebRenderCanvasData* aCanvasData) {
+ MOZ_ASSERT(!mOffscreenDisplay);
+
+ if (mCurrentContext) {
+ return mCurrentContext->UpdateWebRenderCanvasData(aBuilder, aCanvasData);
+ }
+
+ // Clear CanvasRenderer of WebRenderCanvasData
+ aCanvasData->ClearCanvasRenderer();
+ return false;
+}
+
+bool HTMLCanvasElement::InitializeCanvasRenderer(nsDisplayListBuilder* aBuilder,
+ CanvasRenderer* aRenderer) {
+ MOZ_ASSERT(!mOffscreenDisplay);
+
+ if (mCurrentContext) {
+ return mCurrentContext->InitializeCanvasRenderer(aBuilder, aRenderer);
+ }
+
+ return false;
+}
+
+void HTMLCanvasElement::MarkContextClean() {
+ if (!mCurrentContext) return;
+
+ mCurrentContext->MarkContextClean();
+}
+
+void HTMLCanvasElement::MarkContextCleanForFrameCapture() {
+ if (!mCurrentContext) return;
+
+ mCurrentContext->MarkContextCleanForFrameCapture();
+}
+
+Watchable<FrameCaptureState>* HTMLCanvasElement::GetFrameCaptureState() {
+ if (!mCurrentContext) {
+ return nullptr;
+ }
+ return mCurrentContext->GetFrameCaptureState();
+}
+
+nsresult HTMLCanvasElement::RegisterFrameCaptureListener(
+ FrameCaptureListener* aListener, bool aReturnPlaceholderData) {
+ WeakPtr<FrameCaptureListener> listener = aListener;
+
+ if (mRequestedFrameListeners.Contains(listener)) {
+ return NS_OK;
+ }
+
+ if (!mRequestedFrameRefreshObserver) {
+ Document* doc = OwnerDoc();
+ if (!doc) {
+ return NS_ERROR_FAILURE;
+ }
+
+ PresShell* shell = nsContentUtils::FindPresShellForDocument(doc);
+ if (!shell) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsPresContext* context = shell->GetPresContext();
+ if (!context) {
+ return NS_ERROR_FAILURE;
+ }
+
+ context = context->GetRootPresContext();
+ if (!context) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsRefreshDriver* driver = context->RefreshDriver();
+ if (!driver) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mRequestedFrameRefreshObserver =
+ new RequestedFrameRefreshObserver(this, driver, aReturnPlaceholderData);
+ } else {
+ mRequestedFrameRefreshObserver->SetReturnPlaceholderData(
+ aReturnPlaceholderData);
+ }
+
+ mRequestedFrameListeners.AppendElement(listener);
+ mRequestedFrameRefreshObserver->Register();
+ return NS_OK;
+}
+
+bool HTMLCanvasElement::IsFrameCaptureRequested(const TimeStamp& aTime) const {
+ for (WeakPtr<FrameCaptureListener> listener : mRequestedFrameListeners) {
+ if (!listener) {
+ continue;
+ }
+
+ if (listener->FrameCaptureRequested(aTime)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void HTMLCanvasElement::ProcessDestroyedFrameListeners() {
+ // Remove destroyed listeners from the list.
+ mRequestedFrameListeners.RemoveElementsBy(
+ [](const auto& weakListener) { return !weakListener; });
+
+ if (mRequestedFrameListeners.IsEmpty()) {
+ mRequestedFrameRefreshObserver->Unregister();
+ }
+}
+
+void HTMLCanvasElement::SetFrameCapture(
+ already_AddRefed<SourceSurface> aSurface, const TimeStamp& aTime) {
+ RefPtr<SourceSurface> surface = aSurface;
+ RefPtr<SourceSurfaceImage> image =
+ new SourceSurfaceImage(surface->GetSize(), surface);
+
+ for (WeakPtr<FrameCaptureListener> listener : mRequestedFrameListeners) {
+ if (!listener) {
+ continue;
+ }
+
+ RefPtr<Image> imageRefCopy = image.get();
+ listener->NewFrame(imageRefCopy.forget(), aTime);
+ }
+}
+
+already_AddRefed<SourceSurface> HTMLCanvasElement::GetSurfaceSnapshot(
+ gfxAlphaType* const aOutAlphaType, DrawTarget* aTarget) {
+ if (mCurrentContext) {
+ return mCurrentContext->GetOptimizedSnapshot(aTarget, aOutAlphaType);
+ } else if (mOffscreenDisplay) {
+ return mOffscreenDisplay->GetSurfaceSnapshot();
+ }
+ return nullptr;
+}
+
+layers::LayersBackend HTMLCanvasElement::GetCompositorBackendType() const {
+ nsIWidget* docWidget = nsContentUtils::WidgetForDocument(OwnerDoc());
+ if (docWidget) {
+ WindowRenderer* renderer = docWidget->GetWindowRenderer();
+ if (renderer) {
+ return renderer->GetCompositorBackendType();
+ }
+ }
+
+ return LayersBackend::LAYERS_NONE;
+}
+
+void HTMLCanvasElement::OnMemoryPressure() {
+ // FIXME(aosmond): We need to implement memory pressure handling for
+ // OffscreenCanvas when it is on worker threads. See bug 1746260.
+
+ if (mCurrentContext) {
+ mCurrentContext->OnMemoryPressure();
+ }
+}
+
+void HTMLCanvasElement::OnDeviceReset() {
+ if (!mOffscreenCanvas && mCurrentContext) {
+ mCurrentContext->ResetBitmap();
+ }
+}
+
+ClientWebGLContext* HTMLCanvasElement::GetWebGLContext() {
+ if (GetCurrentContextType() != CanvasContextType::WebGL1 &&
+ GetCurrentContextType() != CanvasContextType::WebGL2) {
+ return nullptr;
+ }
+
+ return static_cast<ClientWebGLContext*>(GetCurrentContext());
+}
+
+webgpu::CanvasContext* HTMLCanvasElement::GetWebGPUContext() {
+ if (GetCurrentContextType() != CanvasContextType::WebGPU) {
+ return nullptr;
+ }
+
+ return static_cast<webgpu::CanvasContext*>(GetCurrentContext());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLCanvasElement.h b/dom/html/HTMLCanvasElement.h
new file mode 100644
index 0000000000..586a43fedc
--- /dev/null
+++ b/dom/html/HTMLCanvasElement.h
@@ -0,0 +1,447 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(mozilla_dom_HTMLCanvasElement_h)
+# define mozilla_dom_HTMLCanvasElement_h
+
+# include "mozilla/Attributes.h"
+# include "mozilla/StateWatching.h"
+# include "mozilla/WeakPtr.h"
+# include "nsIDOMEventListener.h"
+# include "nsIObserver.h"
+# include "nsGenericHTMLElement.h"
+# include "nsGkAtoms.h"
+# include "nsSize.h"
+# include "nsError.h"
+
+# include "mozilla/dom/CanvasRenderingContextHelper.h"
+# include "mozilla/gfx/Rect.h"
+# include "mozilla/layers/LayersTypes.h"
+
+class nsICanvasRenderingContextInternal;
+class nsIInputStream;
+class nsITimerCallback;
+enum class gfxAlphaType;
+enum class FrameCaptureState : uint8_t;
+
+namespace mozilla {
+
+class nsDisplayListBuilder;
+class ClientWebGLContext;
+
+namespace layers {
+class CanvasRenderer;
+class Image;
+class ImageContainer;
+class Layer;
+class LayerManager;
+class OOPCanvasRenderer;
+class SharedSurfaceTextureClient;
+class WebRenderCanvasData;
+} // namespace layers
+namespace gfx {
+class DrawTarget;
+class SourceSurface;
+class VRLayerChild;
+} // namespace gfx
+namespace webgpu {
+class CanvasContext;
+} // namespace webgpu
+
+namespace dom {
+class BlobCallback;
+class CanvasCaptureMediaStream;
+class File;
+class HTMLCanvasPrintState;
+class OffscreenCanvas;
+class OffscreenCanvasDisplayHelper;
+class PrintCallback;
+class PWebGLChild;
+class RequestedFrameRefreshObserver;
+
+// Listen visibilitychange and memory-pressure event and inform
+// context when event is fired.
+class HTMLCanvasElementObserver final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ explicit HTMLCanvasElementObserver(HTMLCanvasElement* aElement);
+ void Destroy();
+
+ void RegisterObserverEvents();
+ void UnregisterObserverEvents();
+
+ private:
+ ~HTMLCanvasElementObserver();
+
+ HTMLCanvasElement* mElement;
+};
+
+/*
+ * FrameCaptureListener is used by captureStream() as a way of getting video
+ * frames from the canvas. On a refresh driver tick after something has been
+ * drawn to the canvas since the last such tick, all registered
+ * FrameCaptureListeners that report true for FrameCaptureRequested() will be
+ * given a copy of the just-painted canvas.
+ * All FrameCaptureListeners get the same copy.
+ */
+class FrameCaptureListener : public SupportsWeakPtr {
+ public:
+ FrameCaptureListener() = default;
+
+ /*
+ * Indicates to the canvas whether or not this listener has requested a frame.
+ */
+ virtual bool FrameCaptureRequested(const TimeStamp& aTime) const = 0;
+
+ /*
+ * Interface through which new video frames will be provided while
+ * `mFrameCaptureRequested` is `true`.
+ */
+ virtual void NewFrame(already_AddRefed<layers::Image> aImage,
+ const TimeStamp& aTime) = 0;
+
+ protected:
+ virtual ~FrameCaptureListener() = default;
+};
+
+class HTMLCanvasElement final : public nsGenericHTMLElement,
+ public CanvasRenderingContextHelper,
+ public SupportsWeakPtr {
+ enum { DEFAULT_CANVAS_WIDTH = 300, DEFAULT_CANVAS_HEIGHT = 150 };
+
+ typedef layers::CanvasRenderer CanvasRenderer;
+ typedef layers::LayerManager LayerManager;
+ typedef layers::WebRenderCanvasData WebRenderCanvasData;
+
+ public:
+ explicit HTMLCanvasElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLCanvasElement, canvas)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // CC
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLCanvasElement,
+ nsGenericHTMLElement)
+
+ // WebIDL
+ uint32_t Height() {
+ return GetUnsignedIntAttr(nsGkAtoms::height, DEFAULT_CANVAS_HEIGHT);
+ }
+ uint32_t Width() {
+ return GetUnsignedIntAttr(nsGkAtoms::width, DEFAULT_CANVAS_WIDTH);
+ }
+ void SetHeight(uint32_t aHeight, ErrorResult& aRv);
+ void SetWidth(uint32_t aWidth, ErrorResult& aRv);
+
+ already_AddRefed<nsISupports> GetContext(
+ JSContext* aCx, const nsAString& aContextId,
+ JS::Handle<JS::Value> aContextOptions, ErrorResult& aRv);
+
+ void ToDataURL(JSContext* aCx, const nsAString& aType,
+ JS::Handle<JS::Value> aParams, nsAString& aDataURL,
+ nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv);
+
+ void ToBlob(JSContext* aCx, BlobCallback& aCallback, const nsAString& aType,
+ JS::Handle<JS::Value> aParams, nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aRv);
+
+ OffscreenCanvas* TransferControlToOffscreen(ErrorResult& aRv);
+
+ bool MozOpaque() const { return GetBoolAttr(nsGkAtoms::moz_opaque); }
+ void SetMozOpaque(bool aValue, ErrorResult& aRv) {
+ if (mOffscreenCanvas) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ SetHTMLBoolAttr(nsGkAtoms::moz_opaque, aValue, aRv);
+ }
+ PrintCallback* GetMozPrintCallback() const;
+ void SetMozPrintCallback(PrintCallback* aCallback);
+
+ already_AddRefed<CanvasCaptureMediaStream> CaptureStream(
+ const Optional<double>& aFrameRate, nsIPrincipal& aSubjectPrincipal,
+ ErrorResult& aRv);
+
+ /**
+ * Get the size in pixels of this canvas element
+ */
+ nsIntSize GetSize();
+
+ /**
+ * Set the size in pixels of this canvas element.
+ */
+ void SetSize(const nsIntSize& aSize, ErrorResult& aRv);
+
+ /**
+ * Determine whether the canvas is write-only.
+ */
+ bool IsWriteOnly() const;
+
+ /**
+ * Force the canvas to be write-only, except for readers from
+ * a specific extension's content script expanded principal, if
+ * available.
+ */
+ void SetWriteOnly(nsIPrincipal* aExpandedReader = nullptr);
+
+ /**
+ * Notify the placeholder offscreen canvas of an updated size.
+ */
+ void InvalidateCanvasPlaceholder(uint32_t aWidth, uint32_t aHeight);
+
+ /**
+ * Notify that some canvas content has changed and the window may
+ * need to be updated. aDamageRect is in canvas coordinates.
+ */
+ void InvalidateCanvasContent(const mozilla::gfx::Rect* aDamageRect);
+ /*
+ * Notify that we need to repaint the entire canvas, including updating of
+ * the layer tree.
+ */
+ void InvalidateCanvas();
+
+ nsICanvasRenderingContextInternal* GetCurrentContext() {
+ return mCurrentContext;
+ }
+
+ /*
+ * Returns true if the canvas context content is guaranteed to be opaque
+ * across its entire area.
+ */
+ bool GetIsOpaque();
+ virtual bool GetOpaqueAttr() override;
+
+ /**
+ * Retrieve a snapshot of the internal surface, returning the alpha type if
+ * requested. An optional target may be supplied for which the snapshot will
+ * be optimized for, if possible.
+ */
+ virtual already_AddRefed<gfx::SourceSurface> GetSurfaceSnapshot(
+ gfxAlphaType* aOutAlphaType = nullptr,
+ gfx::DrawTarget* aTarget = nullptr);
+
+ /*
+ * Register a FrameCaptureListener with this canvas.
+ * The canvas hooks into the RefreshDriver while there are
+ * FrameCaptureListeners registered.
+ * The registered FrameCaptureListeners are stored as WeakPtrs, thus it's the
+ * caller's responsibility to keep them alive. Once a registered
+ * FrameCaptureListener is destroyed it will be automatically deregistered.
+ */
+ nsresult RegisterFrameCaptureListener(FrameCaptureListener* aListener,
+ bool aReturnPlaceholderData);
+
+ /*
+ * Returns true when there is at least one registered FrameCaptureListener
+ * that has requested a frame capture.
+ */
+ bool IsFrameCaptureRequested(const TimeStamp& aTime) const;
+
+ /*
+ * Processes destroyed FrameCaptureListeners and removes them if necessary.
+ * Should there be none left, the FrameRefreshObserver will be unregistered.
+ */
+ void ProcessDestroyedFrameListeners();
+
+ /*
+ * Called by the RefreshDriver hook when a frame has been captured.
+ * Makes a copy of the provided surface and hands it to all
+ * FrameCaptureListeners having requested frame capture.
+ */
+ void SetFrameCapture(already_AddRefed<gfx::SourceSurface> aSurface,
+ const TimeStamp& aTime);
+
+ virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+ nsresult CopyInnerTo(HTMLCanvasElement* aDest);
+
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+
+ /*
+ * Helpers called by various users of Canvas
+ */
+
+ already_AddRefed<layers::Image> GetAsImage();
+ bool UpdateWebRenderCanvasData(nsDisplayListBuilder* aBuilder,
+ WebRenderCanvasData* aCanvasData);
+ bool InitializeCanvasRenderer(nsDisplayListBuilder* aBuilder,
+ CanvasRenderer* aRenderer);
+
+ // Call this whenever we need future changes to the canvas
+ // to trigger fresh invalidation requests. This needs to be called
+ // whenever we render the canvas contents to the screen, or whenever we
+ // take a snapshot of the canvas that needs to be "live" (e.g. -moz-element).
+ void MarkContextClean();
+
+ // Call this after capturing a frame, so we can avoid unnecessary surface
+ // copies for future frames when no drawing has occurred.
+ void MarkContextCleanForFrameCapture();
+
+ // Returns non-null when the current context supports captureStream().
+ // The FrameCaptureState gets set to DIRTY when something is drawn.
+ Watchable<FrameCaptureState>* GetFrameCaptureState();
+
+ nsresult GetContext(const nsAString& aContextId, nsISupports** aContext);
+
+ layers::LayersBackend GetCompositorBackendType() const;
+
+ void OnMemoryPressure();
+ void OnDeviceReset();
+
+ already_AddRefed<layers::SharedSurfaceTextureClient> GetVRFrame();
+ void ClearVRFrame();
+
+ bool MaybeModified() const { return mMaybeModified; };
+
+ protected:
+ virtual ~HTMLCanvasElement();
+ void Destroy();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual nsIntSize GetWidthHeight() override;
+
+ virtual already_AddRefed<nsICanvasRenderingContextInternal> CreateContext(
+ CanvasContextType aContextType) override;
+
+ nsresult UpdateContext(JSContext* aCx,
+ JS::Handle<JS::Value> aNewContextOptions,
+ ErrorResult& aRvForDictionaryInit) override;
+
+ nsresult ExtractData(JSContext* aCx, nsIPrincipal& aSubjectPrincipal,
+ nsAString& aType, const nsAString& aOptions,
+ nsIInputStream** aStream);
+ nsresult ToDataURLImpl(JSContext* aCx, nsIPrincipal& aSubjectPrincipal,
+ const nsAString& aMimeType,
+ const JS::Value& aEncoderOptions, nsAString& aDataURL);
+
+ UniquePtr<uint8_t[]> GetImageBuffer(int32_t* aOutFormat,
+ gfx::IntSize* aOutImageSize) override;
+
+ MOZ_CAN_RUN_SCRIPT void CallPrintCallback();
+
+ virtual void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) override;
+ virtual void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) override;
+
+ public:
+ ClientWebGLContext* GetWebGLContext();
+ webgpu::CanvasContext* GetWebGPUContext();
+
+ bool IsOffscreen() const { return !!mOffscreenCanvas; }
+ OffscreenCanvas* GetOffscreenCanvas() const { return mOffscreenCanvas; }
+ void FlushOffscreenCanvas();
+
+ layers::ImageContainer* GetImageContainer() const { return mImageContainer; }
+
+ protected:
+ bool mResetLayer;
+ bool mMaybeModified; // we fetched the context, so we may have written to the
+ // canvas
+ RefPtr<HTMLCanvasElement> mOriginalCanvas;
+ RefPtr<PrintCallback> mPrintCallback;
+ RefPtr<HTMLCanvasPrintState> mPrintState;
+ nsTArray<WeakPtr<FrameCaptureListener>> mRequestedFrameListeners;
+ RefPtr<RequestedFrameRefreshObserver> mRequestedFrameRefreshObserver;
+ RefPtr<OffscreenCanvas> mOffscreenCanvas;
+ RefPtr<OffscreenCanvasDisplayHelper> mOffscreenDisplay;
+ RefPtr<layers::ImageContainer> mImageContainer;
+ RefPtr<HTMLCanvasElementObserver> mContextObserver;
+
+ public:
+ // Record whether this canvas should be write-only or not.
+ // We set this when script paints an image from a different origin.
+ // We also transitively set it when script paints a canvas which
+ // is itself write-only.
+ bool mWriteOnly;
+
+ // When this canvas is (only) tainted by an image from an extension
+ // content script, allow reads from the same extension afterwards.
+ RefPtr<nsIPrincipal> mExpandedReader;
+
+ // Determines if the caller should be able to read the content.
+ bool CallerCanRead(nsIPrincipal& aPrincipal) const;
+
+ bool IsPrintCallbackDone();
+
+ void HandlePrintCallback(nsPresContext*);
+
+ nsresult DispatchPrintCallback(nsITimerCallback* aCallback);
+
+ void ResetPrintCallback();
+
+ HTMLCanvasElement* GetOriginalCanvas();
+
+ CanvasContextType GetCurrentContextType();
+
+ private:
+ /**
+ * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
+ * This function will be called by AfterSetAttr whether the attribute is being
+ * set or unset.
+ *
+ * @param aNamespaceID the namespace of the attr being set
+ * @param aName the localname of the attribute being set
+ * @param aNotify Whether we plan to notify document observers.
+ */
+ void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify);
+};
+
+class HTMLCanvasPrintState final : public nsWrapperCache {
+ public:
+ HTMLCanvasPrintState(HTMLCanvasElement* aCanvas,
+ nsICanvasRenderingContextInternal* aContext,
+ nsITimerCallback* aCallback);
+
+ nsISupports* Context() const;
+
+ void Done();
+
+ void NotifyDone();
+
+ bool mIsDone;
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(HTMLCanvasPrintState)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(HTMLCanvasPrintState)
+
+ virtual JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ HTMLCanvasElement* GetParentObject() { return mCanvas; }
+
+ private:
+ ~HTMLCanvasPrintState();
+ bool mPendingNotify;
+
+ protected:
+ RefPtr<HTMLCanvasElement> mCanvas;
+ nsCOMPtr<nsICanvasRenderingContextInternal> mContext;
+ nsCOMPtr<nsITimerCallback> mCallback;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_HTMLCanvasElement_h */
diff --git a/dom/html/HTMLDNSPrefetch.cpp b/dom/html/HTMLDNSPrefetch.cpp
new file mode 100644
index 0000000000..a4043195fe
--- /dev/null
+++ b/dom/html/HTMLDNSPrefetch.cpp
@@ -0,0 +1,647 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLDNSPrefetch.h"
+
+#include "base/basictypes.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLLinkElement.h"
+#include "mozilla/dom/HTMLAnchorElement.h"
+#include "mozilla/net/NeckoCommon.h"
+#include "mozilla/net/NeckoChild.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "nsURLHelper.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+#include "nsNetUtil.h"
+#include "nsNetCID.h"
+#include "nsIProtocolHandler.h"
+
+#include "nsIDNSListener.h"
+#include "nsIWebProgressListener.h"
+#include "nsIWebProgress.h"
+#include "nsIDNSRecord.h"
+#include "nsIDNSService.h"
+#include "nsICancelable.h"
+#include "nsGkAtoms.h"
+#include "mozilla/dom/Document.h"
+#include "nsThreadUtils.h"
+#include "nsITimer.h"
+#include "nsIObserverService.h"
+
+#include "mozilla/Components.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_network.h"
+
+using namespace mozilla::net;
+
+namespace mozilla::dom {
+
+class NoOpDNSListener final : public nsIDNSListener {
+ // This class exists to give a safe callback no-op DNSListener
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIDNSLISTENER
+
+ NoOpDNSListener() = default;
+
+ private:
+ ~NoOpDNSListener() = default;
+};
+
+NS_IMPL_ISUPPORTS(NoOpDNSListener, nsIDNSListener)
+
+NS_IMETHODIMP
+NoOpDNSListener::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec,
+ nsresult status) {
+ return NS_OK;
+}
+
+// This is just a (size) optimization and could be avoided by storing the
+// SupportsDNSPrefetch pointer of the element in the prefetch queue, but given
+// we need this for GetURIForDNSPrefetch...
+static SupportsDNSPrefetch& ToSupportsDNSPrefetch(Element& aElement) {
+ if (auto* link = HTMLLinkElement::FromNode(aElement)) {
+ return *link;
+ }
+ auto* anchor = HTMLAnchorElement::FromNode(aElement);
+ MOZ_DIAGNOSTIC_ASSERT(anchor);
+ return *anchor;
+}
+
+nsIURI* SupportsDNSPrefetch::GetURIForDNSPrefetch(Element& aElement) {
+ MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == this);
+ if (auto* link = HTMLLinkElement::FromNode(aElement)) {
+ return link->GetURI();
+ }
+ auto* anchor = HTMLAnchorElement::FromNode(aElement);
+ MOZ_DIAGNOSTIC_ASSERT(anchor);
+ return anchor->GetURI();
+}
+
+class DeferredDNSPrefetches final : public nsIWebProgressListener,
+ public nsSupportsWeakReference,
+ public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIOBSERVER
+
+ DeferredDNSPrefetches();
+
+ void Activate();
+ nsresult Add(nsIDNSService::DNSFlags flags, SupportsDNSPrefetch&, Element&);
+
+ void RemoveUnboundLinks();
+
+ private:
+ ~DeferredDNSPrefetches();
+ void Flush();
+
+ void SubmitQueue();
+ void SubmitQueueEntry(Element&, nsIDNSService::DNSFlags aFlags);
+
+ uint16_t mHead;
+ uint16_t mTail;
+ uint32_t mActiveLoaderCount;
+
+ nsCOMPtr<nsITimer> mTimer;
+ bool mTimerArmed;
+ static void Tick(nsITimer* aTimer, void* aClosure);
+
+ static const int sMaxDeferred = 512; // keep power of 2 for masking
+ static const int sMaxDeferredMask = (sMaxDeferred - 1);
+
+ struct deferred_entry {
+ nsIDNSService::DNSFlags mFlags;
+ // SupportsDNSPrefetch clears this raw pointer in Destroyed().
+ Element* mElement;
+ } mEntries[sMaxDeferred];
+};
+
+static NS_DEFINE_CID(kDNSServiceCID, NS_DNSSERVICE_CID);
+static bool sInitialized = false;
+static nsIDNSService* sDNSService = nullptr;
+static DeferredDNSPrefetches* sPrefetches = nullptr;
+static NoOpDNSListener* sDNSListener = nullptr;
+
+nsresult HTMLDNSPrefetch::Initialize() {
+ if (sInitialized) {
+ NS_WARNING("Initialize() called twice");
+ return NS_OK;
+ }
+
+ sPrefetches = new DeferredDNSPrefetches();
+ NS_ADDREF(sPrefetches);
+
+ sDNSListener = new NoOpDNSListener();
+ NS_ADDREF(sDNSListener);
+
+ sPrefetches->Activate();
+
+ if (IsNeckoChild()) NeckoChild::InitNeckoChild();
+
+ sInitialized = true;
+ return NS_OK;
+}
+
+nsresult HTMLDNSPrefetch::Shutdown() {
+ if (!sInitialized) {
+ NS_WARNING("Not Initialized");
+ return NS_OK;
+ }
+ sInitialized = false;
+ NS_IF_RELEASE(sDNSService);
+ NS_IF_RELEASE(sPrefetches);
+ NS_IF_RELEASE(sDNSListener);
+
+ return NS_OK;
+}
+
+static bool EnsureDNSService() {
+ if (sDNSService) {
+ return true;
+ }
+
+ NS_IF_RELEASE(sDNSService);
+ nsresult rv;
+ rv = CallGetService(kDNSServiceCID, &sDNSService);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ return !!sDNSService;
+}
+
+bool HTMLDNSPrefetch::IsAllowed(Document* aDocument) {
+ // There is no need to do prefetch on non UI scenarios such as XMLHttpRequest.
+ return aDocument->IsDNSPrefetchAllowed() && aDocument->GetWindow();
+}
+
+static nsIDNSService::DNSFlags GetDNSFlagsFromElement(Element& aElement) {
+ nsIChannel* channel = aElement.OwnerDoc()->GetChannel();
+ if (!channel) {
+ return nsIDNSService::RESOLVE_DEFAULT_FLAGS;
+ }
+ return nsIDNSService::GetFlagsFromTRRMode(channel->GetTRRMode());
+}
+
+nsIDNSService::DNSFlags HTMLDNSPrefetch::PriorityToDNSServiceFlags(
+ Priority aPriority) {
+ switch (aPriority) {
+ case Priority::Low:
+ return nsIDNSService::RESOLVE_PRIORITY_LOW;
+ case Priority::Medium:
+ return nsIDNSService::RESOLVE_PRIORITY_MEDIUM;
+ case Priority::High:
+ return nsIDNSService::RESOLVE_DEFAULT_FLAGS;
+ }
+ MOZ_ASSERT_UNREACHABLE("Unknown priority");
+ return nsIDNSService::RESOLVE_DEFAULT_FLAGS;
+}
+
+nsresult HTMLDNSPrefetch::Prefetch(SupportsDNSPrefetch& aSupports,
+ Element& aElement, Priority aPriority) {
+ MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == &aSupports);
+ if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return sPrefetches->Add(
+ GetDNSFlagsFromElement(aElement) | PriorityToDNSServiceFlags(aPriority),
+ aSupports, aElement);
+}
+
+nsresult HTMLDNSPrefetch::Prefetch(
+ const nsAString& hostname, bool isHttps,
+ const OriginAttributes& aPartitionedPrincipalOriginAttributes,
+ nsIDNSService::DNSFlags flags) {
+ if (IsNeckoChild()) {
+ // We need to check IsEmpty() because net_IsValidHostName()
+ // considers empty strings to be valid hostnames
+ if (!hostname.IsEmpty() &&
+ net_IsValidHostName(NS_ConvertUTF16toUTF8(hostname))) {
+ // during shutdown gNeckoChild might be null
+ if (gNeckoChild) {
+ gNeckoChild->SendHTMLDNSPrefetch(
+ hostname, isHttps, aPartitionedPrincipalOriginAttributes, flags);
+ }
+ }
+ return NS_OK;
+ }
+
+ if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService())
+ return NS_ERROR_NOT_AVAILABLE;
+
+ nsCOMPtr<nsICancelable> tmpOutstanding;
+ nsresult rv = sDNSService->AsyncResolveNative(
+ NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_DEFAULT,
+ flags | nsIDNSService::RESOLVE_SPECULATE, nullptr, sDNSListener, nullptr,
+ aPartitionedPrincipalOriginAttributes, getter_AddRefs(tmpOutstanding));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (StaticPrefs::network_dns_upgrade_with_https_rr() ||
+ StaticPrefs::network_dns_use_https_rr_as_altsvc()) {
+ Unused << sDNSService->AsyncResolveNative(
+ NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_HTTPSSVC,
+ flags | nsIDNSService::RESOLVE_SPECULATE, nullptr, sDNSListener,
+ nullptr, aPartitionedPrincipalOriginAttributes,
+ getter_AddRefs(tmpOutstanding));
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLDNSPrefetch::Prefetch(
+ const nsAString& hostname, bool isHttps,
+ const OriginAttributes& aPartitionedPrincipalOriginAttributes,
+ nsIRequest::TRRMode aMode, Priority aPriority) {
+ return Prefetch(hostname, isHttps, aPartitionedPrincipalOriginAttributes,
+ nsIDNSService::GetFlagsFromTRRMode(aMode) |
+ PriorityToDNSServiceFlags(aPriority));
+}
+
+nsresult HTMLDNSPrefetch::CancelPrefetch(SupportsDNSPrefetch& aSupports,
+ Element& aElement, Priority aPriority,
+ nsresult aReason) {
+ MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == &aSupports);
+
+ if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsIDNSService::DNSFlags flags =
+ GetDNSFlagsFromElement(aElement) | PriorityToDNSServiceFlags(aPriority);
+
+ nsIURI* uri = aSupports.GetURIForDNSPrefetch(aElement);
+ if (!uri) {
+ return NS_OK;
+ }
+
+ nsAutoCString hostname;
+ uri->GetAsciiHost(hostname);
+
+ nsAutoString protocol;
+ bool isHttps = uri->SchemeIs("https");
+
+ OriginAttributes oa;
+ StoragePrincipalHelper::GetOriginAttributesForNetworkState(
+ aElement.OwnerDoc(), oa);
+
+ return CancelPrefetch(NS_ConvertUTF8toUTF16(hostname), isHttps, oa, flags,
+ aReason);
+}
+
+nsresult HTMLDNSPrefetch::CancelPrefetch(
+ const nsAString& hostname, bool isHttps,
+ const OriginAttributes& aPartitionedPrincipalOriginAttributes,
+ nsIDNSService::DNSFlags flags, nsresult aReason) {
+ // Forward this request to Necko Parent if we're a child process
+ if (IsNeckoChild()) {
+ // We need to check IsEmpty() because net_IsValidHostName()
+ // considers empty strings to be valid hostnames
+ if (!hostname.IsEmpty() &&
+ net_IsValidHostName(NS_ConvertUTF16toUTF8(hostname))) {
+ // during shutdown gNeckoChild might be null
+ if (gNeckoChild) {
+ gNeckoChild->SendCancelHTMLDNSPrefetch(
+ hostname, isHttps, aPartitionedPrincipalOriginAttributes, flags,
+ aReason);
+ }
+ }
+ return NS_OK;
+ }
+
+ if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Forward cancellation to DNS service
+ nsresult rv = sDNSService->CancelAsyncResolveNative(
+ NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_DEFAULT,
+ flags | nsIDNSService::RESOLVE_SPECULATE,
+ nullptr, // AdditionalInfo
+ sDNSListener, aReason, aPartitionedPrincipalOriginAttributes);
+
+ if (StaticPrefs::network_dns_upgrade_with_https_rr() ||
+ StaticPrefs::network_dns_use_https_rr_as_altsvc()) {
+ Unused << sDNSService->CancelAsyncResolveNative(
+ NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_HTTPSSVC,
+ flags | nsIDNSService::RESOLVE_SPECULATE,
+ nullptr, // AdditionalInfo
+ sDNSListener, aReason, aPartitionedPrincipalOriginAttributes);
+ }
+ return rv;
+}
+
+nsresult HTMLDNSPrefetch::CancelPrefetch(
+ const nsAString& hostname, bool isHttps,
+ const OriginAttributes& aPartitionedPrincipalOriginAttributes,
+ nsIRequest::TRRMode aTRRMode, Priority aPriority, nsresult aReason) {
+ return CancelPrefetch(hostname, isHttps,
+ aPartitionedPrincipalOriginAttributes,
+ nsIDNSService::GetFlagsFromTRRMode(aTRRMode) |
+ PriorityToDNSServiceFlags(aPriority),
+ aReason);
+}
+
+void HTMLDNSPrefetch::ElementDestroyed(Element& aElement,
+ SupportsDNSPrefetch& aSupports) {
+ MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == &aSupports);
+ MOZ_ASSERT(aSupports.IsInDNSPrefetch());
+ if (sPrefetches) {
+ // Clean up all the possible links at once.
+ sPrefetches->RemoveUnboundLinks();
+ }
+}
+
+void SupportsDNSPrefetch::TryDNSPrefetch(Element& aOwner) {
+ MOZ_ASSERT(aOwner.IsInComposedDoc());
+ if (HTMLDNSPrefetch::IsAllowed(aOwner.OwnerDoc())) {
+ HTMLDNSPrefetch::Prefetch(*this, aOwner, HTMLDNSPrefetch::Priority::Low);
+ }
+}
+
+void SupportsDNSPrefetch::CancelDNSPrefetch(Element& aOwner) {
+ // If prefetch was deferred, clear flag and move on
+ if (mDNSPrefetchDeferred) {
+ mDNSPrefetchDeferred = false;
+ // Else if prefetch was requested, clear flag and send cancellation
+ } else if (mDNSPrefetchRequested) {
+ mDNSPrefetchRequested = false;
+ // Possible that hostname could have changed since binding, but since this
+ // covers common cases, most DNS prefetch requests will be canceled
+ HTMLDNSPrefetch::CancelPrefetch(
+ *this, aOwner, HTMLDNSPrefetch::Priority::Low, NS_ERROR_ABORT);
+ }
+}
+
+DeferredDNSPrefetches::DeferredDNSPrefetches()
+ : mHead(0), mTail(0), mActiveLoaderCount(0), mTimerArmed(false) {
+ mTimer = NS_NewTimer();
+}
+
+DeferredDNSPrefetches::~DeferredDNSPrefetches() {
+ if (mTimerArmed) {
+ mTimerArmed = false;
+ mTimer->Cancel();
+ }
+
+ Flush();
+}
+
+NS_IMPL_ISUPPORTS(DeferredDNSPrefetches, nsIWebProgressListener,
+ nsISupportsWeakReference, nsIObserver)
+
+void DeferredDNSPrefetches::Flush() {
+ for (; mHead != mTail; mTail = (mTail + 1) & sMaxDeferredMask) {
+ Element* element = mEntries[mTail].mElement;
+ if (element) {
+ ToSupportsDNSPrefetch(*element).ClearIsInDNSPrefetch();
+ }
+ mEntries[mTail].mElement = nullptr;
+ }
+}
+
+nsresult DeferredDNSPrefetches::Add(nsIDNSService::DNSFlags flags,
+ SupportsDNSPrefetch& aSupports,
+ Element& aElement) {
+ // The FIFO has no lock, so it can only be accessed on main thread
+ NS_ASSERTION(NS_IsMainThread(),
+ "DeferredDNSPrefetches::Add must be on main thread");
+
+ aSupports.DNSPrefetchRequestDeferred();
+
+ if (((mHead + 1) & sMaxDeferredMask) == mTail) {
+ return NS_ERROR_DNS_LOOKUP_QUEUE_FULL;
+ }
+
+ aSupports.SetIsInDNSPrefetch();
+ mEntries[mHead].mFlags = flags;
+ mEntries[mHead].mElement = &aElement;
+ mHead = (mHead + 1) & sMaxDeferredMask;
+
+ if (!mActiveLoaderCount && !mTimerArmed && mTimer) {
+ mTimerArmed = true;
+ mTimer->InitWithNamedFuncCallback(
+ Tick, this, 2000, nsITimer::TYPE_ONE_SHOT,
+ "HTMLDNSPrefetch::DeferredDNSPrefetches::Tick");
+ }
+
+ return NS_OK;
+}
+
+void DeferredDNSPrefetches::SubmitQueue() {
+ NS_ASSERTION(NS_IsMainThread(),
+ "DeferredDNSPrefetches::SubmitQueue must be on main thread");
+ if (!EnsureDNSService()) {
+ return;
+ }
+
+ for (; mHead != mTail; mTail = (mTail + 1) & sMaxDeferredMask) {
+ Element* element = mEntries[mTail].mElement;
+ if (!element) {
+ continue;
+ }
+ SubmitQueueEntry(*element, mEntries[mTail].mFlags);
+ mEntries[mTail].mElement = nullptr;
+ }
+
+ if (mTimerArmed) {
+ mTimerArmed = false;
+ mTimer->Cancel();
+ }
+}
+
+void DeferredDNSPrefetches::SubmitQueueEntry(Element& aElement,
+ nsIDNSService::DNSFlags aFlags) {
+ auto& supports = ToSupportsDNSPrefetch(aElement);
+ supports.ClearIsInDNSPrefetch();
+
+ // Only prefetch here if request was deferred and deferral not cancelled
+ if (!supports.IsDNSPrefetchRequestDeferred()) {
+ return;
+ }
+
+ nsIURI* uri = supports.GetURIForDNSPrefetch(aElement);
+ if (!uri) {
+ return;
+ }
+
+ nsAutoCString hostName;
+ uri->GetAsciiHost(hostName);
+ if (hostName.IsEmpty()) {
+ return;
+ }
+
+ bool isLocalResource = false;
+ nsresult rv = NS_URIChainHasFlags(
+ uri, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &isLocalResource);
+ if (NS_FAILED(rv) || isLocalResource) {
+ return;
+ }
+
+ OriginAttributes oa;
+ StoragePrincipalHelper::GetOriginAttributesForNetworkState(
+ aElement.OwnerDoc(), oa);
+
+ bool isHttps = uri->SchemeIs("https");
+
+ if (IsNeckoChild()) {
+ // during shutdown gNeckoChild might be null
+ if (gNeckoChild) {
+ gNeckoChild->SendHTMLDNSPrefetch(NS_ConvertUTF8toUTF16(hostName), isHttps,
+ oa, mEntries[mTail].mFlags);
+ }
+ } else {
+ nsCOMPtr<nsICancelable> tmpOutstanding;
+
+ rv = sDNSService->AsyncResolveNative(
+ hostName, nsIDNSService::RESOLVE_TYPE_DEFAULT,
+ mEntries[mTail].mFlags | nsIDNSService::RESOLVE_SPECULATE, nullptr,
+ sDNSListener, nullptr, oa, getter_AddRefs(tmpOutstanding));
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ // Fetch HTTPS RR if needed.
+ if (StaticPrefs::network_dns_upgrade_with_https_rr() ||
+ StaticPrefs::network_dns_use_https_rr_as_altsvc()) {
+ sDNSService->AsyncResolveNative(
+ hostName, nsIDNSService::RESOLVE_TYPE_HTTPSSVC,
+ mEntries[mTail].mFlags | nsIDNSService::RESOLVE_SPECULATE, nullptr,
+ sDNSListener, nullptr, oa, getter_AddRefs(tmpOutstanding));
+ }
+ }
+
+ // Tell element that deferred prefetch was requested.
+ supports.DNSPrefetchRequestStarted();
+}
+
+void DeferredDNSPrefetches::Activate() {
+ // Register as an observer for the document loader
+ nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service();
+ if (progress)
+ progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT);
+
+ // Register as an observer for xpcom shutdown events so we can drop any
+ // element refs
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService)
+ observerService->AddObserver(this, "xpcom-shutdown", true);
+}
+
+void DeferredDNSPrefetches::RemoveUnboundLinks() {
+ uint16_t tail = mTail;
+ while (mHead != tail) {
+ Element* element = mEntries[tail].mElement;
+ if (element && !element->IsInComposedDoc()) {
+ ToSupportsDNSPrefetch(*element).ClearIsInDNSPrefetch();
+ mEntries[tail].mElement = nullptr;
+ }
+ tail = (tail + 1) & sMaxDeferredMask;
+ }
+}
+
+// nsITimer related method
+
+void DeferredDNSPrefetches::Tick(nsITimer* aTimer, void* aClosure) {
+ auto* self = static_cast<DeferredDNSPrefetches*>(aClosure);
+
+ NS_ASSERTION(NS_IsMainThread(),
+ "DeferredDNSPrefetches::Tick must be on main thread");
+ NS_ASSERTION(self->mTimerArmed, "Timer is not armed");
+
+ self->mTimerArmed = false;
+
+ // If the queue is not submitted here because there are outstanding pages
+ // being loaded, there is no need to rearm the timer as the queue will be
+ // submtited when those loads complete.
+ if (!self->mActiveLoaderCount) {
+ self->SubmitQueue();
+ }
+}
+
+//////////// nsIWebProgressListener methods
+
+NS_IMETHODIMP
+DeferredDNSPrefetches::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t progressStateFlags,
+ nsresult aStatus) {
+ // The FIFO has no lock, so it can only be accessed on main thread
+ NS_ASSERTION(NS_IsMainThread(),
+ "DeferredDNSPrefetches::OnStateChange must be on main thread");
+
+ if (progressStateFlags & STATE_IS_DOCUMENT) {
+ if (progressStateFlags & STATE_STOP) {
+ // Initialization may have missed a STATE_START notification, so do
+ // not go negative
+ if (mActiveLoaderCount) mActiveLoaderCount--;
+
+ if (!mActiveLoaderCount) {
+ SubmitQueue();
+ }
+ } else if (progressStateFlags & STATE_START)
+ mActiveLoaderCount++;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeferredDNSPrefetches::OnProgressChange(nsIWebProgress* aProgress,
+ nsIRequest* aRequest,
+ int32_t curSelfProgress,
+ int32_t maxSelfProgress,
+ int32_t curTotalProgress,
+ int32_t maxTotalProgress) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeferredDNSPrefetches::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsIURI* location,
+ uint32_t aFlags) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeferredDNSPrefetches::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeferredDNSPrefetches::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aState) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DeferredDNSPrefetches::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ return NS_OK;
+}
+
+//////////// nsIObserver method
+
+NS_IMETHODIMP
+DeferredDNSPrefetches::Observe(nsISupports* subject, const char* topic,
+ const char16_t* data) {
+ if (!strcmp(topic, "xpcom-shutdown")) Flush();
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLDNSPrefetch.h b/dom/html/HTMLDNSPrefetch.h
new file mode 100644
index 0000000000..5820a6ecb2
--- /dev/null
+++ b/dom/html/HTMLDNSPrefetch.h
@@ -0,0 +1,147 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLDNSPrefetch_h___
+#define mozilla_dom_HTMLDNSPrefetch_h___
+
+#include "nsCOMPtr.h"
+#include "nsIRequest.h"
+#include "nsString.h"
+#include "nsIDNSService.h"
+
+class nsITimer;
+class nsIURI;
+namespace mozilla {
+
+class OriginAttributes;
+
+namespace net {
+class NeckoParent;
+} // namespace net
+
+namespace dom {
+class Document;
+class Element;
+
+class SupportsDNSPrefetch;
+
+class HTMLDNSPrefetch {
+ public:
+ // The required aDocument parameter is the context requesting the prefetch -
+ // under certain circumstances (e.g. headers, or security context) associated
+ // with the context the prefetch will not be performed.
+ static bool IsAllowed(Document* aDocument);
+
+ static nsresult Initialize();
+ static nsresult Shutdown();
+
+ // Call one of the Prefetch* methods to start the lookup.
+ //
+ // The URI versions will defer DNS lookup until pageload is
+ // complete, while the string versions submit the lookup to
+ // the DNS system immediately. The URI version is somewhat lighter
+ // weight, but its request is also more likely to be dropped due to a
+ // full queue and it may only be used from the main thread.
+ //
+ // If you are planning to use the methods with the OriginAttributes param, be
+ // sure that you pass a partitioned one. See StoragePrincipalHelper.h to know
+ // more.
+
+ enum class Priority {
+ Low,
+ Medium,
+ High,
+ };
+ static nsresult Prefetch(SupportsDNSPrefetch&, Element&, Priority);
+ static nsresult Prefetch(
+ const nsAString& host, bool isHttps,
+ const OriginAttributes& aPartitionedPrincipalOriginAttributes,
+ nsIRequest::TRRMode aTRRMode, Priority);
+ static nsresult CancelPrefetch(
+ const nsAString& host, bool isHttps,
+ const OriginAttributes& aPartitionedPrincipalOriginAttributes,
+ nsIRequest::TRRMode aTRRMode, Priority, nsresult aReason);
+ static nsresult CancelPrefetch(SupportsDNSPrefetch&, Element&, Priority,
+ nsresult aReason);
+ static void ElementDestroyed(Element&, SupportsDNSPrefetch&);
+
+ private:
+ static nsIDNSService::DNSFlags PriorityToDNSServiceFlags(Priority);
+
+ static nsresult Prefetch(
+ const nsAString& host, bool isHttps,
+ const OriginAttributes& aPartitionedPrincipalOriginAttributes,
+ nsIDNSService::DNSFlags flags);
+ static nsresult CancelPrefetch(
+ const nsAString& hostname, bool isHttps,
+ const OriginAttributes& aPartitionedPrincipalOriginAttributes,
+ nsIDNSService::DNSFlags flags, nsresult aReason);
+
+ friend class net::NeckoParent;
+};
+
+// Elements that support DNS prefetch are expected to subclass this.
+class SupportsDNSPrefetch {
+ public:
+ bool IsInDNSPrefetch() { return mInDNSPrefetch; }
+ void SetIsInDNSPrefetch() { mInDNSPrefetch = true; }
+ void ClearIsInDNSPrefetch() { mInDNSPrefetch = false; }
+
+ void DNSPrefetchRequestStarted() {
+ mDNSPrefetchDeferred = false;
+ mDNSPrefetchRequested = true;
+ }
+
+ void DNSPrefetchRequestDeferred() {
+ mDNSPrefetchDeferred = true;
+ mDNSPrefetchRequested = false;
+ }
+
+ bool IsDNSPrefetchRequestDeferred() const { return mDNSPrefetchDeferred; }
+
+ // This could be a virtual function or something like that, but that would
+ // cause our subclasses to grow by two pointers, rather than just 1 byte at
+ // most.
+ nsIURI* GetURIForDNSPrefetch(Element& aOwner);
+
+ protected:
+ SupportsDNSPrefetch()
+ : mInDNSPrefetch(false),
+ mDNSPrefetchRequested(false),
+ mDNSPrefetchDeferred(false),
+ mDestroyedCalled(false) {}
+
+ void CancelDNSPrefetch(Element&);
+ void TryDNSPrefetch(Element&);
+
+ // This MUST be called on the destructor of the Element subclass.
+ // Our own destructor ensures that.
+ void Destroyed(Element& aOwner) {
+ MOZ_DIAGNOSTIC_ASSERT(!mDestroyedCalled,
+ "Multiple calls to SupportsDNSPrefetch::Destroyed?");
+ mDestroyedCalled = true;
+ if (mInDNSPrefetch) {
+ HTMLDNSPrefetch::ElementDestroyed(aOwner, *this);
+ }
+ }
+
+ ~SupportsDNSPrefetch() {
+ MOZ_DIAGNOSTIC_ASSERT(mDestroyedCalled,
+ "Need to call SupportsDNSPrefetch::Destroyed "
+ "from the owner element");
+ }
+
+ private:
+ bool mInDNSPrefetch : 1;
+ bool mDNSPrefetchRequested : 1;
+ bool mDNSPrefetchDeferred : 1;
+ bool mDestroyedCalled : 1;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/html/HTMLDataElement.cpp b/dom/html/HTMLDataElement.cpp
new file mode 100644
index 0000000000..9693990e39
--- /dev/null
+++ b/dom/html/HTMLDataElement.cpp
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLDataElement.h"
+#include "mozilla/dom/HTMLDataElementBinding.h"
+#include "nsGenericHTMLElement.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Data)
+
+namespace mozilla::dom {
+
+HTMLDataElement::HTMLDataElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLDataElement::~HTMLDataElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLDataElement)
+
+JSObject* HTMLDataElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLDataElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLDataElement.h b/dom/html/HTMLDataElement.h
new file mode 100644
index 0000000000..7b5b4b12c7
--- /dev/null
+++ b/dom/html/HTMLDataElement.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLDataElement_h
+#define mozilla_dom_HTMLDataElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+
+namespace mozilla::dom {
+
+class HTMLDataElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLDataElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // HTMLDataElement WebIDL
+ void GetValue(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::value, aValue); }
+
+ void SetValue(const nsAString& aValue, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::value, aValue, aError);
+ }
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLDataElement();
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLDataElement_h
diff --git a/dom/html/HTMLDataListElement.cpp b/dom/html/HTMLDataListElement.cpp
new file mode 100644
index 0000000000..4d89967775
--- /dev/null
+++ b/dom/html/HTMLDataListElement.cpp
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLDataListElement.h"
+#include "mozilla/dom/HTMLDataListElementBinding.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(DataList)
+
+namespace mozilla::dom {
+
+HTMLDataListElement::~HTMLDataListElement() {
+ MOZ_ASSERT(HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR));
+}
+
+JSObject* HTMLDataListElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLDataListElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLDataListElement, nsGenericHTMLElement,
+ mOptions)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLDataListElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLDataListElement)
+
+bool HTMLDataListElement::MatchOptions(Element* aElement, int32_t aNamespaceID,
+ nsAtom* aAtom, void* aData) {
+ return aElement->NodeInfo()->Equals(nsGkAtoms::option, kNameSpaceID_XHTML) &&
+ !aElement->HasAttr(nsGkAtoms::disabled);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLDataListElement.h b/dom/html/HTMLDataListElement.h
new file mode 100644
index 0000000000..a5a3fe4997
--- /dev/null
+++ b/dom/html/HTMLDataListElement.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef HTMLDataListElement_h___
+#define HTMLDataListElement_h___
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsContentList.h"
+
+namespace mozilla::dom {
+
+class HTMLDataListElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLDataListElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetFlags(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR);
+ }
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLDataListElement, datalist)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ nsContentList* Options() {
+ if (!mOptions) {
+ mOptions = new nsContentList(this, MatchOptions, nullptr, nullptr, true);
+ }
+
+ return mOptions;
+ }
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // This function is used to generate the nsContentList (option elements).
+ static bool MatchOptions(Element* aElement, int32_t aNamespaceID,
+ nsAtom* aAtom, void* aData);
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLDataListElement,
+ nsGenericHTMLElement)
+ protected:
+ virtual ~HTMLDataListElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // <option>'s list inside the datalist element.
+ RefPtr<nsContentList> mOptions;
+};
+
+} // namespace mozilla::dom
+
+#endif /* HTMLDataListElement_h___ */
diff --git a/dom/html/HTMLDetailsElement.cpp b/dom/html/HTMLDetailsElement.cpp
new file mode 100644
index 0000000000..c8e9c11c4f
--- /dev/null
+++ b/dom/html/HTMLDetailsElement.cpp
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLDetailsElement.h"
+
+#include "mozilla/dom/HTMLDetailsElementBinding.h"
+#include "mozilla/dom/HTMLSummaryElement.h"
+#include "mozilla/dom/ShadowRoot.h"
+#include "mozilla/ScopeExit.h"
+#include "nsContentUtils.h"
+#include "nsTextNode.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Details)
+
+namespace mozilla::dom {
+
+HTMLDetailsElement::~HTMLDetailsElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLDetailsElement)
+
+HTMLDetailsElement::HTMLDetailsElement(already_AddRefed<NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetupShadowTree();
+}
+
+HTMLSummaryElement* HTMLDetailsElement::GetFirstSummary() const {
+ // XXX: Bug 1245032: Might want to cache the first summary element.
+ for (nsIContent* child = nsINode::GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (auto* summary = HTMLSummaryElement::FromNode(child)) {
+ return summary;
+ }
+ }
+ return nullptr;
+}
+
+void HTMLDetailsElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::open) {
+ bool wasOpen = !!aOldValue;
+ bool isOpen = !!aValue;
+ if (wasOpen != isOpen) {
+ auto stringForState = [](bool aOpen) {
+ return aOpen ? u"open"_ns : u"closed"_ns;
+ };
+ nsAutoString oldState;
+ if (mToggleEventDispatcher) {
+ oldState.Truncate();
+ static_cast<ToggleEvent*>(mToggleEventDispatcher->mEvent.get())
+ ->GetOldState(oldState);
+ mToggleEventDispatcher->Cancel();
+ } else {
+ oldState.Assign(stringForState(wasOpen));
+ }
+ RefPtr<ToggleEvent> toggleEvent = CreateToggleEvent(
+ u"toggle"_ns, oldState, stringForState(isOpen), Cancelable::eNo);
+ mToggleEventDispatcher =
+ new AsyncEventDispatcher(this, toggleEvent.forget());
+ mToggleEventDispatcher->PostDOMEvent();
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void HTMLDetailsElement::SetupShadowTree() {
+ const bool kNotify = false;
+ AttachAndSetUAShadowRoot(NotifyUAWidgetSetup::No);
+ RefPtr<ShadowRoot> sr = GetShadowRoot();
+ if (NS_WARN_IF(!sr)) {
+ return;
+ }
+
+ nsNodeInfoManager* nim = OwnerDoc()->NodeInfoManager();
+ RefPtr<NodeInfo> slotNodeInfo = nim->GetNodeInfo(
+ nsGkAtoms::slot, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+ {
+ RefPtr<NodeInfo> linkNodeInfo = nim->GetNodeInfo(
+ nsGkAtoms::link, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+ RefPtr<nsGenericHTMLElement> link =
+ NS_NewHTMLLinkElement(linkNodeInfo.forget());
+ if (NS_WARN_IF(!link)) {
+ return;
+ }
+ link->SetAttr(nsGkAtoms::rel, u"stylesheet"_ns, IgnoreErrors());
+ link->SetAttr(nsGkAtoms::href,
+ u"resource://content-accessible/details.css"_ns,
+ IgnoreErrors());
+ sr->AppendChildTo(link, kNotify, IgnoreErrors());
+ }
+ {
+ RefPtr<nsGenericHTMLElement> slot =
+ NS_NewHTMLSlotElement(do_AddRef(slotNodeInfo));
+ if (NS_WARN_IF(!slot)) {
+ return;
+ }
+ slot->SetAttr(kNameSpaceID_None, nsGkAtoms::name,
+ u"internal-main-summary"_ns, kNotify);
+ sr->AppendChildTo(slot, kNotify, IgnoreErrors());
+
+ RefPtr<NodeInfo> summaryNodeInfo = nim->GetNodeInfo(
+ nsGkAtoms::summary, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+ RefPtr<nsGenericHTMLElement> summary =
+ NS_NewHTMLSummaryElement(summaryNodeInfo.forget());
+ if (NS_WARN_IF(!summary)) {
+ return;
+ }
+
+ nsAutoString defaultSummaryText;
+ nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "DefaultSummary", defaultSummaryText);
+ RefPtr<nsTextNode> description = new (nim) nsTextNode(nim);
+ description->SetText(defaultSummaryText, kNotify);
+ summary->AppendChildTo(description, kNotify, IgnoreErrors());
+
+ slot->AppendChildTo(summary, kNotify, IgnoreErrors());
+ }
+ {
+ RefPtr<nsGenericHTMLElement> slot =
+ NS_NewHTMLSlotElement(slotNodeInfo.forget());
+ if (NS_WARN_IF(!slot)) {
+ return;
+ }
+ sr->AppendChildTo(slot, kNotify, IgnoreErrors());
+ }
+}
+
+void HTMLDetailsElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) {
+ if (mToggleEventDispatcher == aEvent) {
+ mToggleEventDispatcher = nullptr;
+ }
+}
+
+JSObject* HTMLDetailsElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLDetailsElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void HTMLDetailsElement::HandleInvokeInternal(nsAtom* aAction,
+ ErrorResult& aRv) {
+ if (nsContentUtils::EqualsIgnoreASCIICase(aAction, nsGkAtoms::_auto) ||
+ nsContentUtils::EqualsIgnoreASCIICase(aAction, nsGkAtoms::toggle)) {
+ ToggleOpen();
+ } else if (nsContentUtils::EqualsIgnoreASCIICase(aAction, nsGkAtoms::close)) {
+ if (Open()) {
+ SetOpen(false, IgnoreErrors());
+ }
+ } else if (nsContentUtils::EqualsIgnoreASCIICase(aAction, nsGkAtoms::open)) {
+ if (!Open()) {
+ SetOpen(true, IgnoreErrors());
+ }
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLDetailsElement.h b/dom/html/HTMLDetailsElement.h
new file mode 100644
index 0000000000..2c7ed56d98
--- /dev/null
+++ b/dom/html/HTMLDetailsElement.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLDetailsElement_h
+#define mozilla_dom_HTMLDetailsElement_h
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLSummaryElement;
+
+// HTMLDetailsElement implements the <details> tag, which is used as a
+// disclosure widget from which the user can obtain additional information or
+// controls. Please see the spec for more information.
+// https://html.spec.whatwg.org/multipage/forms.html#the-details-element
+//
+class HTMLDetailsElement final : public nsGenericHTMLElement {
+ public:
+ using NodeInfo = mozilla::dom::NodeInfo;
+
+ explicit HTMLDetailsElement(already_AddRefed<NodeInfo>&& aNodeInfo);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLDetailsElement, details)
+
+ HTMLSummaryElement* GetFirstSummary() const;
+
+ nsresult Clone(NodeInfo* aNodeInfo, nsINode** aResult) const override;
+
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+
+ bool IsInteractiveHTMLContent() const override { return true; }
+
+ // HTMLDetailsElement WebIDL
+ bool Open() const { return GetBoolAttr(nsGkAtoms::open); }
+
+ void SetOpen(bool aOpen, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::open, aOpen, aError);
+ }
+
+ void ToggleOpen() { SetOpen(!Open(), IgnoreErrors()); }
+
+ virtual void AsyncEventRunning(AsyncEventDispatcher* aEvent) override;
+
+ void HandleInvokeInternal(nsAtom* aAction, ErrorResult& aRv) override;
+
+ protected:
+ virtual ~HTMLDetailsElement();
+ void SetupShadowTree();
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ RefPtr<AsyncEventDispatcher> mToggleEventDispatcher;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLDetailsElement_h */
diff --git a/dom/html/HTMLDialogElement.cpp b/dom/html/HTMLDialogElement.cpp
new file mode 100644
index 0000000000..6d4bda0392
--- /dev/null
+++ b/dom/html/HTMLDialogElement.cpp
@@ -0,0 +1,200 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLDialogElement.h"
+#include "mozilla/dom/ElementBinding.h"
+#include "mozilla/dom/HTMLDialogElementBinding.h"
+
+#include "nsContentUtils.h"
+#include "nsFocusManager.h"
+#include "nsIFrame.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Dialog)
+
+namespace mozilla::dom {
+
+HTMLDialogElement::~HTMLDialogElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLDialogElement)
+
+void HTMLDialogElement::Close(
+ const mozilla::dom::Optional<nsAString>& aReturnValue) {
+ if (!Open()) {
+ return;
+ }
+ if (aReturnValue.WasPassed()) {
+ SetReturnValue(aReturnValue.Value());
+ }
+
+ SetOpen(false, IgnoreErrors());
+
+ RemoveFromTopLayerIfNeeded();
+
+ RefPtr<Element> previouslyFocusedElement =
+ do_QueryReferent(mPreviouslyFocusedElement);
+
+ if (previouslyFocusedElement) {
+ mPreviouslyFocusedElement = nullptr;
+
+ FocusOptions options;
+ options.mPreventScroll = true;
+ previouslyFocusedElement->Focus(options, CallerType::NonSystem,
+ IgnoredErrorResult());
+ }
+
+ RefPtr<AsyncEventDispatcher> eventDispatcher =
+ new AsyncEventDispatcher(this, u"close"_ns, CanBubble::eNo);
+ eventDispatcher->PostDOMEvent();
+}
+
+void HTMLDialogElement::Show(ErrorResult& aError) {
+ if (Open()) {
+ if (!IsInTopLayer()) {
+ return;
+ }
+ return aError.ThrowInvalidStateError(
+ "Cannot call show() on an open modal dialog.");
+ }
+
+ SetOpen(true, IgnoreErrors());
+
+ StorePreviouslyFocusedElement();
+
+ RefPtr<nsINode> hideUntil = GetTopmostPopoverAncestor(nullptr, false);
+ if (!hideUntil) {
+ hideUntil = OwnerDoc();
+ }
+
+ OwnerDoc()->HideAllPopoversUntil(*hideUntil, false, true);
+ FocusDialog();
+}
+
+bool HTMLDialogElement::IsInTopLayer() const {
+ return State().HasState(ElementState::MODAL);
+}
+
+void HTMLDialogElement::AddToTopLayerIfNeeded() {
+ MOZ_ASSERT(IsInComposedDoc());
+ if (IsInTopLayer()) {
+ return;
+ }
+
+ OwnerDoc()->AddModalDialog(*this);
+}
+
+void HTMLDialogElement::RemoveFromTopLayerIfNeeded() {
+ if (!IsInTopLayer()) {
+ return;
+ }
+ OwnerDoc()->RemoveModalDialog(*this);
+}
+
+void HTMLDialogElement::StorePreviouslyFocusedElement() {
+ if (Element* element = nsFocusManager::GetFocusedElementStatic()) {
+ if (NS_SUCCEEDED(nsContentUtils::CheckSameOrigin(this, element))) {
+ mPreviouslyFocusedElement = do_GetWeakReference(element);
+ }
+ } else if (Document* doc = GetComposedDoc()) {
+ // Looks like there's a discrepancy sometimes when focus is moved
+ // to a different in-process window.
+ if (nsIContent* unretargetedFocus = doc->GetUnretargetedFocusedContent()) {
+ mPreviouslyFocusedElement = do_GetWeakReference(unretargetedFocus);
+ }
+ }
+}
+
+void HTMLDialogElement::UnbindFromTree(bool aNullParent) {
+ RemoveFromTopLayerIfNeeded();
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+void HTMLDialogElement::ShowModal(ErrorResult& aError) {
+ if (Open()) {
+ if (IsInTopLayer()) {
+ return;
+ }
+ return aError.ThrowInvalidStateError(
+ "Cannot call showModal() on an open non-modal dialog.");
+ }
+
+ if (!IsInComposedDoc()) {
+ return aError.ThrowInvalidStateError("Dialog element is not connected");
+ }
+
+ if (IsPopoverOpen()) {
+ return aError.ThrowInvalidStateError(
+ "Dialog element is already an open popover.");
+ }
+
+ AddToTopLayerIfNeeded();
+
+ SetOpen(true, aError);
+
+ StorePreviouslyFocusedElement();
+
+ RefPtr<nsINode> hideUntil = GetTopmostPopoverAncestor(nullptr, false);
+ if (!hideUntil) {
+ hideUntil = OwnerDoc();
+ }
+
+ OwnerDoc()->HideAllPopoversUntil(*hideUntil, false, true);
+ FocusDialog();
+
+ aError.SuppressException();
+}
+
+void HTMLDialogElement::FocusDialog() {
+ // 1) If subject is inert, return.
+ // 2) Let control be the first descendant element of subject, in tree
+ // order, that is not inert and has the autofocus attribute specified.
+ RefPtr<Document> doc = OwnerDoc();
+ if (IsInComposedDoc()) {
+ doc->FlushPendingNotifications(FlushType::Frames);
+ }
+
+ RefPtr<Element> control = HasAttr(nsGkAtoms::autofocus)
+ ? this
+ : GetFocusDelegate(false /* aWithMouse */);
+
+ // If there isn't one of those either, then let control be subject.
+ if (!control) {
+ control = this;
+ }
+
+ FocusCandidate(control, IsInTopLayer());
+}
+
+int32_t HTMLDialogElement::TabIndexDefault() { return 0; }
+
+void HTMLDialogElement::QueueCancelDialog() {
+ // queues an element task on the user interaction task source
+ OwnerDoc()->Dispatch(
+ NewRunnableMethod("HTMLDialogElement::RunCancelDialogSteps", this,
+ &HTMLDialogElement::RunCancelDialogSteps));
+}
+
+void HTMLDialogElement::RunCancelDialogSteps() {
+ // 1) Let close be the result of firing an event named cancel at dialog, with
+ // the cancelable attribute initialized to true.
+ bool defaultAction = true;
+ nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, u"cancel"_ns,
+ CanBubble::eNo, Cancelable::eYes,
+ &defaultAction);
+
+ // 2) If close is true and dialog has an open attribute, then close the dialog
+ // with no return value.
+ if (defaultAction) {
+ Optional<nsAString> retValue;
+ Close(retValue);
+ }
+}
+
+JSObject* HTMLDialogElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLDialogElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLDialogElement.h b/dom/html/HTMLDialogElement.h
new file mode 100644
index 0000000000..556e86b891
--- /dev/null
+++ b/dom/html/HTMLDialogElement.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLDialogElement_h
+#define HTMLDialogElement_h
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+
+namespace mozilla::dom {
+
+class HTMLDialogElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLDialogElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)),
+ mPreviouslyFocusedElement(nullptr) {}
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLDialogElement, dialog)
+
+ nsresult Clone(dom::NodeInfo* aNodeInfo, nsINode** aResult) const override;
+
+ bool Open() const { return GetBoolAttr(nsGkAtoms::open); }
+ void SetOpen(bool aOpen, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::open, aOpen, aError);
+ }
+
+ void GetReturnValue(nsAString& aReturnValue) { aReturnValue = mReturnValue; }
+ void SetReturnValue(const nsAString& aReturnValue) {
+ mReturnValue = aReturnValue;
+ }
+
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ void Close(const mozilla::dom::Optional<nsAString>& aReturnValue);
+ MOZ_CAN_RUN_SCRIPT void Show(ErrorResult& aError);
+ MOZ_CAN_RUN_SCRIPT void ShowModal(ErrorResult& aError);
+
+ bool IsInTopLayer() const;
+ void QueueCancelDialog();
+ void RunCancelDialogSteps();
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void FocusDialog();
+
+ int32_t TabIndexDefault() override;
+
+ nsString mReturnValue;
+
+ protected:
+ virtual ~HTMLDialogElement();
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ void AddToTopLayerIfNeeded();
+ void RemoveFromTopLayerIfNeeded();
+ void StorePreviouslyFocusedElement();
+
+ nsWeakPtr mPreviouslyFocusedElement;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/html/HTMLDivElement.cpp b/dom/html/HTMLDivElement.cpp
new file mode 100644
index 0000000000..9752dfe50a
--- /dev/null
+++ b/dom/html/HTMLDivElement.cpp
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLDivElement.h"
+#include "nsGenericHTMLElement.h"
+#include "nsStyleConsts.h"
+#include "mozilla/dom/HTMLDivElementBinding.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Div)
+
+namespace mozilla::dom {
+
+HTMLDivElement::~HTMLDivElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLDivElement)
+
+JSObject* HTMLDivElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::HTMLDivElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool HTMLDivElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::align) {
+ return ParseDivAlignValue(aValue, aResult);
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLDivElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapDivAlignAttributeInto(aBuilder);
+ MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLDivElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry* const map[] = {sDivAlignAttributeMap,
+ sCommonAttributeMap};
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLDivElement::GetAttributeMappingFunction() const {
+ return &MapAttributesIntoRule;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLDivElement.h b/dom/html/HTMLDivElement.h
new file mode 100644
index 0000000000..d61b78ce06
--- /dev/null
+++ b/dom/html/HTMLDivElement.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef HTMLDivElement_h___
+#define HTMLDivElement_h___
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLDivElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLDivElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::div),
+ "HTMLDivElement should be a div");
+ }
+
+ void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); }
+ void SetAlign(const nsAString& aAlign, mozilla::ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLDivElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif /* HTMLDivElement_h___ */
diff --git a/dom/html/HTMLElement.cpp b/dom/html/HTMLElement.cpp
new file mode 100644
index 0000000000..ff9c786c55
--- /dev/null
+++ b/dom/html/HTMLElement.cpp
@@ -0,0 +1,467 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLElement.h"
+
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/PresState.h"
+#include "mozilla/dom/CustomElementRegistry.h"
+#include "mozilla/dom/ElementInternalsBinding.h"
+#include "mozilla/dom/FormData.h"
+#include "mozilla/dom/FromParser.h"
+#include "mozilla/dom/HTMLElementBinding.h"
+#include "nsContentUtils.h"
+#include "nsGenericHTMLElement.h"
+#include "nsILayoutHistoryState.h"
+
+namespace mozilla::dom {
+
+HTMLElement::HTMLElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : nsGenericHTMLFormElement(std::move(aNodeInfo)) {
+ if (NodeInfo()->Equals(nsGkAtoms::bdi)) {
+ AddStatesSilently(ElementState::HAS_DIR_ATTR_LIKE_AUTO);
+ }
+
+ InhibitRestoration(!(aFromParser & FROM_PARSER_NETWORK));
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLElement, nsGenericHTMLFormElement)
+
+// QueryInterface implementation for HTMLElement
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLElement)
+ NS_INTERFACE_MAP_ENTRY_TEAROFF(nsIFormControl, GetElementInternals())
+ NS_INTERFACE_MAP_ENTRY_TEAROFF(nsIConstraintValidation, GetElementInternals())
+NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLFormElement)
+
+NS_IMPL_ADDREF_INHERITED(HTMLElement, nsGenericHTMLFormElement)
+NS_IMPL_RELEASE_INHERITED(HTMLElement, nsGenericHTMLFormElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLElement)
+
+JSObject* HTMLElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::HTMLElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void HTMLElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ if (IsDisabledForEvents(aVisitor.mEvent)) {
+ // Do not process any DOM events if the element is disabled
+ aVisitor.mCanHandle = false;
+ return;
+ }
+
+ nsGenericHTMLFormElement::GetEventTargetParent(aVisitor);
+}
+
+nsINode* HTMLElement::GetScopeChainParent() const {
+ if (IsFormAssociatedCustomElements()) {
+ auto* form = GetFormInternal();
+ if (form) {
+ return form;
+ }
+ }
+ return nsGenericHTMLFormElement::GetScopeChainParent();
+}
+
+nsresult HTMLElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLFormElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(false);
+ return rv;
+}
+
+void HTMLElement::UnbindFromTree(bool aNullParent) {
+ nsGenericHTMLFormElement::UnbindFromTree(aNullParent);
+
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(false);
+}
+
+void HTMLElement::DoneCreatingElement() {
+ if (MOZ_UNLIKELY(IsFormAssociatedElement())) {
+ MaybeRestoreFormAssociatedCustomElementState();
+ }
+}
+
+void HTMLElement::SaveState() {
+ if (MOZ_LIKELY(!IsFormAssociatedElement())) {
+ return;
+ }
+
+ auto* internals = GetElementInternals();
+
+ nsCString stateKey = internals->GetStateKey();
+ if (stateKey.IsEmpty()) {
+ return;
+ }
+
+ nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(false);
+ if (!history) {
+ return;
+ }
+
+ // Get the pres state for this key, if it doesn't exist, create one.
+ PresState* result = history->GetState(stateKey);
+ if (!result) {
+ UniquePtr<PresState> newState = NewPresState();
+ result = newState.get();
+ history->AddState(stateKey, std::move(newState));
+ }
+
+ const auto& state = internals->GetFormState();
+ const auto& value = internals->GetFormSubmissionValue();
+ result->contentData() = CustomElementTuple(
+ nsContentUtils::ConvertToCustomElementFormValue(value),
+ nsContentUtils::ConvertToCustomElementFormValue(state));
+}
+
+void HTMLElement::MaybeRestoreFormAssociatedCustomElementState() {
+ MOZ_ASSERT(IsFormAssociatedElement());
+
+ if (HasFlag(HTML_ELEMENT_INHIBIT_RESTORATION)) {
+ return;
+ }
+
+ auto* internals = GetElementInternals();
+ if (internals->GetStateKey().IsEmpty()) {
+ Document* doc = GetUncomposedDoc();
+ nsCString stateKey;
+ nsContentUtils::GenerateStateKey(this, doc, stateKey);
+ internals->SetStateKey(std::move(stateKey));
+
+ RestoreFormAssociatedCustomElementState();
+ }
+}
+
+void HTMLElement::RestoreFormAssociatedCustomElementState() {
+ MOZ_ASSERT(IsFormAssociatedElement());
+
+ auto* internals = GetElementInternals();
+
+ const nsCString& stateKey = internals->GetStateKey();
+ if (stateKey.IsEmpty()) {
+ return;
+ }
+ nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(true);
+ if (!history) {
+ return;
+ }
+ PresState* result = history->GetState(stateKey);
+ if (!result) {
+ return;
+ }
+ auto& content = result->contentData();
+ if (content.type() != PresContentData::TCustomElementTuple) {
+ return;
+ }
+
+ auto& ce = content.get_CustomElementTuple();
+ nsCOMPtr<nsIGlobalObject> global = GetOwnerDocument()->GetOwnerGlobal();
+ internals->RestoreFormValue(
+ nsContentUtils::ExtractFormAssociatedCustomElementValue(global,
+ ce.value()),
+ nsContentUtils::ExtractFormAssociatedCustomElementValue(global,
+ ce.state()));
+}
+
+void HTMLElement::InhibitRestoration(bool aShouldInhibit) {
+ if (aShouldInhibit) {
+ SetFlags(HTML_ELEMENT_INHIBIT_RESTORATION);
+ } else {
+ UnsetFlags(HTML_ELEMENT_INHIBIT_RESTORATION);
+ }
+}
+
+void HTMLElement::SetCustomElementDefinition(
+ CustomElementDefinition* aDefinition) {
+ nsGenericHTMLFormElement::SetCustomElementDefinition(aDefinition);
+ // Always create an ElementInternal for form-associated custom element as the
+ // Form related implementation lives in ElementInternal which implements
+ // nsIFormControl. It is okay for the attachElementInternal API as there is a
+ // separated flag for whether attachElementInternal is called.
+ if (aDefinition && !aDefinition->IsCustomBuiltIn() &&
+ aDefinition->mFormAssociated) {
+ CustomElementData* data = GetCustomElementData();
+ MOZ_ASSERT(data);
+ auto* internals = data->GetOrCreateElementInternals(this);
+
+ // This is for the case that script constructs a custom element directly,
+ // e.g. via new MyCustomElement(), where the upgrade steps won't be ran to
+ // update the disabled state in UpdateFormOwner().
+ if (data->mState == CustomElementData::State::eCustom) {
+ UpdateDisabledState(true);
+ } else if (!HasFlag(HTML_ELEMENT_INHIBIT_RESTORATION)) {
+ internals->InitializeControlNumber();
+ }
+ }
+}
+
+// https://html.spec.whatwg.org/commit-snapshots/53bc3803433e1c817918b83e8a84f3db900031dd/#dom-attachinternals
+already_AddRefed<ElementInternals> HTMLElement::AttachInternals(
+ ErrorResult& aRv) {
+ CustomElementData* ceData = GetCustomElementData();
+
+ // 1. If element's is value is not null, then throw a "NotSupportedError"
+ // DOMException.
+ if (nsAtom* isAtom = ceData ? ceData->GetIs(this) : nullptr) {
+ aRv.ThrowNotSupportedError(nsPrintfCString(
+ "Cannot attach ElementInternals to a customized built-in element "
+ "'%s'",
+ NS_ConvertUTF16toUTF8(isAtom->GetUTF16String()).get()));
+ return nullptr;
+ }
+
+ // 2. Let definition be the result of looking up a custom element definition
+ // given element's node document, its namespace, its local name, and null
+ // as is value.
+ nsAtom* nameAtom = NodeInfo()->NameAtom();
+ CustomElementDefinition* definition = nullptr;
+ if (ceData) {
+ definition = ceData->GetCustomElementDefinition();
+
+ // If the definition is null, the element possible hasn't yet upgraded.
+ // Fallback to use LookupCustomElementDefinition to find its definition.
+ if (!definition) {
+ definition = nsContentUtils::LookupCustomElementDefinition(
+ NodeInfo()->GetDocument(), nameAtom, NodeInfo()->NamespaceID(),
+ ceData->GetCustomElementType());
+ }
+ }
+
+ // 3. If definition is null, then throw an "NotSupportedError" DOMException.
+ if (!definition) {
+ aRv.ThrowNotSupportedError(nsPrintfCString(
+ "Cannot attach ElementInternals to a non-custom element '%s'",
+ NS_ConvertUTF16toUTF8(nameAtom->GetUTF16String()).get()));
+ return nullptr;
+ }
+
+ // 4. If definition's disable internals is true, then throw a
+ // "NotSupportedError" DOMException.
+ if (definition->mDisableInternals) {
+ aRv.ThrowNotSupportedError(nsPrintfCString(
+ "AttachInternal() to '%s' is disabled by disabledFeatures",
+ NS_ConvertUTF16toUTF8(nameAtom->GetUTF16String()).get()));
+ return nullptr;
+ }
+
+ // If this is not a custom element, i.e. ceData is nullptr, we are unable to
+ // find a definition and should return earlier above.
+ MOZ_ASSERT(ceData);
+
+ // 5. If element's attached internals is true, then throw an
+ // "NotSupportedError" DOMException.
+ if (ceData->HasAttachedInternals()) {
+ aRv.ThrowNotSupportedError(nsPrintfCString(
+ "AttachInternals() has already been called from '%s'",
+ NS_ConvertUTF16toUTF8(nameAtom->GetUTF16String()).get()));
+ return nullptr;
+ }
+
+ // 6. If element's custom element state is not "precustomized" or "custom",
+ // then throw a "NotSupportedError" DOMException.
+ if (ceData->mState != CustomElementData::State::ePrecustomized &&
+ ceData->mState != CustomElementData::State::eCustom) {
+ aRv.ThrowNotSupportedError(
+ R"(Custom element state is not "precustomized" or "custom".)");
+ return nullptr;
+ }
+
+ // 7. Set element's attached internals to true.
+ ceData->AttachedInternals();
+
+ // 8. Create a new ElementInternals instance targeting element, and return it.
+ return do_AddRef(ceData->GetOrCreateElementInternals(this));
+}
+
+void HTMLElement::AfterClearForm(bool aUnbindOrDelete) {
+ // No need to enqueue formAssociated callback if we aren't releasing or
+ // unbinding from tree, UpdateFormOwner() will handle it.
+ if (aUnbindOrDelete) {
+ MOZ_ASSERT(IsFormAssociatedElement());
+ nsContentUtils::EnqueueLifecycleCallback(
+ ElementCallbackType::eFormAssociated, this, {});
+ }
+}
+
+void HTMLElement::UpdateFormOwner() {
+ MOZ_ASSERT(IsFormAssociatedElement());
+
+ // If @form is set, the element *has* to be in a composed document,
+ // otherwise it wouldn't be possible to find an element with the
+ // corresponding id. If @form isn't set, the element *has* to have a parent,
+ // otherwise it wouldn't be possible to find a form ancestor. We should not
+ // call UpdateFormOwner if none of these conditions are fulfilled.
+ if (HasAttr(nsGkAtoms::form) ? IsInComposedDoc() : !!GetParent()) {
+ UpdateFormOwner(true, nullptr);
+ }
+ UpdateFieldSet(true);
+ UpdateDisabledState(true);
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(true);
+
+ MaybeRestoreFormAssociatedCustomElementState();
+}
+
+bool HTMLElement::IsDisabledForEvents(WidgetEvent* aEvent) {
+ if (IsFormAssociatedElement()) {
+ return IsElementDisabledForEvents(aEvent, GetPrimaryFrame());
+ }
+
+ return false;
+}
+
+void HTMLElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None &&
+ (aName == nsGkAtoms::disabled || aName == nsGkAtoms::readonly)) {
+ if (aName == nsGkAtoms::disabled) {
+ // This *has* to be called *before* validity state check because
+ // UpdateBarredFromConstraintValidation depend on our disabled state.
+ UpdateDisabledState(aNotify);
+ }
+ if (aName == nsGkAtoms::readonly && !!aValue != !!aOldValue) {
+ UpdateReadOnlyState(aNotify);
+ }
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(aNotify);
+ }
+
+ return nsGenericHTMLFormElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void HTMLElement::UpdateValidityElementStates(bool aNotify) {
+ AutoStateChangeNotifier notifier(*this, aNotify);
+ RemoveStatesSilently(ElementState::VALIDITY_STATES);
+ ElementInternals* internals = GetElementInternals();
+ if (!internals || !internals->IsCandidateForConstraintValidation()) {
+ return;
+ }
+ if (internals->IsValid()) {
+ AddStatesSilently(ElementState::VALID | ElementState::USER_VALID);
+ } else {
+ AddStatesSilently(ElementState::INVALID | ElementState::USER_INVALID);
+ }
+}
+
+void HTMLElement::SetFormInternal(HTMLFormElement* aForm, bool aBindToTree) {
+ ElementInternals* internals = GetElementInternals();
+ MOZ_ASSERT(internals);
+ internals->SetForm(aForm);
+}
+
+HTMLFormElement* HTMLElement::GetFormInternal() const {
+ ElementInternals* internals = GetElementInternals();
+ MOZ_ASSERT(internals);
+ return internals->GetForm();
+}
+
+void HTMLElement::SetFieldSetInternal(HTMLFieldSetElement* aFieldset) {
+ ElementInternals* internals = GetElementInternals();
+ MOZ_ASSERT(internals);
+ internals->SetFieldSet(aFieldset);
+}
+
+HTMLFieldSetElement* HTMLElement::GetFieldSetInternal() const {
+ ElementInternals* internals = GetElementInternals();
+ MOZ_ASSERT(internals);
+ return internals->GetFieldSet();
+}
+
+bool HTMLElement::CanBeDisabled() const { return IsFormAssociatedElement(); }
+
+bool HTMLElement::DoesReadOnlyApply() const {
+ return IsFormAssociatedElement();
+}
+
+void HTMLElement::UpdateDisabledState(bool aNotify) {
+ bool oldState = IsDisabled();
+ nsGenericHTMLFormElement::UpdateDisabledState(aNotify);
+ if (oldState != IsDisabled()) {
+ MOZ_ASSERT(IsFormAssociatedElement());
+ LifecycleCallbackArgs args;
+ args.mDisabled = !oldState;
+ nsContentUtils::EnqueueLifecycleCallback(ElementCallbackType::eFormDisabled,
+ this, args);
+ }
+}
+
+void HTMLElement::UpdateFormOwner(bool aBindToTree, Element* aFormIdElement) {
+ HTMLFormElement* oldForm = GetFormInternal();
+ nsGenericHTMLFormElement::UpdateFormOwner(aBindToTree, aFormIdElement);
+ HTMLFormElement* newForm = GetFormInternal();
+ if (newForm != oldForm) {
+ LifecycleCallbackArgs args;
+ args.mForm = newForm;
+ nsContentUtils::EnqueueLifecycleCallback(
+ ElementCallbackType::eFormAssociated, this, args);
+ }
+}
+
+bool HTMLElement::IsFormAssociatedElement() const {
+ CustomElementData* data = GetCustomElementData();
+ return data && data->IsFormAssociated();
+}
+
+void HTMLElement::FieldSetDisabledChanged(bool aNotify) {
+ // This *has* to be called *before* UpdateBarredFromConstraintValidation
+ // because this function depend on our disabled state.
+ nsGenericHTMLFormElement::FieldSetDisabledChanged(aNotify);
+
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(aNotify);
+}
+
+ElementInternals* HTMLElement::GetElementInternals() const {
+ CustomElementData* data = GetCustomElementData();
+ if (!data || !data->IsFormAssociated()) {
+ // If the element is not a form associated custom element, it should not be
+ // able to be QueryInterfaced to nsIFormControl and could not perform
+ // the form operation, either, so we return nullptr here.
+ return nullptr;
+ }
+
+ return data->GetElementInternals();
+}
+
+void HTMLElement::UpdateBarredFromConstraintValidation() {
+ CustomElementData* data = GetCustomElementData();
+ if (data && data->IsFormAssociated()) {
+ ElementInternals* internals = data->GetElementInternals();
+ MOZ_ASSERT(internals);
+ internals->UpdateBarredFromConstraintValidation();
+ }
+}
+
+} // namespace mozilla::dom
+
+// Here, we expand 'NS_IMPL_NS_NEW_HTML_ELEMENT()' by hand.
+// (Calling the macro directly (with no args) produces compiler warnings.)
+nsGenericHTMLElement* NS_NewHTMLElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
+ auto* nim = nodeInfo->NodeInfoManager();
+ return new (nim) mozilla::dom::HTMLElement(nodeInfo.forget(), aFromParser);
+}
+
+// Distinct from the above in order to have function pointer that compared
+// unequal to a function pointer to the above.
+nsGenericHTMLElement* NS_NewCustomElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
+ auto* nim = nodeInfo->NodeInfoManager();
+ return new (nim) mozilla::dom::HTMLElement(nodeInfo.forget(), aFromParser);
+}
diff --git a/dom/html/HTMLElement.h b/dom/html/HTMLElement.h
new file mode 100644
index 0000000000..8fbbc6f2b9
--- /dev/null
+++ b/dom/html/HTMLElement.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLElement_h
+#define mozilla_dom_HTMLElement_h
+
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLElement final : public nsGenericHTMLFormElement {
+ public:
+ explicit HTMLElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser = NOT_FROM_PARSER);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLElement,
+ nsGenericHTMLFormElement)
+
+ // EventTarget
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+
+ // nsINode
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+ nsINode* GetScopeChainParent() const override;
+
+ // nsIContent
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+ void DoneCreatingElement() override;
+
+ // Element
+ void SetCustomElementDefinition(
+ CustomElementDefinition* aDefinition) override;
+ bool IsLabelable() const override { return IsFormAssociatedElement(); }
+
+ // nsGenericHTMLElement
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#dom-attachinternals
+ already_AddRefed<mozilla::dom::ElementInternals> AttachInternals(
+ ErrorResult& aRv) override;
+ bool IsDisabledForEvents(WidgetEvent* aEvent) override;
+
+ // nsGenericHTMLFormElement
+ bool IsFormAssociatedElement() const override;
+ void AfterClearForm(bool aUnbindOrDelete) override;
+ void FieldSetDisabledChanged(bool aNotify) override;
+ void SaveState() override;
+ void UpdateValidityElementStates(bool aNotify);
+
+ void UpdateFormOwner();
+
+ void MaybeRestoreFormAssociatedCustomElementState();
+
+ void InhibitRestoration(bool aShouldInhibit);
+
+ protected:
+ virtual ~HTMLElement() = default;
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Element
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+
+ // nsGenericHTMLFormElement
+ void SetFormInternal(HTMLFormElement* aForm, bool aBindToTree) override;
+ HTMLFormElement* GetFormInternal() const override;
+ void SetFieldSetInternal(HTMLFieldSetElement* aFieldset) override;
+ HTMLFieldSetElement* GetFieldSetInternal() const override;
+ bool CanBeDisabled() const override;
+ bool DoesReadOnlyApply() const override;
+ void UpdateDisabledState(bool aNotify) override;
+ void UpdateFormOwner(bool aBindToTree, Element* aFormIdElement) override;
+
+ void UpdateBarredFromConstraintValidation();
+
+ ElementInternals* GetElementInternals() const;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void RestoreFormAssociatedCustomElementState();
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLElement_h
diff --git a/dom/html/HTMLEmbedElement.cpp b/dom/html/HTMLEmbedElement.cpp
new file mode 100644
index 0000000000..34d79defbf
--- /dev/null
+++ b/dom/html/HTMLEmbedElement.cpp
@@ -0,0 +1,244 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/HTMLEmbedElement.h"
+#include "mozilla/dom/HTMLEmbedElementBinding.h"
+#include "mozilla/dom/ElementInlines.h"
+
+#include "mozilla/dom/Document.h"
+#include "nsObjectLoadingContent.h"
+#include "nsThreadUtils.h"
+#include "nsIWidget.h"
+#include "nsContentUtils.h"
+#include "nsFrameLoader.h"
+#ifdef XP_MACOSX
+# include "mozilla/EventDispatcher.h"
+# include "mozilla/dom/Event.h"
+#endif
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Embed)
+
+namespace mozilla::dom {
+
+HTMLEmbedElement::HTMLEmbedElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetIsNetworkCreated(aFromParser == FROM_PARSER_NETWORK);
+}
+
+HTMLEmbedElement::~HTMLEmbedElement() = default;
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEmbedElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLEmbedElement,
+ nsGenericHTMLElement)
+ nsObjectLoadingContent::Traverse(tmp, cb);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLEmbedElement,
+ nsGenericHTMLElement)
+ nsObjectLoadingContent::Unlink(tmp);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(
+ HTMLEmbedElement, nsGenericHTMLElement, nsIRequestObserver,
+ nsIStreamListener, nsFrameLoaderOwner, nsIObjectLoadingContent,
+ nsIChannelEventSink)
+
+NS_IMPL_ELEMENT_CLONE(HTMLEmbedElement)
+
+nsresult HTMLEmbedElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsInComposedDoc()) {
+ void (HTMLEmbedElement::*start)() = &HTMLEmbedElement::StartObjectLoad;
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod("dom::HTMLEmbedElement::BindToTree", this, start));
+ }
+
+ return NS_OK;
+}
+
+void HTMLEmbedElement::UnbindFromTree(bool aNullParent) {
+ nsObjectLoadingContent::UnbindFromTree(aNullParent);
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+void HTMLEmbedElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aValue) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+ }
+
+ if (aNamespaceID == kNameSpaceID_None &&
+ aName == nsGkAtoms::allowfullscreen && mFrameLoader) {
+ if (auto* bc = mFrameLoader->GetExtantBrowsingContext()) {
+ MOZ_ALWAYS_SUCCEEDS(bc->SetFullscreenAllowedByOwner(AllowFullscreen()));
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLEmbedElement::OnAttrSetButNotChanged(int32_t aNamespaceID,
+ nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+ return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName,
+ aValue, aNotify);
+}
+
+void HTMLEmbedElement::AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName,
+ bool aNotify) {
+ if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::src) {
+ return;
+ }
+ // If aNotify is false, we are coming from the parser or some such place;
+ // we'll get bound after all the attributes have been set, so we'll do the
+ // object load from BindToTree.
+ // Skip the LoadObject call in that case.
+ // We also don't want to start loading the object when we're not yet in
+ // a document, just in case that the caller wants to set additional
+ // attributes before inserting the node into the document.
+ if (!aNotify || !IsInComposedDoc() || BlockEmbedOrObjectContentLoading()) {
+ return;
+ }
+ nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+ "HTMLEmbedElement::LoadObject",
+ [self = RefPtr<HTMLEmbedElement>(this), aNotify]() {
+ if (self->IsInComposedDoc()) {
+ self->LoadObject(aNotify, true);
+ }
+ }));
+}
+
+int32_t HTMLEmbedElement::TabIndexDefault() {
+ // Only when we loaded a sub-document, <embed> should be tabbable by default
+ // because it's a navigable containers mentioned in 6.6.3 The tabindex
+ // attribute in the standard (see "If the value is null" section).
+ // https://html.spec.whatwg.org/#the-tabindex-attribute
+ // Otherwise, the default tab-index of <embed> is expected as -1 in a WPT:
+ // https://searchfox.org/mozilla-central/rev/7d98e651953f3135d91e98fa6d33efa131aec7ea/testing/web-platform/tests/html/interaction/focus/sequential-focus-navigation-and-the-tabindex-attribute/tabindex-getter.html#63
+ return Type() == ObjectType::Document ? 0 : -1;
+}
+
+bool HTMLEmbedElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ // Has non-plugin content: let the plugin decide what to do in terms of
+ // internal focus from mouse clicks
+ if (aTabIndex) {
+ *aTabIndex = TabIndex();
+ }
+
+ *aIsFocusable = true;
+
+ // Let the plugin decide, so override.
+ return true;
+}
+
+bool HTMLEmbedElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height ||
+ aAttribute == nsGkAtoms::hspace || aAttribute == nsGkAtoms::vspace) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+static void MapAttributesIntoRuleBase(MappedDeclarationsBuilder& aBuilder) {
+ nsGenericHTMLElement::MapImageMarginAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapImageSizeAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapImageAlignAttributeInto(aBuilder);
+}
+
+static void MapAttributesIntoRuleExceptHidden(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapAttributesIntoRuleBase(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesIntoExceptHidden(aBuilder);
+}
+
+void HTMLEmbedElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapAttributesIntoRuleBase(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLEmbedElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry* const map[] = {
+ sCommonAttributeMap,
+ sImageMarginSizeAttributeMap,
+ sImageBorderAttributeMap,
+ sImageAlignAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLEmbedElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRuleExceptHidden;
+}
+
+void HTMLEmbedElement::StartObjectLoad(bool aNotify, bool aForceLoad) {
+ // BindToTree can call us asynchronously, and we may be removed from the tree
+ // in the interim
+ if (!IsInComposedDoc() || !OwnerDoc()->IsActive() ||
+ BlockEmbedOrObjectContentLoading()) {
+ return;
+ }
+
+ LoadObject(aNotify, aForceLoad);
+ SetIsNetworkCreated(false);
+}
+
+uint32_t HTMLEmbedElement::GetCapabilities() const {
+ return eAllowPluginSkipChannel | eSupportImages | eSupportDocuments;
+}
+
+void HTMLEmbedElement::DestroyContent() {
+ nsObjectLoadingContent::Destroy();
+ nsGenericHTMLElement::DestroyContent();
+}
+
+nsresult HTMLEmbedElement::CopyInnerTo(HTMLEmbedElement* aDest) {
+ nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aDest->OwnerDoc()->IsStaticDocument()) {
+ CreateStaticClone(aDest);
+ }
+
+ return rv;
+}
+
+JSObject* HTMLEmbedElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLEmbedElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsContentPolicyType HTMLEmbedElement::GetContentPolicyType() const {
+ return nsIContentPolicy::TYPE_INTERNAL_EMBED;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLEmbedElement.h b/dom/html/HTMLEmbedElement.h
new file mode 100644
index 0000000000..0cb82c0baa
--- /dev/null
+++ b/dom/html/HTMLEmbedElement.h
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLEmbedElement_h
+#define mozilla_dom_HTMLEmbedElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsObjectLoadingContent.h"
+#include "nsGkAtoms.h"
+#include "nsError.h"
+
+namespace mozilla::dom {
+
+class HTMLEmbedElement final : public nsGenericHTMLElement,
+ public nsObjectLoadingContent {
+ public:
+ explicit HTMLEmbedElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLEmbedElement, embed)
+
+ bool AllowFullscreen() const {
+ // We don't need to check prefixed attributes because Flash does not support
+ // them.
+ return IsRewrittenYoutubeEmbed() && GetBoolAttr(nsGkAtoms::allowfullscreen);
+ }
+
+ // nsObjectLoadingContent
+ const Element* AsElement() const final { return this; }
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+
+ int32_t TabIndexDefault() override;
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ void DestroyContent() override;
+
+ // nsObjectLoadingContent
+ uint32_t GetCapabilities() const override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ nsresult CopyInnerTo(HTMLEmbedElement* aDest);
+
+ void StartObjectLoad() { StartObjectLoad(true, false); }
+
+ virtual bool IsInteractiveHTMLContent() const override { return true; }
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLEmbedElement,
+ nsGenericHTMLElement)
+
+ // WebIDL <embed> api
+ void GetAlign(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::align, aValue); }
+ void SetAlign(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::align, aValue, aRv);
+ }
+ void GetHeight(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::height, aValue); }
+ void SetHeight(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::height, aValue, aRv);
+ }
+ void GetName(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); }
+ void SetName(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aValue, aRv);
+ }
+ void GetWidth(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::width, aValue); }
+ void SetWidth(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::width, aValue, aRv);
+ }
+ // WebIDL <embed> api
+ void GetSrc(DOMString& aValue) {
+ GetURIAttr(nsGkAtoms::src, nullptr, aValue);
+ }
+ void SetSrc(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::src, aValue, aRv);
+ }
+ void GetType(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::type, aValue); }
+ void SetType(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::type, aValue, aRv);
+ }
+ Document* GetSVGDocument(nsIPrincipal& aSubjectPrincipal) {
+ return GetContentDocument(aSubjectPrincipal);
+ }
+
+ /**
+ * Calls LoadObject with the correct arguments to start the plugin load.
+ */
+ void StartObjectLoad(bool aNotify, bool aForceLoad);
+
+ protected:
+ void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+ void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) override;
+
+ private:
+ ~HTMLEmbedElement();
+
+ nsContentPolicyType GetContentPolicyType() const override;
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+
+ /**
+ * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
+ * It will not be called if the value is being unset.
+ *
+ * @param aNamespaceID the namespace of the attr being set
+ * @param aName the localname of the attribute being set
+ * @param aNotify Whether we plan to notify document observers.
+ */
+ void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLEmbedElement_h
diff --git a/dom/html/HTMLFieldSetElement.cpp b/dom/html/HTMLFieldSetElement.cpp
new file mode 100644
index 0000000000..828bed9c8d
--- /dev/null
+++ b/dom/html/HTMLFieldSetElement.cpp
@@ -0,0 +1,314 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/BasicEvents.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/CustomElementRegistry.h"
+#include "mozilla/dom/HTMLFieldSetElement.h"
+#include "mozilla/dom/HTMLFieldSetElementBinding.h"
+#include "nsContentList.h"
+#include "nsQueryObject.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(FieldSet)
+
+namespace mozilla::dom {
+
+HTMLFieldSetElement::HTMLFieldSetElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLFormControlElement(std::move(aNodeInfo),
+ FormControlType::Fieldset),
+ mElements(nullptr),
+ mFirstLegend(nullptr),
+ mInvalidElementsCount(0) {
+ // <fieldset> is always barred from constraint validation.
+ SetBarredFromConstraintValidation(true);
+
+ // We start out enabled and valid.
+ AddStatesSilently(ElementState::ENABLED | ElementState::VALID);
+}
+
+HTMLFieldSetElement::~HTMLFieldSetElement() {
+ uint32_t length = mDependentElements.Length();
+ for (uint32_t i = 0; i < length; ++i) {
+ mDependentElements[i]->ForgetFieldSet(this);
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLFieldSetElement,
+ nsGenericHTMLFormControlElement, mValidity,
+ mElements)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLFieldSetElement,
+ nsGenericHTMLFormControlElement,
+ nsIConstraintValidation)
+
+NS_IMPL_ELEMENT_CLONE(HTMLFieldSetElement)
+
+bool HTMLFieldSetElement::IsDisabledForEvents(WidgetEvent* aEvent) {
+ if (StaticPrefs::dom_forms_fieldset_disable_only_descendants_enabled()) {
+ return false;
+ }
+ return IsElementDisabledForEvents(aEvent, nullptr);
+}
+
+// nsIContent
+void HTMLFieldSetElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ // Do not process any DOM events if the element is disabled.
+ aVisitor.mCanHandle = false;
+ if (IsDisabledForEvents(aVisitor.mEvent)) {
+ return;
+ }
+
+ nsGenericHTMLFormControlElement::GetEventTargetParent(aVisitor);
+}
+
+void HTMLFieldSetElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::disabled) {
+ // This *has* to be called *before* calling FieldSetDisabledChanged on our
+ // controls, as they may depend on our disabled state.
+ UpdateDisabledState(aNotify);
+ }
+
+ return nsGenericHTMLFormControlElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLFieldSetElement::GetType(nsAString& aType) const {
+ aType.AssignLiteral("fieldset");
+}
+
+/* static */
+bool HTMLFieldSetElement::MatchListedElements(Element* aElement,
+ int32_t aNamespaceID,
+ nsAtom* aAtom, void* aData) {
+ nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(aElement);
+ return formControl;
+}
+
+nsIHTMLCollection* HTMLFieldSetElement::Elements() {
+ if (!mElements) {
+ mElements =
+ new nsContentList(this, MatchListedElements, nullptr, nullptr, true);
+ }
+
+ return mElements;
+}
+
+// nsIFormControl
+
+nsresult HTMLFieldSetElement::Reset() { return NS_OK; }
+
+void HTMLFieldSetElement::InsertChildBefore(nsIContent* aChild,
+ nsIContent* aBeforeThis,
+ bool aNotify, ErrorResult& aRv) {
+ bool firstLegendHasChanged = false;
+
+ if (aChild->IsHTMLElement(nsGkAtoms::legend)) {
+ if (!mFirstLegend) {
+ mFirstLegend = aChild;
+ // We do not want to notify the first time mFirstElement is set.
+ } else {
+ // If mFirstLegend is before aIndex, we do not change it.
+ // Otherwise, mFirstLegend is now aChild.
+ const Maybe<uint32_t> indexOfRef =
+ aBeforeThis ? ComputeIndexOf(aBeforeThis) : Some(GetChildCount());
+ const Maybe<uint32_t> indexOfFirstLegend = ComputeIndexOf(mFirstLegend);
+ if ((indexOfRef.isSome() && indexOfFirstLegend.isSome() &&
+ *indexOfRef <= *indexOfFirstLegend) ||
+ // XXX Keep the odd traditional behavior for now.
+ indexOfRef.isNothing()) {
+ mFirstLegend = aChild;
+ firstLegendHasChanged = true;
+ }
+ }
+ }
+
+ nsGenericHTMLFormControlElement::InsertChildBefore(aChild, aBeforeThis,
+ aNotify, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (firstLegendHasChanged) {
+ NotifyElementsForFirstLegendChange(aNotify);
+ }
+}
+
+void HTMLFieldSetElement::RemoveChildNode(nsIContent* aKid, bool aNotify) {
+ bool firstLegendHasChanged = false;
+
+ if (mFirstLegend && aKid == mFirstLegend) {
+ // If we are removing the first legend we have to found another one.
+ nsIContent* child = mFirstLegend->GetNextSibling();
+ mFirstLegend = nullptr;
+ firstLegendHasChanged = true;
+
+ for (; child; child = child->GetNextSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::legend)) {
+ mFirstLegend = child;
+ break;
+ }
+ }
+ }
+
+ nsGenericHTMLFormControlElement::RemoveChildNode(aKid, aNotify);
+
+ if (firstLegendHasChanged) {
+ NotifyElementsForFirstLegendChange(aNotify);
+ }
+}
+
+void HTMLFieldSetElement::AddElement(nsGenericHTMLFormElement* aElement) {
+ mDependentElements.AppendElement(aElement);
+
+ // If the element that we are adding aElement is a fieldset, then all the
+ // invalid elements in aElement are also invalid elements of this.
+ HTMLFieldSetElement* fieldSet = FromNode(aElement);
+ if (fieldSet) {
+ for (int32_t i = 0; i < fieldSet->mInvalidElementsCount; i++) {
+ UpdateValidity(false);
+ }
+ return;
+ }
+
+ // If the element is a form-associated custom element, adding element might be
+ // caused by FACE upgrade which won't trigger mutation observer, so mark
+ // mElements dirty manually here.
+ CustomElementData* data = aElement->GetCustomElementData();
+ if (data && data->IsFormAssociated() && mElements) {
+ mElements->SetDirty();
+ }
+
+ // We need to update the validity of the fieldset.
+ nsCOMPtr<nsIConstraintValidation> cvElmt = do_QueryObject(aElement);
+ if (cvElmt && cvElmt->IsCandidateForConstraintValidation() &&
+ !cvElmt->IsValid()) {
+ UpdateValidity(false);
+ }
+
+#if DEBUG
+ int32_t debugInvalidElementsCount = 0;
+ for (uint32_t i = 0; i < mDependentElements.Length(); i++) {
+ HTMLFieldSetElement* fieldSet = FromNode(mDependentElements[i]);
+ if (fieldSet) {
+ debugInvalidElementsCount += fieldSet->mInvalidElementsCount;
+ continue;
+ }
+ nsCOMPtr<nsIConstraintValidation> cvElmt =
+ do_QueryObject(mDependentElements[i]);
+ if (cvElmt && cvElmt->IsCandidateForConstraintValidation() &&
+ !(cvElmt->IsValid())) {
+ debugInvalidElementsCount += 1;
+ }
+ }
+ MOZ_ASSERT(debugInvalidElementsCount == mInvalidElementsCount);
+#endif
+}
+
+void HTMLFieldSetElement::RemoveElement(nsGenericHTMLFormElement* aElement) {
+ mDependentElements.RemoveElement(aElement);
+
+ // If the element that we are removing aElement is a fieldset, then all the
+ // invalid elements in aElement are also removed from this.
+ HTMLFieldSetElement* fieldSet = FromNode(aElement);
+ if (fieldSet) {
+ for (int32_t i = 0; i < fieldSet->mInvalidElementsCount; i++) {
+ UpdateValidity(true);
+ }
+ return;
+ }
+
+ // We need to update the validity of the fieldset.
+ nsCOMPtr<nsIConstraintValidation> cvElmt = do_QueryObject(aElement);
+ if (cvElmt && cvElmt->IsCandidateForConstraintValidation() &&
+ !cvElmt->IsValid()) {
+ UpdateValidity(true);
+ }
+
+#if DEBUG
+ int32_t debugInvalidElementsCount = 0;
+ for (uint32_t i = 0; i < mDependentElements.Length(); i++) {
+ HTMLFieldSetElement* fieldSet = FromNode(mDependentElements[i]);
+ if (fieldSet) {
+ debugInvalidElementsCount += fieldSet->mInvalidElementsCount;
+ continue;
+ }
+ nsCOMPtr<nsIConstraintValidation> cvElmt =
+ do_QueryObject(mDependentElements[i]);
+ if (cvElmt && cvElmt->IsCandidateForConstraintValidation() &&
+ !(cvElmt->IsValid())) {
+ debugInvalidElementsCount += 1;
+ }
+ }
+ MOZ_ASSERT(debugInvalidElementsCount == mInvalidElementsCount);
+#endif
+}
+
+void HTMLFieldSetElement::UpdateDisabledState(bool aNotify) {
+ nsGenericHTMLFormControlElement::UpdateDisabledState(aNotify);
+
+ for (nsGenericHTMLFormElement* element : mDependentElements) {
+ element->FieldSetDisabledChanged(aNotify);
+ }
+}
+
+void HTMLFieldSetElement::NotifyElementsForFirstLegendChange(bool aNotify) {
+ /**
+ * NOTE: this could be optimized if only call when the fieldset is currently
+ * disabled.
+ * This should also make sure that mElements is set when we happen to be here.
+ * However, this method shouldn't be called very often in normal use cases.
+ */
+ if (!mElements) {
+ mElements =
+ new nsContentList(this, MatchListedElements, nullptr, nullptr, true);
+ }
+
+ uint32_t length = mElements->Length(true);
+ for (uint32_t i = 0; i < length; ++i) {
+ static_cast<nsGenericHTMLFormElement*>(mElements->Item(i))
+ ->FieldSetFirstLegendChanged(aNotify);
+ }
+}
+
+void HTMLFieldSetElement::UpdateValidity(bool aElementValidity) {
+ if (aElementValidity) {
+ --mInvalidElementsCount;
+ } else {
+ ++mInvalidElementsCount;
+ }
+
+ MOZ_ASSERT(mInvalidElementsCount >= 0);
+
+ // The fieldset validity has just changed if:
+ // - there are no more invalid elements ;
+ // - or there is one invalid elmement and an element just became invalid.
+ if (!mInvalidElementsCount ||
+ (mInvalidElementsCount == 1 && !aElementValidity)) {
+ AutoStateChangeNotifier notifier(*this, true);
+ RemoveStatesSilently(ElementState::VALID | ElementState::INVALID);
+ AddStatesSilently(mInvalidElementsCount ? ElementState::INVALID
+ : ElementState::VALID);
+ }
+
+ // We should propagate the change to the fieldset parent chain.
+ if (mFieldSet) {
+ mFieldSet->UpdateValidity(aElementValidity);
+ }
+}
+
+JSObject* HTMLFieldSetElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLFieldSetElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLFieldSetElement.h b/dom/html/HTMLFieldSetElement.h
new file mode 100644
index 0000000000..4b592937b2
--- /dev/null
+++ b/dom/html/HTMLFieldSetElement.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLFieldSetElement_h
+#define mozilla_dom_HTMLFieldSetElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/ConstraintValidation.h"
+#include "mozilla/dom/ValidityState.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla {
+class ErrorResult;
+class EventChainPreVisitor;
+namespace dom {
+class FormData;
+
+class HTMLFieldSetElement final : public nsGenericHTMLFormControlElement,
+ public ConstraintValidation {
+ public:
+ using ConstraintValidation::GetValidationMessage;
+ using ConstraintValidation::SetCustomValidity;
+
+ explicit HTMLFieldSetElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLFieldSetElement, fieldset)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsINode
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // nsIContent
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ void InsertChildBefore(nsIContent* aChild, nsIContent* aBeforeThis,
+ bool aNotify, ErrorResult& aRv) override;
+ void RemoveChildNode(nsIContent* aKid, bool aNotify) override;
+
+ // nsGenericHTMLElement
+ bool IsDisabledForEvents(WidgetEvent* aEvent) override;
+
+ // nsIFormControl
+ NS_IMETHOD Reset() override;
+ NS_IMETHOD SubmitNamesValues(FormData* aFormData) override { return NS_OK; }
+
+ const nsIContent* GetFirstLegend() const { return mFirstLegend; }
+
+ void AddElement(nsGenericHTMLFormElement* aElement);
+
+ void RemoveElement(nsGenericHTMLFormElement* aElement);
+
+ // nsGenericHTMLFormElement
+ void UpdateDisabledState(bool aNotify) override;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLFieldSetElement,
+ nsGenericHTMLFormControlElement)
+
+ // WebIDL
+ bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); }
+ void SetDisabled(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::disabled, aValue, aRv);
+ }
+
+ void GetName(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); }
+
+ void SetName(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aValue, aRv);
+ }
+
+ void GetType(nsAString& aType) const;
+
+ nsIHTMLCollection* Elements();
+
+ // XPCOM WillValidate is OK for us
+
+ // XPCOM Validity is OK for us
+
+ // XPCOM GetValidationMessage is OK for us
+
+ // XPCOM CheckValidity is OK for us
+
+ // XPCOM SetCustomValidity is OK for us
+
+ /*
+ * This method will update the fieldset's validity. This method has to be
+ * called by fieldset elements whenever their validity state or status
+ * regarding constraint validation changes.
+ *
+ * @note If an element becomes barred from constraint validation, it has to
+ * be considered as valid.
+ *
+ * @param aElementValidityState the new validity state of the element
+ */
+ void UpdateValidity(bool aElementValidityState);
+
+ protected:
+ virtual ~HTMLFieldSetElement();
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ /**
+ * Notify all elements (in mElements) that the first legend of the fieldset
+ * has now changed.
+ */
+ void NotifyElementsForFirstLegendChange(bool aNotify);
+
+ // This function is used to generate the nsContentList (listed form elements).
+ static bool MatchListedElements(Element* aElement, int32_t aNamespaceID,
+ nsAtom* aAtom, void* aData);
+
+ // listed form controls elements.
+ RefPtr<nsContentList> mElements;
+
+ // List of elements which have this fieldset as first fieldset ancestor.
+ nsTArray<nsGenericHTMLFormElement*> mDependentElements;
+
+ nsIContent* mFirstLegend;
+
+ /**
+ * Number of invalid and candidate for constraint validation
+ * elements in the fieldSet the last time UpdateValidity has been called.
+ *
+ * @note Should only be used by UpdateValidity()
+ */
+ int32_t mInvalidElementsCount;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_HTMLFieldSetElement_h */
diff --git a/dom/html/HTMLFontElement.cpp b/dom/html/HTMLFontElement.cpp
new file mode 100644
index 0000000000..fb57407a87
--- /dev/null
+++ b/dom/html/HTMLFontElement.cpp
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLFontElement.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLFontElementBinding.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsAttrValueInlines.h"
+#include "nsContentUtils.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Font)
+
+namespace mozilla::dom {
+
+HTMLFontElement::~HTMLFontElement() = default;
+
+JSObject* HTMLFontElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLFontElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLFontElement)
+
+bool HTMLFontElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::size) {
+ int32_t size = nsContentUtils::ParseLegacyFontSize(aValue);
+ if (size) {
+ aResult.SetTo(size, &aValue);
+ return true;
+ }
+ return false;
+ }
+ if (aAttribute == nsGkAtoms::color) {
+ return aResult.ParseColor(aValue);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLFontElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ // face: string list
+ if (!aBuilder.PropertyIsSet(eCSSProperty_font_family)) {
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::face);
+ if (value && value->Type() == nsAttrValue::eString &&
+ !value->IsEmptyString()) {
+ aBuilder.SetFontFamily(NS_ConvertUTF16toUTF8(value->GetStringValue()));
+ }
+ }
+ // size: int
+ if (!aBuilder.PropertyIsSet(eCSSProperty_font_size)) {
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::size);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ aBuilder.SetKeywordValue(eCSSProperty_font_size,
+ value->GetIntegerValue());
+ }
+ }
+ if (!aBuilder.PropertyIsSet(eCSSProperty_color)) {
+ // color: color
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::color);
+ nscolor color;
+ if (value && value->GetColorValue(color)) {
+ aBuilder.SetColorValue(eCSSProperty_color, color);
+ }
+ }
+ if (aBuilder.Document().GetCompatibilityMode() == eCompatibility_NavQuirks) {
+ // Make <a><font color="red">text</font></a> give the text a red underline
+ // in quirks mode. The StyleTextDecorationLine_COLOR_OVERRIDE flag only
+ // affects quirks mode rendering.
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::color);
+ nscolor color;
+ if (value && value->GetColorValue(color)) {
+ aBuilder.SetTextDecorationColorOverride();
+ }
+ }
+
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLFontElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::face}, {nsGkAtoms::size}, {nsGkAtoms::color}, {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLFontElement::GetAttributeMappingFunction() const {
+ return &MapAttributesIntoRule;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLFontElement.h b/dom/html/HTMLFontElement.h
new file mode 100644
index 0000000000..ddaa0e8944
--- /dev/null
+++ b/dom/html/HTMLFontElement.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef HTMLFontElement_h___
+#define HTMLFontElement_h___
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLFontElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLFontElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ void GetColor(DOMString& aColor) { GetHTMLAttr(nsGkAtoms::color, aColor); }
+ void SetColor(const nsAString& aColor, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::color, aColor, aError);
+ }
+ void GetFace(DOMString& aFace) { GetHTMLAttr(nsGkAtoms::face, aFace); }
+ void SetFace(const nsAString& aFace, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::face, aFace, aError);
+ }
+ void GetSize(DOMString& aSize) { GetHTMLAttr(nsGkAtoms::size, aSize); }
+ void SetSize(const nsAString& aSize, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::size, aSize, aError);
+ }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLFontElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif /* HTMLFontElement_h___ */
diff --git a/dom/html/HTMLFormControlsCollection.cpp b/dom/html/HTMLFormControlsCollection.cpp
new file mode 100644
index 0000000000..aa10daceeb
--- /dev/null
+++ b/dom/html/HTMLFormControlsCollection.cpp
@@ -0,0 +1,305 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLFormControlsCollection.h"
+
+#include "mozilla/FlushType.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLFormControlsCollectionBinding.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "nsGenericHTMLElement.h" // nsGenericHTMLFormElement
+#include "nsQueryObject.h"
+#include "nsIFormControl.h"
+#include "RadioNodeList.h"
+#include "jsfriendapi.h"
+
+namespace mozilla::dom {
+
+/* static */
+bool HTMLFormControlsCollection::ShouldBeInElements(
+ nsIFormControl* aFormControl) {
+ // For backwards compatibility (with 4.x and IE) we must not add
+ // <input type=image> elements to the list of form controls in a
+ // form.
+
+ switch (aFormControl->ControlType()) {
+ case FormControlType::ButtonButton:
+ case FormControlType::ButtonReset:
+ case FormControlType::ButtonSubmit:
+ case FormControlType::InputButton:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputColor:
+ case FormControlType::InputEmail:
+ case FormControlType::InputFile:
+ case FormControlType::InputHidden:
+ case FormControlType::InputReset:
+ case FormControlType::InputPassword:
+ case FormControlType::InputRadio:
+ case FormControlType::InputSearch:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputText:
+ case FormControlType::InputTel:
+ case FormControlType::InputUrl:
+ case FormControlType::InputNumber:
+ case FormControlType::InputRange:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ case FormControlType::Select:
+ case FormControlType::Textarea:
+ case FormControlType::Fieldset:
+ case FormControlType::Object:
+ case FormControlType::Output:
+ case FormControlType::FormAssociatedCustomElement:
+ return true;
+
+ // These form control types are not supposed to end up in the
+ // form.elements array
+ // XXXbz maybe we should just return aType != InputImage or something
+ // instead of the big switch?
+ case FormControlType::InputImage:
+ break;
+ }
+ return false;
+}
+
+HTMLFormControlsCollection::HTMLFormControlsCollection(HTMLFormElement* aForm)
+ : mForm(aForm),
+ mNameLookupTable(HTMLFormElement::FORM_CONTROL_LIST_HASHTABLE_LENGTH) {}
+
+HTMLFormControlsCollection::~HTMLFormControlsCollection() {
+ mForm = nullptr;
+ Clear();
+}
+
+void HTMLFormControlsCollection::DropFormReference() {
+ mForm = nullptr;
+ Clear();
+}
+
+void HTMLFormControlsCollection::Clear() {
+ // Null out childrens' pointer to me. No refcounting here
+ for (nsGenericHTMLFormElement* element : Reversed(mElements.AsList())) {
+ nsCOMPtr<nsIFormControl> formControl = do_QueryObject(element);
+ MOZ_ASSERT(formControl);
+ formControl->ClearForm(false, false);
+ }
+ mElements.Clear();
+
+ for (nsGenericHTMLFormElement* element : Reversed(mNotInElements.AsList())) {
+ nsCOMPtr<nsIFormControl> formControl = do_QueryObject(element);
+ MOZ_ASSERT(formControl);
+ formControl->ClearForm(false, false);
+ }
+ mNotInElements.Clear();
+
+ mNameLookupTable.Clear();
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLFormControlsCollection)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HTMLFormControlsCollection)
+ // Note: We intentionally don't set tmp->mForm to nullptr here, since doing
+ // so may result in crashes because of inconsistent null-checking after the
+ // object gets unlinked.
+ tmp->Clear();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(HTMLFormControlsCollection)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNameLookupTable)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(HTMLFormControlsCollection)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+// XPConnect interface list for HTMLFormControlsCollection
+NS_INTERFACE_TABLE_HEAD(HTMLFormControlsCollection)
+ NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY
+ NS_INTERFACE_TABLE(HTMLFormControlsCollection, nsIHTMLCollection)
+ NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(HTMLFormControlsCollection)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLFormControlsCollection)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLFormControlsCollection)
+
+// nsIHTMLCollection interfac
+
+uint32_t HTMLFormControlsCollection::Length() { return mElements->Length(); }
+
+nsISupports* HTMLFormControlsCollection::NamedItemInternal(
+ const nsAString& aName) {
+ return mNameLookupTable.GetWeak(aName);
+}
+
+nsresult HTMLFormControlsCollection::AddElementToTable(
+ nsGenericHTMLFormElement* aChild, const nsAString& aName) {
+ nsCOMPtr<nsIFormControl> formControl = do_QueryObject(aChild);
+ MOZ_ASSERT(formControl);
+ if (!ShouldBeInElements(formControl)) {
+ return NS_OK;
+ }
+
+ return mForm->AddElementToTableInternal(mNameLookupTable, aChild, aName);
+}
+
+nsresult HTMLFormControlsCollection::IndexOfContent(nsIContent* aContent,
+ int32_t* aIndex) {
+ // Note -- not a DOM method; callers should handle flushing themselves
+
+ NS_ENSURE_ARG_POINTER(aIndex);
+ *aIndex = mElements->IndexOf(aContent);
+ return NS_OK;
+}
+
+nsresult HTMLFormControlsCollection::RemoveElementFromTable(
+ nsGenericHTMLFormElement* aChild, const nsAString& aName) {
+ nsCOMPtr<nsIFormControl> formControl = do_QueryObject(aChild);
+ MOZ_ASSERT(formControl);
+ if (!ShouldBeInElements(formControl)) {
+ return NS_OK;
+ }
+
+ return mForm->RemoveElementFromTableInternal(mNameLookupTable, aChild, aName);
+}
+
+nsresult HTMLFormControlsCollection::GetSortedControls(
+ nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls) const {
+#ifdef DEBUG
+ HTMLFormElement::AssertDocumentOrder(mElements, mForm);
+ HTMLFormElement::AssertDocumentOrder(mNotInElements, mForm);
+#endif
+
+ aControls.Clear();
+
+ // Merge the elements list and the not in elements list. Both lists are
+ // already sorted.
+ uint32_t elementsLen = mElements->Length();
+ uint32_t notInElementsLen = mNotInElements->Length();
+ aControls.SetCapacity(elementsLen + notInElementsLen);
+
+ uint32_t elementsIdx = 0;
+ uint32_t notInElementsIdx = 0;
+
+ while (elementsIdx < elementsLen || notInElementsIdx < notInElementsLen) {
+ // Check whether we're done with mElements
+ if (elementsIdx == elementsLen) {
+ NS_ASSERTION(notInElementsIdx < notInElementsLen,
+ "Should have remaining not-in-elements");
+ // Append the remaining mNotInElements elements
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ aControls.AppendElements(mNotInElements->Elements() + notInElementsIdx,
+ notInElementsLen - notInElementsIdx);
+ break;
+ }
+ // Check whether we're done with mNotInElements
+ if (notInElementsIdx == notInElementsLen) {
+ NS_ASSERTION(elementsIdx < elementsLen,
+ "Should have remaining in-elements");
+ // Append the remaining mElements elements
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ aControls.AppendElements(mElements->Elements() + elementsIdx,
+ elementsLen - elementsIdx);
+ break;
+ }
+ // Both lists have elements left.
+ NS_ASSERTION(mElements->ElementAt(elementsIdx) &&
+ mNotInElements->ElementAt(notInElementsIdx),
+ "Should have remaining elements");
+ // Determine which of the two elements should be ordered
+ // first and add it to the end of the list.
+ nsGenericHTMLFormElement* elementToAdd;
+ if (nsContentUtils::CompareTreePosition<TreeKind::DOM>(
+ mElements->ElementAt(elementsIdx),
+ mNotInElements->ElementAt(notInElementsIdx), mForm) < 0) {
+ elementToAdd = mElements->ElementAt(elementsIdx);
+ ++elementsIdx;
+ } else {
+ elementToAdd = mNotInElements->ElementAt(notInElementsIdx);
+ ++notInElementsIdx;
+ }
+ // Add the first element to the list.
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ aControls.AppendElement(elementToAdd);
+ }
+
+ NS_ASSERTION(aControls.Length() == elementsLen + notInElementsLen,
+ "Not all form controls were added to the sorted list");
+#ifdef DEBUG
+ HTMLFormElement::AssertDocumentOrder(aControls, mForm);
+#endif
+
+ return NS_OK;
+}
+
+Element* HTMLFormControlsCollection::GetElementAt(uint32_t aIndex) {
+ return mElements->SafeElementAt(aIndex, nullptr);
+}
+
+/* virtual */
+nsINode* HTMLFormControlsCollection::GetParentObject() { return mForm; }
+
+/* virtual */
+Element* HTMLFormControlsCollection::GetFirstNamedElement(
+ const nsAString& aName, bool& aFound) {
+ Nullable<OwningRadioNodeListOrElement> maybeResult;
+ NamedGetter(aName, aFound, maybeResult);
+ if (!aFound) {
+ return nullptr;
+ }
+ MOZ_ASSERT(!maybeResult.IsNull());
+ const OwningRadioNodeListOrElement& result = maybeResult.Value();
+ if (result.IsElement()) {
+ return result.GetAsElement().get();
+ }
+ if (result.IsRadioNodeList()) {
+ RadioNodeList& nodelist = result.GetAsRadioNodeList();
+ return nodelist.Item(0)->AsElement();
+ }
+ MOZ_ASSERT_UNREACHABLE("Should only have Elements and NodeLists here.");
+ return nullptr;
+}
+
+void HTMLFormControlsCollection::NamedGetter(
+ const nsAString& aName, bool& aFound,
+ Nullable<OwningRadioNodeListOrElement>& aResult) {
+ nsISupports* item = NamedItemInternal(aName);
+ if (!item) {
+ aFound = false;
+ return;
+ }
+ aFound = true;
+ if (nsCOMPtr<Element> element = do_QueryInterface(item)) {
+ aResult.SetValue().SetAsElement() = element;
+ return;
+ }
+ if (nsCOMPtr<RadioNodeList> nodelist = do_QueryInterface(item)) {
+ aResult.SetValue().SetAsRadioNodeList() = nodelist;
+ return;
+ }
+ MOZ_ASSERT_UNREACHABLE("Should only have Elements and NodeLists here.");
+}
+
+void HTMLFormControlsCollection::GetSupportedNames(nsTArray<nsString>& aNames) {
+ // Just enumerate mNameLookupTable. This won't guarantee order, but
+ // that's OK, because the HTML5 spec doesn't define an order for
+ // this enumeration.
+ AppendToArray(aNames, mNameLookupTable.Keys());
+}
+
+/* virtual */
+JSObject* HTMLFormControlsCollection::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return HTMLFormControlsCollection_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLFormControlsCollection.h b/dom/html/HTMLFormControlsCollection.h
new file mode 100644
index 0000000000..367c230b84
--- /dev/null
+++ b/dom/html/HTMLFormControlsCollection.h
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLFormControlsCollection_h
+#define mozilla_dom_HTMLFormControlsCollection_h
+
+#include "nsIHTMLCollection.h"
+#include "nsInterfaceHashtable.h"
+#include "mozilla/dom/TreeOrderedArray.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+
+class nsGenericHTMLFormElement;
+class nsIContent;
+class nsIFormControl;
+template <class T>
+class RefPtr;
+
+namespace mozilla::dom {
+class Element;
+class HTMLFormElement;
+class HTMLImageElement;
+class OwningRadioNodeListOrElement;
+template <typename>
+struct Nullable;
+
+class HTMLFormControlsCollection final : public nsIHTMLCollection,
+ public nsWrapperCache {
+ public:
+ explicit HTMLFormControlsCollection(HTMLFormElement* aForm);
+
+ void DropFormReference();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+
+ virtual uint32_t Length() override;
+ virtual Element* GetElementAt(uint32_t index) override;
+ virtual nsINode* GetParentObject() override;
+
+ virtual Element* GetFirstNamedElement(const nsAString& aName,
+ bool& aFound) override;
+
+ void NamedGetter(const nsAString& aName, bool& aFound,
+ Nullable<OwningRadioNodeListOrElement>& aResult);
+ void NamedItem(const nsAString& aName,
+ Nullable<OwningRadioNodeListOrElement>& aResult) {
+ bool dummy;
+ NamedGetter(aName, dummy, aResult);
+ }
+ virtual void GetSupportedNames(nsTArray<nsString>& aNames) override;
+
+ nsresult AddElementToTable(nsGenericHTMLFormElement* aChild,
+ const nsAString& aName);
+ nsresult AddImageElementToTable(HTMLImageElement* aChild,
+ const nsAString& aName);
+ nsresult RemoveElementFromTable(nsGenericHTMLFormElement* aChild,
+ const nsAString& aName);
+ nsresult IndexOfContent(nsIContent* aContent, int32_t* aIndex);
+
+ nsISupports* NamedItemInternal(const nsAString& aName);
+
+ /**
+ * Create a sorted list of form control elements. This list is sorted
+ * in document order and contains the controls in the mElements and
+ * mNotInElements list. This function does not add references to the
+ * elements.
+ *
+ * @param aControls The list of sorted controls[out].
+ * @return NS_OK or NS_ERROR_OUT_OF_MEMORY.
+ */
+ nsresult GetSortedControls(
+ nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls) const;
+
+ // nsWrapperCache
+ using nsWrapperCache::GetWrapper;
+ using nsWrapperCache::GetWrapperPreserveColor;
+ using nsWrapperCache::PreserveWrapper;
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ protected:
+ virtual ~HTMLFormControlsCollection();
+ virtual JSObject* GetWrapperPreserveColorInternal() override {
+ return nsWrapperCache::GetWrapperPreserveColor();
+ }
+ virtual void PreserveWrapperInternal(
+ nsISupports* aScriptObjectHolder) override {
+ nsWrapperCache::PreserveWrapper(aScriptObjectHolder);
+ }
+
+ public:
+ static bool ShouldBeInElements(nsIFormControl* aFormControl);
+
+ HTMLFormElement* mForm; // WEAK - the form owns me
+
+ // Holds WEAK references - bug 36639
+ // NOTE(emilio): These are not guaranteed to be descendants of mForm, because
+ // of the form attribute, though that's likely.
+ TreeOrderedArray<nsGenericHTMLFormElement*> mElements;
+
+ // This array holds on to all form controls that are not contained
+ // in mElements (form.elements in JS, see ShouldBeInFormControl()).
+ // This is needed to properly clean up the bi-directional references
+ // (both weak and strong) between the form and its form controls.
+ TreeOrderedArray<nsGenericHTMLFormElement*> mNotInElements;
+
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(HTMLFormControlsCollection)
+
+ protected:
+ // Drop all our references to the form elements
+ void Clear();
+
+ // A map from an ID or NAME attribute to the form control(s), this
+ // hash holds strong references either to the named form control, or
+ // to a list of named form controls, in the case where this hash
+ // holds on to a list of named form controls the list has weak
+ // references to the form control.
+
+ nsInterfaceHashtable<nsStringHashKey, nsISupports> mNameLookupTable;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLFormControlsCollection_h
diff --git a/dom/html/HTMLFormElement.cpp b/dom/html/HTMLFormElement.cpp
new file mode 100644
index 0000000000..46f2c2a735
--- /dev/null
+++ b/dom/html/HTMLFormElement.cpp
@@ -0,0 +1,2054 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLFormElement.h"
+
+#include <utility>
+
+#include "Attr.h"
+#include "jsapi.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/BinarySearch.h"
+#include "mozilla/Components.h"
+#include "mozilla/ContentEvents.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/CustomEvent.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLFormControlsCollection.h"
+#include "mozilla/dom/HTMLFormElementBinding.h"
+#include "mozilla/dom/TreeOrderedArrayInlines.h"
+#include "mozilla/dom/nsCSPContext.h"
+#include "mozilla/dom/nsCSPUtils.h"
+#include "mozilla/dom/nsMixedContentBlocker.h"
+#include "nsCOMArray.h"
+#include "nsContentList.h"
+#include "nsContentUtils.h"
+#include "nsDOMAttributeMap.h"
+#include "nsDocShell.h"
+#include "nsDocShellLoadState.h"
+#include "nsError.h"
+#include "nsFocusManager.h"
+#include "nsGkAtoms.h"
+#include "nsHTMLDocument.h"
+#include "nsIFormControlFrame.h"
+#include "nsInterfaceHashtable.h"
+#include "nsPresContext.h"
+#include "nsQueryObject.h"
+#include "nsStyleConsts.h"
+#include "nsTArray.h"
+
+// form submission
+#include "HTMLFormSubmissionConstants.h"
+#include "mozilla/dom/FormData.h"
+#include "mozilla/dom/FormDataEvent.h"
+#include "mozilla/dom/SubmitEvent.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_prompts.h"
+#include "nsCategoryManagerUtils.h"
+#include "nsIContentInlines.h"
+#include "nsISimpleEnumerator.h"
+#include "nsRange.h"
+#include "nsIScriptError.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsNetUtil.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIDocShell.h"
+#include "nsIPromptService.h"
+#include "nsISecurityUITelemetry.h"
+#include "nsIStringBundle.h"
+
+// radio buttons
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/HTMLButtonElement.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+#include "nsIRadioVisitor.h"
+#include "RadioNodeList.h"
+
+#include "nsLayoutUtils.h"
+
+#include "mozAutoDocUpdate.h"
+#include "nsIHTMLCollection.h"
+
+#include "nsIConstraintValidation.h"
+
+#include "nsSandboxFlags.h"
+
+#include "mozilla/dom/HTMLAnchorElement.h"
+
+// images
+#include "mozilla/dom/HTMLImageElement.h"
+#include "mozilla/dom/HTMLButtonElement.h"
+
+// construction, destruction
+NS_IMPL_NS_NEW_HTML_ELEMENT(Form)
+
+namespace mozilla::dom {
+
+static const uint8_t NS_FORM_AUTOCOMPLETE_ON = 1;
+static const uint8_t NS_FORM_AUTOCOMPLETE_OFF = 0;
+
+static const nsAttrValue::EnumTable kFormAutocompleteTable[] = {
+ {"on", NS_FORM_AUTOCOMPLETE_ON},
+ {"off", NS_FORM_AUTOCOMPLETE_OFF},
+ {nullptr, 0}};
+// Default autocomplete value is 'on'.
+static const nsAttrValue::EnumTable* kFormDefaultAutocomplete =
+ &kFormAutocompleteTable[0];
+
+HTMLFormElement::HTMLFormElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)),
+ mControls(new HTMLFormControlsCollection(this)),
+ mPendingSubmission(nullptr),
+ mDefaultSubmitElement(nullptr),
+ mFirstSubmitInElements(nullptr),
+ mFirstSubmitNotInElements(nullptr),
+ mImageNameLookupTable(FORM_CONTROL_LIST_HASHTABLE_LENGTH),
+ mPastNameLookupTable(FORM_CONTROL_LIST_HASHTABLE_LENGTH),
+ mSubmitPopupState(PopupBlocker::openAbused),
+ mInvalidElementsCount(0),
+ mFormNumber(-1),
+ mGeneratingSubmit(false),
+ mGeneratingReset(false),
+ mDeferSubmission(false),
+ mNotifiedObservers(false),
+ mNotifiedObserversResult(false),
+ mIsConstructingEntryList(false),
+ mIsFiringSubmissionEvents(false) {
+ // We start out valid.
+ AddStatesSilently(ElementState::VALID);
+}
+
+HTMLFormElement::~HTMLFormElement() {
+ if (mControls) {
+ mControls->DropFormReference();
+ }
+
+ Clear();
+}
+
+// nsISupports
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLFormElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLFormElement,
+ nsGenericHTMLElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControls)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImageNameLookupTable)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPastNameLookupTable)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelList)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTargetContext)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLFormElement,
+ nsGenericHTMLElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mRelList)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTargetContext)
+ tmp->Clear();
+ tmp->mExpandoAndGeneration.OwnerUnlinked();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLFormElement,
+ nsGenericHTMLElement)
+
+// EventTarget
+void HTMLFormElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) {
+ if (aEvent->mEventType == u"DOMFormHasPassword"_ns) {
+ mHasPendingPasswordEvent = false;
+ } else if (aEvent->mEventType == u"DOMFormHasPossibleUsername"_ns) {
+ mHasPendingPossibleUsernameEvent = false;
+ }
+}
+
+nsDOMTokenList* HTMLFormElement::RelList() {
+ if (!mRelList) {
+ mRelList =
+ new nsDOMTokenList(this, nsGkAtoms::rel, sAnchorAndFormRelValues);
+ }
+ return mRelList;
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLFormElement)
+
+HTMLFormControlsCollection* HTMLFormElement::Elements() { return mControls; }
+
+void HTMLFormElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::action || aName == nsGkAtoms::target) {
+ // Don't forget we've notified the password manager already if the
+ // page sets the action/target in the during submit. (bug 343182)
+ bool notifiedObservers = mNotifiedObservers;
+ ForgetCurrentSubmission();
+ mNotifiedObservers = notifiedObservers;
+ }
+ }
+
+ return nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue,
+ aNotify);
+}
+
+void HTMLFormElement::GetAutocomplete(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::autocomplete, kFormDefaultAutocomplete->tag, aValue);
+}
+
+void HTMLFormElement::GetEnctype(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::enctype, kFormDefaultEnctype->tag, aValue);
+}
+
+void HTMLFormElement::GetMethod(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::method, kFormDefaultMethod->tag, aValue);
+}
+
+void HTMLFormElement::ReportInvalidUnfocusableElements() {
+ RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
+ MOZ_ASSERT(focusManager);
+
+ // This shouldn't be called recursively, so use a rather large value
+ // for the preallocated buffer.
+ AutoTArray<RefPtr<nsGenericHTMLFormElement>, 100> sortedControls;
+ if (NS_FAILED(mControls->GetSortedControls(sortedControls))) {
+ return;
+ }
+
+ for (auto& _e : sortedControls) {
+ // MOZ_CAN_RUN_SCRIPT requires explicit copy, Bug 1620312
+ RefPtr<nsGenericHTMLFormElement> element = _e;
+ bool isFocusable = false;
+ focusManager->ElementIsFocusable(element, 0, &isFocusable);
+ if (!isFocusable) {
+ nsTArray<nsString> params;
+ nsAutoCString messageName("InvalidFormControlUnfocusable");
+
+ if (Attr* nameAttr = element->GetAttributes()->GetNamedItem(u"name"_ns)) {
+ nsAutoString name;
+ nameAttr->GetValue(name);
+ params.AppendElement(name);
+ messageName = "InvalidNamedFormControlUnfocusable";
+ }
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::errorFlag, "DOM"_ns, element->GetOwnerDocument(),
+ nsContentUtils::eDOM_PROPERTIES, messageName.get(), params,
+ element->GetBaseURI());
+ }
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/forms.html#concept-form-submit
+void HTMLFormElement::MaybeSubmit(Element* aSubmitter) {
+#ifdef DEBUG
+ if (aSubmitter) {
+ nsCOMPtr<nsIFormControl> fc = do_QueryInterface(aSubmitter);
+ MOZ_ASSERT(fc);
+ MOZ_ASSERT(fc->IsSubmitControl(), "aSubmitter is not a submit control?");
+ }
+#endif
+
+ // 1-4 of
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-form-submit
+ Document* doc = GetComposedDoc();
+ if (mIsConstructingEntryList || !doc ||
+ (doc->GetSandboxFlags() & SANDBOXED_FORMS)) {
+ return;
+ }
+
+ // 5.1. If form's firing submission events is true, then return.
+ if (mIsFiringSubmissionEvents) {
+ return;
+ }
+
+ // 5.2. Set form's firing submission events to true.
+ AutoRestore<bool> resetFiringSubmissionEventsFlag(mIsFiringSubmissionEvents);
+ mIsFiringSubmissionEvents = true;
+
+ // Flag elements as user-interacted.
+ // FIXME: Should be specified, see:
+ // https://github.com/whatwg/html/issues/10066
+ {
+ for (nsGenericHTMLFormElement* el : mControls->mElements.AsList()) {
+ el->SetUserInteracted(true);
+ }
+ for (nsGenericHTMLFormElement* el : mControls->mNotInElements.AsList()) {
+ el->SetUserInteracted(true);
+ }
+ }
+
+ // 5.3. If the submitter element's no-validate state is false, then
+ // interactively validate the constraints of form and examine the result.
+ // If the result is negative (i.e., the constraint validation concluded
+ // that there were invalid fields and probably informed the user of this)
+ bool noValidateState =
+ HasAttr(nsGkAtoms::novalidate) ||
+ (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formnovalidate));
+ if (!noValidateState && !CheckValidFormSubmission()) {
+ ReportInvalidUnfocusableElements();
+ return;
+ }
+
+ RefPtr<PresShell> presShell = doc->GetPresShell();
+ if (!presShell) {
+ // We need the nsPresContext for dispatching the submit event. In some
+ // rare cases we need to flush notifications to force creation of the
+ // nsPresContext here (for example when a script calls form.requestSubmit()
+ // from script early during page load). We only flush the notifications
+ // if the PresShell hasn't been created yet, to limit the performance
+ // impact.
+ doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames);
+ presShell = doc->GetPresShell();
+ }
+
+ // If |PresShell::Destroy| has been called due to handling the event the pres
+ // context will return a null pres shell. See bug 125624. Using presShell to
+ // dispatch the event. It makes sure that event is not handled if the window
+ // is being destroyed.
+ if (presShell) {
+ SubmitEventInit init;
+ init.mBubbles = true;
+ init.mCancelable = true;
+ init.mSubmitter =
+ aSubmitter ? nsGenericHTMLElement::FromNode(aSubmitter) : nullptr;
+ RefPtr<SubmitEvent> event =
+ SubmitEvent::Constructor(this, u"submit"_ns, init);
+ event->SetTrusted(true);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ presShell->HandleDOMEventWithTarget(this, event, &status);
+ }
+}
+
+void HTMLFormElement::MaybeReset(Element* aSubmitter) {
+ // If |PresShell::Destroy| has been called due to handling the event the pres
+ // context will return a null pres shell. See bug 125624. Using presShell to
+ // dispatch the event. It makes sure that event is not handled if the window
+ // is being destroyed.
+ if (RefPtr<PresShell> presShell = OwnerDoc()->GetPresShell()) {
+ InternalFormEvent event(true, eFormReset);
+ event.mOriginator = aSubmitter;
+ nsEventStatus status = nsEventStatus_eIgnore;
+ presShell->HandleDOMEventWithTarget(this, &event, &status);
+ }
+}
+
+void HTMLFormElement::Submit(ErrorResult& aRv) { aRv = DoSubmit(); }
+
+// https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit
+void HTMLFormElement::RequestSubmit(nsGenericHTMLElement* aSubmitter,
+ ErrorResult& aRv) {
+ // 1. If submitter is not null, then:
+ if (aSubmitter) {
+ nsCOMPtr<nsIFormControl> fc = do_QueryObject(aSubmitter);
+
+ // 1.1. If submitter is not a submit button, then throw a TypeError.
+ if (!fc || !fc->IsSubmitControl()) {
+ aRv.ThrowTypeError("The submitter is not a submit button.");
+ return;
+ }
+
+ // 1.2. If submitter's form owner is not this form element, then throw a
+ // "NotFoundError" DOMException.
+ if (fc->GetForm() != this) {
+ aRv.ThrowNotFoundError("The submitter is not owned by this form.");
+ return;
+ }
+ }
+
+ // 2. Otherwise, set submitter to this form element.
+ // 3. Submit this form element, from submitter.
+ MaybeSubmit(aSubmitter);
+}
+
+void HTMLFormElement::Reset() {
+ InternalFormEvent event(true, eFormReset);
+ EventDispatcher::Dispatch(this, nullptr, &event);
+}
+
+bool HTMLFormElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::method) {
+ return aResult.ParseEnumValue(aValue, kFormMethodTable, false);
+ }
+ if (aAttribute == nsGkAtoms::enctype) {
+ return aResult.ParseEnumValue(aValue, kFormEnctypeTable, false);
+ }
+ if (aAttribute == nsGkAtoms::autocomplete) {
+ return aResult.ParseEnumValue(aValue, kFormAutocompleteTable, false);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+nsresult HTMLFormElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsInUncomposedDoc() && aContext.OwnerDoc().IsHTMLOrXHTML()) {
+ aContext.OwnerDoc().AsHTMLDocument()->AddedForm();
+ }
+
+ return rv;
+}
+
+template <typename T>
+static void MarkOrphans(const nsTArray<T*>& aArray) {
+ uint32_t length = aArray.Length();
+ for (uint32_t i = 0; i < length; ++i) {
+ aArray[i]->SetFlags(MAYBE_ORPHAN_FORM_ELEMENT);
+ }
+}
+
+static void CollectOrphans(nsINode* aRemovalRoot,
+ const nsTArray<nsGenericHTMLFormElement*>& aArray
+#ifdef DEBUG
+ ,
+ HTMLFormElement* aThisForm
+#endif
+) {
+ // Put a script blocker around all the notifications we're about to do.
+ nsAutoScriptBlocker scriptBlocker;
+
+ // Walk backwards so that if we remove elements we can just keep iterating
+ uint32_t length = aArray.Length();
+ for (uint32_t i = length; i > 0; --i) {
+ nsGenericHTMLFormElement* node = aArray[i - 1];
+
+ // Now if MAYBE_ORPHAN_FORM_ELEMENT is not set, that would mean that the
+ // node is in fact a descendant of the form and hence should stay in the
+ // form. If it _is_ set, then we need to check whether the node is a
+ // descendant of aRemovalRoot. If it is, we leave it in the form.
+#ifdef DEBUG
+ bool removed = false;
+#endif
+ if (node->HasFlag(MAYBE_ORPHAN_FORM_ELEMENT)) {
+ node->UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT);
+ if (!node->IsInclusiveDescendantOf(aRemovalRoot)) {
+ nsCOMPtr<nsIFormControl> fc = do_QueryInterface(node);
+ MOZ_ASSERT(fc);
+ fc->ClearForm(true, false);
+#ifdef DEBUG
+ removed = true;
+#endif
+ }
+ }
+
+#ifdef DEBUG
+ if (!removed) {
+ nsCOMPtr<nsIFormControl> fc = do_QueryInterface(node);
+ MOZ_ASSERT(fc);
+ HTMLFormElement* form = fc->GetForm();
+ NS_ASSERTION(form == aThisForm, "How did that happen?");
+ }
+#endif /* DEBUG */
+ }
+}
+
+static void CollectOrphans(nsINode* aRemovalRoot,
+ const nsTArray<HTMLImageElement*>& aArray
+#ifdef DEBUG
+ ,
+ HTMLFormElement* aThisForm
+#endif
+) {
+ // Walk backwards so that if we remove elements we can just keep iterating
+ uint32_t length = aArray.Length();
+ for (uint32_t i = length; i > 0; --i) {
+ HTMLImageElement* node = aArray[i - 1];
+
+ // Now if MAYBE_ORPHAN_FORM_ELEMENT is not set, that would mean that the
+ // node is in fact a descendant of the form and hence should stay in the
+ // form. If it _is_ set, then we need to check whether the node is a
+ // descendant of aRemovalRoot. If it is, we leave it in the form.
+#ifdef DEBUG
+ bool removed = false;
+#endif
+ if (node->HasFlag(MAYBE_ORPHAN_FORM_ELEMENT)) {
+ node->UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT);
+ if (!node->IsInclusiveDescendantOf(aRemovalRoot)) {
+ node->ClearForm(true);
+
+#ifdef DEBUG
+ removed = true;
+#endif
+ }
+ }
+
+#ifdef DEBUG
+ if (!removed) {
+ HTMLFormElement* form = node->GetForm();
+ NS_ASSERTION(form == aThisForm, "How did that happen?");
+ }
+#endif /* DEBUG */
+ }
+}
+
+void HTMLFormElement::UnbindFromTree(bool aNullParent) {
+ MaybeFireFormRemoved();
+
+ // Note, this is explicitly using uncomposed doc, since we count
+ // only forms in document.
+ RefPtr<Document> oldDocument = GetUncomposedDoc();
+
+ // Mark all of our controls as maybe being orphans
+ MarkOrphans(mControls->mElements.AsList());
+ MarkOrphans(mControls->mNotInElements.AsList());
+ MarkOrphans(mImageElements.AsList());
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+
+ nsINode* ancestor = this;
+ nsINode* cur;
+ do {
+ cur = ancestor->GetParentNode();
+ if (!cur) {
+ break;
+ }
+ ancestor = cur;
+ } while (true);
+
+ CollectOrphans(ancestor, mControls->mElements
+#ifdef DEBUG
+ ,
+ this
+#endif
+ );
+ CollectOrphans(ancestor, mControls->mNotInElements
+#ifdef DEBUG
+ ,
+ this
+#endif
+ );
+ CollectOrphans(ancestor, mImageElements
+#ifdef DEBUG
+ ,
+ this
+#endif
+ );
+
+ if (oldDocument && oldDocument->IsHTMLOrXHTML()) {
+ oldDocument->AsHTMLDocument()->RemovedForm();
+ }
+ ForgetCurrentSubmission();
+}
+
+static bool CanSubmit(WidgetEvent& aEvent) {
+ // According to the UI events spec section "Trusted events", we shouldn't
+ // trigger UA default action with an untrusted event except click.
+ // However, there are still some sites depending on sending untrusted event
+ // to submit form, see Bug 1370630.
+ return !StaticPrefs::dom_forms_submit_trusted_event_only() ||
+ aEvent.IsTrusted();
+}
+
+void HTMLFormElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ aVisitor.mWantsWillHandleEvent = true;
+ if (aVisitor.mEvent->mOriginalTarget == static_cast<nsIContent*>(this) &&
+ CanSubmit(*aVisitor.mEvent)) {
+ uint32_t msg = aVisitor.mEvent->mMessage;
+ if (msg == eFormSubmit) {
+ if (mGeneratingSubmit) {
+ aVisitor.mCanHandle = false;
+ return;
+ }
+ mGeneratingSubmit = true;
+
+ // XXXedgar, the untrusted event would trigger form submission, in this
+ // case, form need to handle defer flag and flushing pending submission by
+ // itself. This could be removed after Bug 1370630.
+ if (!aVisitor.mEvent->IsTrusted()) {
+ // let the form know that it needs to defer the submission,
+ // that means that if there are scripted submissions, the
+ // latest one will be deferred until after the exit point of the
+ // handler.
+ mDeferSubmission = true;
+ }
+ } else if (msg == eFormReset) {
+ if (mGeneratingReset) {
+ aVisitor.mCanHandle = false;
+ return;
+ }
+ mGeneratingReset = true;
+ }
+ }
+ nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+}
+
+void HTMLFormElement::WillHandleEvent(EventChainPostVisitor& aVisitor) {
+ // If this is the bubble stage and there is a nested form below us which
+ // received a submit event we do *not* want to handle the submit event
+ // for this form too.
+ if ((aVisitor.mEvent->mMessage == eFormSubmit ||
+ aVisitor.mEvent->mMessage == eFormReset) &&
+ aVisitor.mEvent->mFlags.mInBubblingPhase &&
+ aVisitor.mEvent->mOriginalTarget != static_cast<nsIContent*>(this)) {
+ aVisitor.mEvent->StopPropagation();
+ }
+}
+
+nsresult HTMLFormElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
+ if (aVisitor.mEvent->mOriginalTarget == static_cast<nsIContent*>(this) &&
+ CanSubmit(*aVisitor.mEvent)) {
+ EventMessage msg = aVisitor.mEvent->mMessage;
+ if (aVisitor.mEventStatus == nsEventStatus_eIgnore) {
+ switch (msg) {
+ case eFormReset: {
+ DoReset();
+ break;
+ }
+ case eFormSubmit: {
+ if (!aVisitor.mEvent->IsTrusted()) {
+ // Warning about the form submission is from untrusted event.
+ OwnerDoc()->WarnOnceAbout(
+ DeprecatedOperations::eFormSubmissionUntrustedEvent);
+ }
+ RefPtr<Event> event = aVisitor.mDOMEvent;
+ DoSubmit(event);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ // XXXedgar, the untrusted event would trigger form submission, in this
+ // case, form need to handle defer flag and flushing pending submission by
+ // itself. This could be removed after Bug 1370630.
+ if (msg == eFormSubmit && !aVisitor.mEvent->IsTrusted()) {
+ // let the form know not to defer subsequent submissions
+ mDeferSubmission = false;
+ // tell the form to flush a possible pending submission.
+ FlushPendingSubmission();
+ }
+
+ if (msg == eFormSubmit) {
+ mGeneratingSubmit = false;
+ } else if (msg == eFormReset) {
+ mGeneratingReset = false;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult HTMLFormElement::DoReset() {
+ // Make sure the presentation is up-to-date
+ Document* doc = GetComposedDoc();
+ if (doc) {
+ doc->FlushPendingNotifications(FlushType::ContentAndNotify);
+ }
+
+ // JBK walk the elements[] array instead of form frame controls - bug 34297
+ uint32_t numElements = mControls->Length();
+ for (uint32_t elementX = 0; elementX < numElements; ++elementX) {
+ // Hold strong ref in case the reset does something weird
+ nsCOMPtr<nsIFormControl> controlNode = do_QueryInterface(
+ mControls->mElements->SafeElementAt(elementX, nullptr));
+ if (controlNode) {
+ controlNode->Reset();
+ }
+ }
+
+ return NS_OK;
+}
+
+#define NS_ENSURE_SUBMIT_SUCCESS(rv) \
+ if (NS_FAILED(rv)) { \
+ ForgetCurrentSubmission(); \
+ return rv; \
+ }
+
+nsresult HTMLFormElement::DoSubmit(Event* aEvent) {
+ Document* doc = GetComposedDoc();
+ NS_ASSERTION(doc, "Should never get here without a current doc");
+
+ // Make sure the presentation is up-to-date
+ if (doc) {
+ doc->FlushPendingNotifications(FlushType::ContentAndNotify);
+ }
+
+ // Don't submit if we're not in a document or if we're in
+ // a sandboxed frame and form submit is disabled.
+ if (mIsConstructingEntryList || !doc ||
+ (doc->GetSandboxFlags() & SANDBOXED_FORMS)) {
+ return NS_OK;
+ }
+
+ if (IsSubmitting()) {
+ NS_WARNING("Preventing double form submission");
+ // XXX Should this return an error?
+ return NS_OK;
+ }
+
+ mTargetContext = nullptr;
+ mCurrentLoadId = Nothing();
+
+ UniquePtr<HTMLFormSubmission> submission;
+
+ //
+ // prepare the submission object
+ //
+ nsresult rv = BuildSubmission(getter_Transfers(submission), aEvent);
+
+ // Don't raise an error if form cannot navigate.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return NS_OK;
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // XXXbz if the script global is that for an sXBL/XBL2 doc, it won't
+ // be a window...
+ nsPIDOMWindowOuter* window = OwnerDoc()->GetWindow();
+ if (window) {
+ mSubmitPopupState = PopupBlocker::GetPopupControlState();
+ } else {
+ mSubmitPopupState = PopupBlocker::openAbused;
+ }
+
+ //
+ // perform the submission
+ //
+ if (!submission) {
+#ifdef DEBUG
+ HTMLDialogElement* dialog = nullptr;
+ for (nsIContent* parent = GetParent(); parent;
+ parent = parent->GetParent()) {
+ dialog = HTMLDialogElement::FromNodeOrNull(parent);
+ if (dialog) {
+ break;
+ }
+ }
+ MOZ_ASSERT(!dialog || !dialog->Open());
+#endif
+ return NS_OK;
+ }
+
+ if (DialogFormSubmission* dialogSubmission =
+ submission->GetAsDialogSubmission()) {
+ return SubmitDialog(dialogSubmission);
+ }
+
+ if (mDeferSubmission) {
+ // we are in an event handler, JS submitted so we have to
+ // defer this submission. let's remember it and return
+ // without submitting
+ mPendingSubmission = std::move(submission);
+ return NS_OK;
+ }
+
+ return SubmitSubmission(submission.get());
+}
+
+nsresult HTMLFormElement::BuildSubmission(HTMLFormSubmission** aFormSubmission,
+ Event* aEvent) {
+ // Get the submitter element
+ nsGenericHTMLElement* submitter = nullptr;
+ if (aEvent) {
+ SubmitEvent* submitEvent = aEvent->AsSubmitEvent();
+ if (submitEvent) {
+ submitter = submitEvent->GetSubmitter();
+ }
+ }
+
+ nsresult rv;
+
+ //
+ // Walk over the form elements and call SubmitNamesValues() on them to get
+ // their data.
+ //
+ auto encoding = GetSubmitEncoding()->OutputEncoding();
+ RefPtr<FormData> formData =
+ new FormData(GetOwnerGlobal(), encoding, submitter);
+ rv = ConstructEntryList(formData);
+ NS_ENSURE_SUBMIT_SUCCESS(rv);
+
+ // Step 9. If form cannot navigate, then return.
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
+ if (!GetComposedDoc()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ //
+ // Get the submission object
+ //
+ rv = HTMLFormSubmission::GetFromForm(this, submitter, encoding,
+ aFormSubmission);
+ NS_ENSURE_SUBMIT_SUCCESS(rv);
+
+ //
+ // Dump the data into the submission object
+ //
+ if (!(*aFormSubmission)->GetAsDialogSubmission()) {
+ rv = formData->CopySubmissionDataTo(*aFormSubmission);
+ NS_ENSURE_SUBMIT_SUCCESS(rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLFormElement::SubmitSubmission(
+ HTMLFormSubmission* aFormSubmission) {
+ MOZ_ASSERT(!mDeferSubmission);
+ MOZ_ASSERT(!mPendingSubmission);
+
+ nsCOMPtr<nsIURI> actionURI = aFormSubmission->GetActionURL();
+ if (!actionURI) {
+ return NS_OK;
+ }
+
+ // If there is no link handler, then we won't actually be able to submit.
+ Document* doc = GetComposedDoc();
+ RefPtr<nsDocShell> container =
+ doc ? nsDocShell::Cast(doc->GetDocShell()) : nullptr;
+ if (!container || IsEditable()) {
+ return NS_OK;
+ }
+
+ // javascript URIs are not really submissions; they just call a function.
+ // Also, they may synchronously call submit(), and we want them to be able to
+ // do so while still disallowing other double submissions. (Bug 139798)
+ // Note that any other URI types that are of equivalent type should also be
+ // added here.
+ // XXXbz this is a mess. The real issue here is that nsJSChannel sets the
+ // LOAD_BACKGROUND flag, so doesn't notify us, compounded by the fact that
+ // the JS executes before we forget the submission in OnStateChange on
+ // STATE_STOP. As a result, we have to make sure that we simply pretend
+ // we're not submitting when submitting to a JS URL. That's kinda bogus, but
+ // there we are.
+ bool schemeIsJavaScript = actionURI->SchemeIs("javascript");
+
+ //
+ // Notify observers of submit
+ //
+ nsresult rv;
+ bool cancelSubmit = false;
+ if (mNotifiedObservers) {
+ cancelSubmit = mNotifiedObserversResult;
+ } else {
+ rv = NotifySubmitObservers(actionURI, &cancelSubmit, true);
+ NS_ENSURE_SUBMIT_SUCCESS(rv);
+ }
+
+ if (cancelSubmit) {
+ return NS_OK;
+ }
+
+ cancelSubmit = false;
+ rv = NotifySubmitObservers(actionURI, &cancelSubmit, false);
+ NS_ENSURE_SUBMIT_SUCCESS(rv);
+
+ if (cancelSubmit) {
+ return NS_OK;
+ }
+
+ //
+ // Submit
+ //
+ uint64_t currentLoadId = 0;
+
+ {
+ AutoPopupStatePusher popupStatePusher(mSubmitPopupState);
+
+ AutoHandlingUserInputStatePusher userInpStatePusher(
+ aFormSubmission->IsInitiatedFromUserInput());
+
+ nsCOMPtr<nsIInputStream> postDataStream;
+ rv = aFormSubmission->GetEncodedSubmission(
+ actionURI, getter_AddRefs(postDataStream), actionURI);
+ NS_ENSURE_SUBMIT_SUCCESS(rv);
+
+ nsAutoString target;
+ aFormSubmission->GetTarget(target);
+
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(actionURI);
+ loadState->SetTarget(target);
+ loadState->SetPostDataStream(postDataStream);
+ loadState->SetFirstParty(true);
+ loadState->SetIsFormSubmission(true);
+ loadState->SetTriggeringPrincipal(NodePrincipal());
+ loadState->SetPrincipalToInherit(NodePrincipal());
+ loadState->SetCsp(GetCsp());
+ loadState->SetAllowFocusMove(UserActivation::IsHandlingUserInput());
+
+ nsCOMPtr<nsIPrincipal> nodePrincipal = NodePrincipal();
+ rv = container->OnLinkClickSync(this, loadState, false, nodePrincipal);
+ NS_ENSURE_SUBMIT_SUCCESS(rv);
+
+ mTargetContext = loadState->TargetBrowsingContext().GetMaybeDiscarded();
+ currentLoadId = loadState->GetLoadIdentifier();
+ }
+
+ // Even if the submit succeeds, it's possible for there to be no
+ // browsing context; for example, if it's to a named anchor within
+ // the same page the submit will not really do anything.
+ if (mTargetContext && !mTargetContext->IsDiscarded() && !schemeIsJavaScript) {
+ mCurrentLoadId = Some(currentLoadId);
+ } else {
+ ForgetCurrentSubmission();
+ }
+
+ return rv;
+}
+
+// https://html.spec.whatwg.org/#concept-form-submit step 11
+nsresult HTMLFormElement::SubmitDialog(DialogFormSubmission* aFormSubmission) {
+ // Close the dialog subject. If there is a result, let that be the return
+ // value.
+ HTMLDialogElement* dialog = aFormSubmission->DialogElement();
+ MOZ_ASSERT(dialog);
+
+ Optional<nsAString> retValue;
+ retValue = &aFormSubmission->ReturnValue();
+ dialog->Close(retValue);
+
+ return NS_OK;
+}
+
+nsresult HTMLFormElement::DoSecureToInsecureSubmitCheck(nsIURI* aActionURL,
+ bool* aCancelSubmit) {
+ *aCancelSubmit = false;
+
+ if (!StaticPrefs::security_warn_submit_secure_to_insecure()) {
+ return NS_OK;
+ }
+
+ // Only ask the user about posting from a secure URI to an insecure URI if
+ // this element is in the root document. When this is not the case, the mixed
+ // content blocker will take care of security for us.
+ if (!OwnerDoc()->IsTopLevelContentDocument()) {
+ return NS_OK;
+ }
+
+ if (nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(aActionURL)) {
+ return NS_OK;
+ }
+
+ if (nsMixedContentBlocker::URISafeToBeLoadedInSecureContext(aActionURL)) {
+ return NS_OK;
+ }
+
+ if (nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(aActionURL)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> window = OwnerDoc()->GetWindow();
+ if (!window) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Now that we know the action URI is insecure check if we're submitting from
+ // a secure URI and if so fall thru and prompt user about posting.
+ if (nsCOMPtr<nsPIDOMWindowInner> innerWindow = OwnerDoc()->GetInnerWindow()) {
+ if (!innerWindow->IsSecureContext()) {
+ return NS_OK;
+ }
+ }
+
+ // Bug 1351358: While file URIs are considered to be secure contexts we allow
+ // submitting a form to an insecure URI from a file URI without an alert in an
+ // attempt to avoid compatibility issues.
+ if (window->GetDocumentURI()->SchemeIs("file")) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
+ if (!docShell) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPromptService> promptSvc =
+ do_GetService("@mozilla.org/prompter;1", &rv);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+ if (!stringBundleService) {
+ return NS_ERROR_FAILURE;
+ }
+ rv = stringBundleService->CreateBundle(
+ "chrome://global/locale/browser.properties",
+ getter_AddRefs(stringBundle));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ nsAutoString title;
+ nsAutoString message;
+ nsAutoString cont;
+ stringBundle->GetStringFromName("formPostSecureToInsecureWarning.title",
+ title);
+ stringBundle->GetStringFromName("formPostSecureToInsecureWarning.message",
+ message);
+ stringBundle->GetStringFromName("formPostSecureToInsecureWarning.continue",
+ cont);
+ int32_t buttonPressed;
+ bool checkState =
+ false; // this is unused (ConfirmEx requires this parameter)
+ rv = promptSvc->ConfirmExBC(
+ docShell->GetBrowsingContext(),
+ StaticPrefs::prompts_modalType_insecureFormSubmit(), title.get(),
+ message.get(),
+ (nsIPromptService::BUTTON_TITLE_IS_STRING *
+ nsIPromptService::BUTTON_POS_0) +
+ (nsIPromptService::BUTTON_TITLE_CANCEL *
+ nsIPromptService::BUTTON_POS_1),
+ cont.get(), nullptr, nullptr, nullptr, &checkState, &buttonPressed);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ *aCancelSubmit = (buttonPressed == 1);
+ uint32_t telemetryBucket =
+ nsISecurityUITelemetry::WARNING_CONFIRM_POST_TO_INSECURE_FROM_SECURE;
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::SECURITY_UI,
+ telemetryBucket);
+ if (!*aCancelSubmit) {
+ // The user opted to continue, so note that in the next telemetry bucket.
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::SECURITY_UI,
+ telemetryBucket + 1);
+ }
+ return NS_OK;
+}
+
+nsresult HTMLFormElement::NotifySubmitObservers(nsIURI* aActionURL,
+ bool* aCancelSubmit,
+ bool aEarlyNotify) {
+ if (!aEarlyNotify) {
+ nsresult rv = DoSecureToInsecureSubmitCheck(aActionURL, aCancelSubmit);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (*aCancelSubmit) {
+ return NS_OK;
+ }
+ }
+
+ bool defaultAction = true;
+ nsresult rv = nsContentUtils::DispatchEventOnlyToChrome(
+ OwnerDoc(), static_cast<nsINode*>(this),
+ aEarlyNotify ? u"DOMFormBeforeSubmit"_ns : u"DOMFormSubmit"_ns,
+ CanBubble::eYes, Cancelable::eYes, &defaultAction);
+ *aCancelSubmit = !defaultAction;
+ if (*aCancelSubmit) {
+ return NS_OK;
+ }
+ return rv;
+}
+
+nsresult HTMLFormElement::ConstructEntryList(FormData* aFormData) {
+ MOZ_ASSERT(aFormData, "Must have FormData!");
+ if (mIsConstructingEntryList) {
+ // Step 2.2 of https://xhr.spec.whatwg.org/#dom-formdata.
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ AutoRestore<bool> resetConstructingEntryList(mIsConstructingEntryList);
+ mIsConstructingEntryList = true;
+ // This shouldn't be called recursively, so use a rather large value
+ // for the preallocated buffer.
+ AutoTArray<RefPtr<nsGenericHTMLFormElement>, 100> sortedControls;
+ nsresult rv = mControls->GetSortedControls(sortedControls);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Walk the list of nodes and call SubmitNamesValues() on the controls
+ for (nsGenericHTMLFormElement* control : sortedControls) {
+ // Disabled elements don't submit
+ if (!control->IsDisabled()) {
+ nsCOMPtr<nsIFormControl> fc = do_QueryInterface(control);
+ MOZ_ASSERT(fc);
+ // Tell the control to submit its name/value pairs to the submission
+ fc->SubmitNamesValues(aFormData);
+ }
+ }
+
+ FormDataEventInit init;
+ init.mBubbles = true;
+ init.mCancelable = false;
+ init.mFormData = aFormData;
+ RefPtr<FormDataEvent> event =
+ FormDataEvent::Constructor(this, u"formdata"_ns, init);
+ event->SetTrusted(true);
+
+ EventDispatcher::DispatchDOMEvent(this, nullptr, event, nullptr, nullptr);
+
+ return NS_OK;
+}
+
+NotNull<const Encoding*> HTMLFormElement::GetSubmitEncoding() {
+ nsAutoString acceptCharsetValue;
+ GetAttr(nsGkAtoms::acceptcharset, acceptCharsetValue);
+
+ int32_t charsetLen = acceptCharsetValue.Length();
+ if (charsetLen > 0) {
+ int32_t offset = 0;
+ int32_t spPos = 0;
+ // get charset from charsets one by one
+ do {
+ spPos = acceptCharsetValue.FindChar(char16_t(' '), offset);
+ int32_t cnt = ((-1 == spPos) ? (charsetLen - offset) : (spPos - offset));
+ if (cnt > 0) {
+ nsAutoString uCharset;
+ acceptCharsetValue.Mid(uCharset, offset, cnt);
+
+ auto encoding = Encoding::ForLabelNoReplacement(uCharset);
+ if (encoding) {
+ return WrapNotNull(encoding);
+ }
+ }
+ offset = spPos + 1;
+ } while (spPos != -1);
+ }
+ // if there are no accept-charset or all the charset are not supported
+ // Get the charset from document
+ Document* doc = GetComposedDoc();
+ if (doc) {
+ return doc->GetDocumentCharacterSet();
+ }
+ return UTF_8_ENCODING;
+}
+
+Element* HTMLFormElement::IndexedGetter(uint32_t aIndex, bool& aFound) {
+ Element* element = mControls->mElements->SafeElementAt(aIndex, nullptr);
+ aFound = element != nullptr;
+ return element;
+}
+
+#ifdef DEBUG
+/**
+ * Checks that all form elements are in document order. Asserts if any pair of
+ * consecutive elements are not in increasing document order.
+ *
+ * @param aControls List of form controls to check.
+ * @param aForm Parent form of the controls.
+ */
+/* static */
+void HTMLFormElement::AssertDocumentOrder(
+ const nsTArray<nsGenericHTMLFormElement*>& aControls, nsIContent* aForm) {
+ // TODO: remove the if directive with bug 598468.
+ // This is done to prevent asserts in some edge cases.
+# if 0
+ // Only iterate if aControls is not empty, since otherwise
+ // |aControls.Length() - 1| will be a very large unsigned number... not what
+ // we want here.
+ if (!aControls.IsEmpty()) {
+ for (uint32_t i = 0; i < aControls.Length() - 1; ++i) {
+ NS_ASSERTION(
+ CompareFormControlPosition(aControls[i], aControls[i + 1], aForm) < 0,
+ "Form controls not ordered correctly");
+ }
+ }
+# endif
+}
+
+/**
+ * Copy of the above function, but with RefPtrs.
+ *
+ * @param aControls List of form controls to check.
+ * @param aForm Parent form of the controls.
+ */
+/* static */
+void HTMLFormElement::AssertDocumentOrder(
+ const nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls,
+ nsIContent* aForm) {
+ // TODO: remove the if directive with bug 598468.
+ // This is done to prevent asserts in some edge cases.
+# if 0
+ // Only iterate if aControls is not empty, since otherwise
+ // |aControls.Length() - 1| will be a very large unsigned number... not what
+ // we want here.
+ if (!aControls.IsEmpty()) {
+ for (uint32_t i = 0; i < aControls.Length() - 1; ++i) {
+ NS_ASSERTION(
+ CompareFormControlPosition(aControls[i], aControls[i + 1], aForm) < 0,
+ "Form controls not ordered correctly");
+ }
+ }
+# endif
+}
+#endif
+
+nsresult HTMLFormElement::AddElement(nsGenericHTMLFormElement* aChild,
+ bool aUpdateValidity, bool aNotify) {
+ // If an element has a @form, we can assume it *might* be able to not have
+ // a parent and still be in the form.
+ NS_ASSERTION(aChild->HasAttr(nsGkAtoms::form) || aChild->GetParent(),
+ "Form control should have a parent");
+ nsCOMPtr<nsIFormControl> fc = do_QueryObject(aChild);
+ MOZ_ASSERT(fc);
+ // Determine whether to add the new element to the elements or
+ // the not-in-elements list.
+ bool childInElements = HTMLFormControlsCollection::ShouldBeInElements(fc);
+ TreeOrderedArray<nsGenericHTMLFormElement*>& controlList =
+ childInElements ? mControls->mElements : mControls->mNotInElements;
+
+ const size_t insertedIndex = controlList.Insert(*aChild, this);
+ const bool lastElement = controlList->Length() == insertedIndex + 1;
+
+#ifdef DEBUG
+ AssertDocumentOrder(controlList, this);
+#endif
+
+ auto type = fc->ControlType();
+
+ // Default submit element handling
+ if (fc->IsSubmitControl()) {
+ // Update mDefaultSubmitElement, mFirstSubmitInElements,
+ // mFirstSubmitNotInElements.
+
+ nsGenericHTMLFormElement** firstSubmitSlot =
+ childInElements ? &mFirstSubmitInElements : &mFirstSubmitNotInElements;
+
+ // The new child is the new first submit in its list if the firstSubmitSlot
+ // is currently empty or if the child is before what's currently in the
+ // slot. Note that if we already have a control in firstSubmitSlot and
+ // we're appending this element can't possibly replace what's currently in
+ // the slot. Also note that aChild can't become the mDefaultSubmitElement
+ // unless it replaces what's in the slot. If it _does_ replace what's in
+ // the slot, it becomes the default submit if either the default submit is
+ // what's in the slot or the child is earlier than the default submit.
+ if (!*firstSubmitSlot ||
+ (!lastElement && nsContentUtils::CompareTreePosition<TreeKind::DOM>(
+ aChild, *firstSubmitSlot, this) < 0)) {
+ // Update mDefaultSubmitElement if it's currently in a valid state.
+ // Valid state means either non-null or null because there are in fact
+ // no submit elements around.
+ if ((mDefaultSubmitElement ||
+ (!mFirstSubmitInElements && !mFirstSubmitNotInElements)) &&
+ (*firstSubmitSlot == mDefaultSubmitElement ||
+ nsContentUtils::CompareTreePosition<TreeKind::DOM>(
+ aChild, mDefaultSubmitElement, this) < 0)) {
+ SetDefaultSubmitElement(aChild);
+ }
+ *firstSubmitSlot = aChild;
+ }
+
+ MOZ_ASSERT(mDefaultSubmitElement == mFirstSubmitInElements ||
+ mDefaultSubmitElement == mFirstSubmitNotInElements ||
+ !mDefaultSubmitElement,
+ "What happened here?");
+ }
+
+ // If the element is subject to constraint validaton and is invalid, we need
+ // to update our internal counter.
+ if (aUpdateValidity) {
+ nsCOMPtr<nsIConstraintValidation> cvElmt = do_QueryObject(aChild);
+ if (cvElmt && cvElmt->IsCandidateForConstraintValidation() &&
+ !cvElmt->IsValid()) {
+ UpdateValidity(false);
+ }
+ }
+
+ // Notify the radio button it's been added to a group
+ // This has to be done _after_ UpdateValidity() call to prevent the element
+ // being count twice.
+ if (type == FormControlType::InputRadio) {
+ RefPtr<HTMLInputElement> radio = static_cast<HTMLInputElement*>(aChild);
+ radio->AddToRadioGroup();
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLFormElement::AddElementToTable(nsGenericHTMLFormElement* aChild,
+ const nsAString& aName) {
+ return mControls->AddElementToTable(aChild, aName);
+}
+
+void HTMLFormElement::SetDefaultSubmitElement(
+ nsGenericHTMLFormElement* aElement) {
+ if (mDefaultSubmitElement) {
+ // It just so happens that a radio button or an <option> can't be our
+ // default submit element, so we can just blindly remove the bit.
+ mDefaultSubmitElement->RemoveStates(ElementState::DEFAULT);
+ }
+ mDefaultSubmitElement = aElement;
+ if (mDefaultSubmitElement) {
+ mDefaultSubmitElement->AddStates(ElementState::DEFAULT);
+ }
+}
+
+nsresult HTMLFormElement::RemoveElement(nsGenericHTMLFormElement* aChild,
+ bool aUpdateValidity) {
+ RemoveElementFromPastNamesMap(aChild);
+
+ //
+ // Remove it from the radio group if it's a radio button
+ //
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIFormControl> fc = do_QueryInterface(aChild);
+ MOZ_ASSERT(fc);
+ if (fc->ControlType() == FormControlType::InputRadio) {
+ RefPtr<HTMLInputElement> radio = static_cast<HTMLInputElement*>(aChild);
+ radio->RemoveFromRadioGroup();
+ }
+
+ // Determine whether to remove the child from the elements list
+ // or the not in elements list.
+ bool childInElements = HTMLFormControlsCollection::ShouldBeInElements(fc);
+ TreeOrderedArray<nsGenericHTMLFormElement*>& controls =
+ childInElements ? mControls->mElements : mControls->mNotInElements;
+
+ // Find the index of the child. This will be used later if necessary
+ // to find the default submit.
+ size_t index = controls->IndexOf(aChild);
+ NS_ENSURE_STATE(index != controls.AsList().NoIndex);
+
+ controls.RemoveElementAt(index);
+
+ // Update our mFirstSubmit* values.
+ nsGenericHTMLFormElement** firstSubmitSlot =
+ childInElements ? &mFirstSubmitInElements : &mFirstSubmitNotInElements;
+ if (aChild == *firstSubmitSlot) {
+ *firstSubmitSlot = nullptr;
+
+ // We are removing the first submit in this list, find the new first submit
+ uint32_t length = controls->Length();
+ for (uint32_t i = index; i < length; ++i) {
+ nsCOMPtr<nsIFormControl> currentControl =
+ do_QueryInterface(controls->ElementAt(i));
+ MOZ_ASSERT(currentControl);
+ if (currentControl->IsSubmitControl()) {
+ *firstSubmitSlot = controls->ElementAt(i);
+ break;
+ }
+ }
+ }
+
+ if (aChild == mDefaultSubmitElement) {
+ // Need to reset mDefaultSubmitElement. Do this asynchronously so
+ // that we're not doing it while the DOM is in flux.
+ SetDefaultSubmitElement(nullptr);
+ nsContentUtils::AddScriptRunner(new RemoveElementRunnable(this));
+
+ // Note that we don't need to notify on the old default submit (which is
+ // being removed) because it's either being removed from the DOM or
+ // changing attributes in a way that makes it responsible for sending its
+ // own notifications.
+ }
+
+ // If the element was subject to constraint validation and is invalid, we need
+ // to update our internal counter.
+ if (aUpdateValidity) {
+ nsCOMPtr<nsIConstraintValidation> cvElmt = do_QueryObject(aChild);
+ if (cvElmt && cvElmt->IsCandidateForConstraintValidation() &&
+ !cvElmt->IsValid()) {
+ UpdateValidity(true);
+ }
+ }
+
+ return rv;
+}
+
+void HTMLFormElement::HandleDefaultSubmitRemoval() {
+ if (mDefaultSubmitElement) {
+ // Already got reset somehow; nothing else to do here
+ return;
+ }
+
+ nsGenericHTMLFormElement* newDefaultSubmit;
+ if (!mFirstSubmitNotInElements) {
+ newDefaultSubmit = mFirstSubmitInElements;
+ } else if (!mFirstSubmitInElements) {
+ newDefaultSubmit = mFirstSubmitNotInElements;
+ } else {
+ NS_ASSERTION(mFirstSubmitInElements != mFirstSubmitNotInElements,
+ "How did that happen?");
+ // Have both; use the earlier one
+ newDefaultSubmit =
+ nsContentUtils::CompareTreePosition<TreeKind::DOM>(
+ mFirstSubmitInElements, mFirstSubmitNotInElements, this) < 0
+ ? mFirstSubmitInElements
+ : mFirstSubmitNotInElements;
+ }
+ SetDefaultSubmitElement(newDefaultSubmit);
+
+ MOZ_ASSERT(mDefaultSubmitElement == mFirstSubmitInElements ||
+ mDefaultSubmitElement == mFirstSubmitNotInElements,
+ "What happened here?");
+}
+
+nsresult HTMLFormElement::RemoveElementFromTableInternal(
+ nsInterfaceHashtable<nsStringHashKey, nsISupports>& aTable,
+ nsIContent* aChild, const nsAString& aName) {
+ auto entry = aTable.Lookup(aName);
+ if (!entry) {
+ return NS_OK;
+ }
+ // Single element in the hash, just remove it if it's the one
+ // we're trying to remove...
+ if (entry.Data() == aChild) {
+ entry.Remove();
+ ++mExpandoAndGeneration.generation;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIContent> content(do_QueryInterface(entry.Data()));
+ if (content) {
+ return NS_OK;
+ }
+
+ // If it's not a content node then it must be a RadioNodeList.
+ MOZ_ASSERT(nsCOMPtr<RadioNodeList>(do_QueryInterface(entry.Data())));
+ auto* list = static_cast<RadioNodeList*>(entry->get());
+
+ list->RemoveElement(aChild);
+
+ uint32_t length = list->Length();
+
+ if (!length) {
+ // If the list is empty we remove if from our hash, this shouldn't
+ // happen tho
+ entry.Remove();
+ ++mExpandoAndGeneration.generation;
+ } else if (length == 1) {
+ // Only one element left, replace the list in the hash with the
+ // single element.
+ nsIContent* node = list->Item(0);
+ if (node) {
+ entry.Data() = node;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLFormElement::RemoveElementFromTable(
+ nsGenericHTMLFormElement* aElement, const nsAString& aName) {
+ return mControls->RemoveElementFromTable(aElement, aName);
+}
+
+already_AddRefed<nsISupports> HTMLFormElement::NamedGetter(
+ const nsAString& aName, bool& aFound) {
+ aFound = true;
+
+ nsCOMPtr<nsISupports> result = DoResolveName(aName);
+ if (result) {
+ AddToPastNamesMap(aName, result);
+ return result.forget();
+ }
+
+ result = mImageNameLookupTable.GetWeak(aName);
+ if (result) {
+ AddToPastNamesMap(aName, result);
+ return result.forget();
+ }
+
+ result = mPastNameLookupTable.GetWeak(aName);
+ if (result) {
+ return result.forget();
+ }
+
+ aFound = false;
+ return nullptr;
+}
+
+void HTMLFormElement::GetSupportedNames(nsTArray<nsString>& aRetval) {
+ // TODO https://github.com/whatwg/html/issues/1731
+}
+
+already_AddRefed<nsISupports> HTMLFormElement::FindNamedItem(
+ const nsAString& aName, nsWrapperCache** aCache) {
+ // FIXME Get the wrapper cache from DoResolveName.
+
+ bool found;
+ nsCOMPtr<nsISupports> result = NamedGetter(aName, found);
+ if (result) {
+ *aCache = nullptr;
+ return result.forget();
+ }
+
+ return nullptr;
+}
+
+already_AddRefed<nsISupports> HTMLFormElement::DoResolveName(
+ const nsAString& aName) {
+ nsCOMPtr<nsISupports> result = mControls->NamedItemInternal(aName);
+ return result.forget();
+}
+
+void HTMLFormElement::OnSubmitClickBegin(Element* aOriginatingElement) {
+ mDeferSubmission = true;
+
+ // Prepare to run NotifySubmitObservers early before the
+ // scripts on the page get to modify the form data, possibly
+ // throwing off any password manager. (bug 257781)
+ nsCOMPtr<nsIURI> actionURI;
+ nsresult rv;
+
+ rv = GetActionURL(getter_AddRefs(actionURI), aOriginatingElement);
+ if (NS_FAILED(rv) || !actionURI) return;
+
+ // Notify observers of submit if the form is valid.
+ // TODO: checking for mInvalidElementsCount is a temporary fix that should be
+ // removed with bug 610402.
+ if (mInvalidElementsCount == 0) {
+ bool cancelSubmit = false;
+ rv = NotifySubmitObservers(actionURI, &cancelSubmit, true);
+ if (NS_SUCCEEDED(rv)) {
+ mNotifiedObservers = true;
+ mNotifiedObserversResult = cancelSubmit;
+ }
+ }
+}
+
+void HTMLFormElement::OnSubmitClickEnd() { mDeferSubmission = false; }
+
+void HTMLFormElement::FlushPendingSubmission() {
+ MOZ_ASSERT(!mDeferSubmission);
+
+ if (mPendingSubmission) {
+ // Transfer owning reference so that the submission doesn't get deleted
+ // if we reenter
+ UniquePtr<HTMLFormSubmission> submission = std::move(mPendingSubmission);
+
+ SubmitSubmission(submission.get());
+ }
+}
+
+void HTMLFormElement::GetAction(nsString& aValue) {
+ if (!GetAttr(nsGkAtoms::action, aValue) || aValue.IsEmpty()) {
+ Document* document = OwnerDoc();
+ nsIURI* docURI = document->GetDocumentURI();
+ if (docURI) {
+ nsAutoCString spec;
+ nsresult rv = docURI->GetSpec(spec);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ CopyUTF8toUTF16(spec, aValue);
+ }
+ } else {
+ GetURIAttr(nsGkAtoms::action, nullptr, aValue);
+ }
+}
+
+nsresult HTMLFormElement::GetActionURL(nsIURI** aActionURL,
+ Element* aOriginatingElement) {
+ nsresult rv = NS_OK;
+
+ *aActionURL = nullptr;
+
+ //
+ // Grab the URL string
+ //
+ // If the originating element is a submit control and has the formaction
+ // attribute specified, it should be used. Otherwise, the action attribute
+ // from the form element should be used.
+ //
+ nsAutoString action;
+
+ if (aOriginatingElement &&
+ aOriginatingElement->HasAttr(nsGkAtoms::formaction)) {
+#ifdef DEBUG
+ nsCOMPtr<nsIFormControl> formControl =
+ do_QueryInterface(aOriginatingElement);
+ NS_ASSERTION(formControl && formControl->IsSubmitControl(),
+ "The originating element must be a submit form control!");
+#endif // DEBUG
+
+ HTMLInputElement* inputElement =
+ HTMLInputElement::FromNode(aOriginatingElement);
+ if (inputElement) {
+ inputElement->GetFormAction(action);
+ } else {
+ auto buttonElement = HTMLButtonElement::FromNode(aOriginatingElement);
+ if (buttonElement) {
+ buttonElement->GetFormAction(action);
+ } else {
+ NS_ERROR("Originating element must be an input or button element!");
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+ } else {
+ GetAction(action);
+ }
+
+ //
+ // Form the full action URL
+ //
+
+ // Get the document to form the URL.
+ // We'll also need it later to get the DOM window when notifying form submit
+ // observers (bug 33203)
+ if (!IsInComposedDoc()) {
+ return NS_OK; // No doc means don't submit, see Bug 28988
+ }
+
+ // Get base URL
+ Document* document = OwnerDoc();
+ nsIURI* docURI = document->GetDocumentURI();
+ NS_ENSURE_TRUE(docURI, NS_ERROR_UNEXPECTED);
+
+ // If an action is not specified and we are inside
+ // a HTML document then reload the URL. This makes us
+ // compatible with 4.x browsers.
+ // If we are in some other type of document such as XML or
+ // XUL, do nothing. This prevents undesirable reloading of
+ // a document inside XUL.
+
+ nsCOMPtr<nsIURI> actionURL;
+ if (action.IsEmpty()) {
+ if (!document->IsHTMLOrXHTML()) {
+ // Must be a XML, XUL or other non-HTML document type
+ // so do nothing.
+ return NS_OK;
+ }
+
+ actionURL = docURI;
+ } else {
+ nsIURI* baseURL = GetBaseURI();
+ NS_ASSERTION(baseURL, "No Base URL found in Form Submit!\n");
+ if (!baseURL) {
+ return NS_OK; // No base URL -> exit early, see Bug 30721
+ }
+ rv = NS_NewURI(getter_AddRefs(actionURL), action, nullptr, baseURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ //
+ // Verify the URL should be reached
+ //
+ // Get security manager, check to see if access to action URI is allowed.
+ //
+ nsIScriptSecurityManager* securityManager =
+ nsContentUtils::GetSecurityManager();
+ rv = securityManager->CheckLoadURIWithPrincipal(
+ NodePrincipal(), actionURL, nsIScriptSecurityManager::STANDARD,
+ OwnerDoc()->InnerWindowID());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Potentially the page uses the CSP directive 'upgrade-insecure-requests'. In
+ // such a case we have to upgrade the action url from http:// to https://.
+ // The upgrade is only required if the actionURL is http and not a potentially
+ // trustworthy loopback URI.
+ bool needsUpgrade =
+ actionURL->SchemeIs("http") &&
+ !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(actionURL) &&
+ document->GetUpgradeInsecureRequests(false);
+ if (needsUpgrade) {
+ // let's use the old specification before the upgrade for logging
+ AutoTArray<nsString, 2> params;
+ nsAutoCString spec;
+ rv = actionURL->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyUTF8toUTF16(spec, *params.AppendElement());
+
+ // upgrade the actionURL from http:// to use https://
+ nsCOMPtr<nsIURI> upgradedActionURL;
+ rv = NS_GetSecureUpgradedURI(actionURL, getter_AddRefs(upgradedActionURL));
+ NS_ENSURE_SUCCESS(rv, rv);
+ actionURL = std::move(upgradedActionURL);
+
+ // let's log a message to the console that we are upgrading a request
+ nsAutoCString scheme;
+ rv = actionURL->GetScheme(scheme);
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyUTF8toUTF16(scheme, *params.AppendElement());
+
+ CSP_LogLocalizedStr(
+ "upgradeInsecureRequest", params,
+ u""_ns, // aSourceFile
+ u""_ns, // aScriptSample
+ 0, // aLineNumber
+ 1, // aColumnNumber
+ nsIScriptError::warningFlag, "upgradeInsecureRequest"_ns,
+ document->InnerWindowID(),
+ !!document->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId);
+ }
+
+ //
+ // Assign to the output
+ //
+ actionURL.forget(aActionURL);
+
+ return rv;
+}
+
+nsGenericHTMLFormElement* HTMLFormElement::GetDefaultSubmitElement() const {
+ MOZ_ASSERT(mDefaultSubmitElement == mFirstSubmitInElements ||
+ mDefaultSubmitElement == mFirstSubmitNotInElements,
+ "What happened here?");
+
+ return mDefaultSubmitElement;
+}
+
+bool HTMLFormElement::ImplicitSubmissionIsDisabled() const {
+ // Input text controls are always in the elements list.
+ uint32_t numDisablingControlsFound = 0;
+ uint32_t length = mControls->mElements->Length();
+ for (uint32_t i = 0; i < length && numDisablingControlsFound < 2; ++i) {
+ nsCOMPtr<nsIFormControl> fc =
+ do_QueryInterface(mControls->mElements->ElementAt(i));
+ MOZ_ASSERT(fc);
+ if (fc->IsSingleLineTextControl(false)) {
+ numDisablingControlsFound++;
+ }
+ }
+ return numDisablingControlsFound != 1;
+}
+
+bool HTMLFormElement::IsLastActiveElement(
+ const nsGenericHTMLFormElement* aElement) const {
+ MOZ_ASSERT(aElement, "Unexpected call");
+
+ for (auto* element : Reversed(mControls->mElements.AsList())) {
+ nsCOMPtr<nsIFormControl> fc = do_QueryInterface(element);
+ MOZ_ASSERT(fc);
+ // XXX How about date/time control?
+ if (fc->IsTextControl(false) && !element->IsDisabled()) {
+ return element == aElement;
+ }
+ }
+ return false;
+}
+
+int32_t HTMLFormElement::Length() { return mControls->Length(); }
+
+void HTMLFormElement::ForgetCurrentSubmission() {
+ mNotifiedObservers = false;
+ mTargetContext = nullptr;
+ mCurrentLoadId = Nothing();
+}
+
+bool HTMLFormElement::CheckFormValidity(
+ nsTArray<RefPtr<Element>>* aInvalidElements) const {
+ bool ret = true;
+
+ // This shouldn't be called recursively, so use a rather large value
+ // for the preallocated buffer.
+ AutoTArray<RefPtr<nsGenericHTMLFormElement>, 100> sortedControls;
+ if (NS_FAILED(mControls->GetSortedControls(sortedControls))) {
+ return false;
+ }
+
+ uint32_t len = sortedControls.Length();
+
+ for (uint32_t i = 0; i < len; ++i) {
+ nsCOMPtr<nsIConstraintValidation> cvElmt =
+ do_QueryObject(sortedControls[i]);
+ bool defaultAction = true;
+ if (cvElmt && !cvElmt->CheckValidity(*sortedControls[i], &defaultAction)) {
+ ret = false;
+
+ // Add all unhandled invalid controls to aInvalidElements if the caller
+ // requested them.
+ if (defaultAction && aInvalidElements) {
+ aInvalidElements->AppendElement(sortedControls[i]);
+ }
+ }
+ }
+
+ return ret;
+}
+
+bool HTMLFormElement::CheckValidFormSubmission() {
+ /**
+ * Check for form validity: do not submit a form if there are unhandled
+ * invalid controls in the form.
+ * This should not be done if the form has been submitted with .submit().
+ *
+ * NOTE: for the moment, we are also checking that whether the MozInvalidForm
+ * event gets prevented default so it will prevent blocking form submission if
+ * the browser does not have implemented a UI yet.
+ *
+ * TODO: the check for MozInvalidForm event should be removed later when HTML5
+ * Forms will be spread enough and authors will assume forms can't be
+ * submitted when invalid. See bug 587671.
+ */
+
+ NS_ASSERTION(!HasAttr(nsGkAtoms::novalidate),
+ "We shouldn't be there if novalidate is set!");
+
+ AutoTArray<RefPtr<Element>, 32> invalidElements;
+ if (CheckFormValidity(&invalidElements)) {
+ return true;
+ }
+
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(GetOwnerGlobal())) {
+ return false;
+ }
+ JS::Rooted<JS::Value> detail(jsapi.cx());
+ if (!ToJSValue(jsapi.cx(), invalidElements, &detail)) {
+ return false;
+ }
+
+ RefPtr<CustomEvent> event =
+ NS_NewDOMCustomEvent(OwnerDoc(), nullptr, nullptr);
+ event->InitCustomEvent(jsapi.cx(), u"MozInvalidForm"_ns,
+ /* CanBubble */ true,
+ /* Cancelable */ true, detail);
+ event->SetTrusted(true);
+ event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
+
+ DispatchEvent(*event);
+
+ return !event->DefaultPrevented();
+}
+
+void HTMLFormElement::UpdateValidity(bool aElementValidity) {
+ if (aElementValidity) {
+ --mInvalidElementsCount;
+ } else {
+ ++mInvalidElementsCount;
+ }
+
+ NS_ASSERTION(mInvalidElementsCount >= 0, "Something went seriously wrong!");
+
+ // The form validity has just changed if:
+ // - there are no more invalid elements ;
+ // - or there is one invalid elmement and an element just became invalid.
+ // If we have invalid elements and we used to before as well, do nothing.
+ if (mInvalidElementsCount &&
+ (mInvalidElementsCount != 1 || aElementValidity)) {
+ return;
+ }
+
+ AutoStateChangeNotifier notifier(*this, true);
+ RemoveStatesSilently(ElementState::VALID | ElementState::INVALID);
+ AddStatesSilently(mInvalidElementsCount ? ElementState::INVALID
+ : ElementState::VALID);
+}
+
+int32_t HTMLFormElement::IndexOfContent(nsIContent* aContent) {
+ int32_t index = 0;
+ return mControls->IndexOfContent(aContent, &index) == NS_OK ? index : 0;
+}
+
+void HTMLFormElement::Clear() {
+ for (HTMLImageElement* image : Reversed(mImageElements.AsList())) {
+ image->ClearForm(false);
+ }
+ mImageElements.Clear();
+ mImageNameLookupTable.Clear();
+ mPastNameLookupTable.Clear();
+}
+
+namespace {
+
+struct PositionComparator {
+ nsIContent* const mElement;
+ explicit PositionComparator(nsIContent* const aElement)
+ : mElement(aElement) {}
+
+ int operator()(nsIContent* aElement) const {
+ if (mElement == aElement) {
+ return 0;
+ }
+ if (nsContentUtils::PositionIsBefore(mElement, aElement)) {
+ return -1;
+ }
+ return 1;
+ }
+};
+
+struct RadioNodeListAdaptor {
+ RadioNodeList* const mList;
+ explicit RadioNodeListAdaptor(RadioNodeList* aList) : mList(aList) {}
+ nsIContent* operator[](size_t aIdx) const { return mList->Item(aIdx); }
+};
+
+} // namespace
+
+nsresult HTMLFormElement::AddElementToTableInternal(
+ nsInterfaceHashtable<nsStringHashKey, nsISupports>& aTable,
+ nsIContent* aChild, const nsAString& aName) {
+ return aTable.WithEntryHandle(aName, [&](auto&& entry) {
+ if (!entry) {
+ // No entry found, add the element
+ entry.Insert(aChild);
+ ++mExpandoAndGeneration.generation;
+ } else {
+ // Found something in the hash, check its type
+ nsCOMPtr<nsIContent> content = do_QueryInterface(entry.Data());
+
+ if (content) {
+ // Check if the new content is the same as the one we found in the
+ // hash, if it is then we leave it in the hash as it is, this will
+ // happen if a form control has both a name and an id with the same
+ // value
+ if (content == aChild) {
+ return NS_OK;
+ }
+
+ // Found an element, create a list, add the element to the list and put
+ // the list in the hash
+ RadioNodeList* list = new RadioNodeList(this);
+
+ // If an element has a @form, we can assume it *might* be able to not
+ // have a parent and still be in the form.
+ NS_ASSERTION(
+ (content->IsElement() && content->AsElement()->HasAttr(
+ kNameSpaceID_None, nsGkAtoms::form)) ||
+ content->GetParent(),
+ "Item in list without parent");
+
+ // Determine the ordering between the new and old element.
+ bool newFirst = nsContentUtils::PositionIsBefore(aChild, content);
+
+ list->AppendElement(newFirst ? aChild : content.get());
+ list->AppendElement(newFirst ? content.get() : aChild);
+
+ nsCOMPtr<nsISupports> listSupports = do_QueryObject(list);
+
+ // Replace the element with the list.
+ entry.Data() = listSupports;
+ } else {
+ // There's already a list in the hash, add the child to the list.
+ MOZ_ASSERT(nsCOMPtr<RadioNodeList>(do_QueryInterface(entry.Data())));
+ auto* list = static_cast<RadioNodeList*>(entry->get());
+
+ NS_ASSERTION(
+ list->Length() > 1,
+ "List should have been converted back to a single element");
+
+ // Fast-path appends; this check is ok even if the child is
+ // already in the list, since if it tests true the child would
+ // have come at the end of the list, and the PositionIsBefore
+ // will test false.
+ if (nsContentUtils::PositionIsBefore(list->Item(list->Length() - 1),
+ aChild)) {
+ list->AppendElement(aChild);
+ return NS_OK;
+ }
+
+ // If a control has a name equal to its id, it could be in the
+ // list already.
+ if (list->IndexOf(aChild) != -1) {
+ return NS_OK;
+ }
+
+ size_t idx;
+ DebugOnly<bool> found =
+ BinarySearchIf(RadioNodeListAdaptor(list), 0, list->Length(),
+ PositionComparator(aChild), &idx);
+ MOZ_ASSERT(!found, "should not have found an element");
+
+ list->InsertElementAt(aChild, idx);
+ }
+ }
+
+ return NS_OK;
+ });
+}
+
+nsresult HTMLFormElement::AddImageElement(HTMLImageElement* aElement) {
+ mImageElements.Insert(*aElement, this);
+ return NS_OK;
+}
+
+nsresult HTMLFormElement::AddImageElementToTable(HTMLImageElement* aChild,
+ const nsAString& aName) {
+ return AddElementToTableInternal(mImageNameLookupTable, aChild, aName);
+}
+
+nsresult HTMLFormElement::RemoveImageElement(HTMLImageElement* aElement) {
+ RemoveElementFromPastNamesMap(aElement);
+ mImageElements.RemoveElement(*aElement);
+ return NS_OK;
+}
+
+nsresult HTMLFormElement::RemoveImageElementFromTable(
+ HTMLImageElement* aElement, const nsAString& aName) {
+ return RemoveElementFromTableInternal(mImageNameLookupTable, aElement, aName);
+}
+
+void HTMLFormElement::AddToPastNamesMap(const nsAString& aName,
+ nsISupports* aChild) {
+ // If candidates contains exactly one node. Add a mapping from name to the
+ // node in candidates in the form element's past names map, replacing the
+ // previous entry with the same name, if any.
+ nsCOMPtr<nsIContent> node = do_QueryInterface(aChild);
+ if (node) {
+ mPastNameLookupTable.InsertOrUpdate(aName, ToSupports(node));
+ node->SetFlags(MAY_BE_IN_PAST_NAMES_MAP);
+ }
+}
+
+void HTMLFormElement::RemoveElementFromPastNamesMap(Element* aElement) {
+ if (!aElement->HasFlag(MAY_BE_IN_PAST_NAMES_MAP)) {
+ return;
+ }
+
+ aElement->UnsetFlags(MAY_BE_IN_PAST_NAMES_MAP);
+
+ uint32_t oldCount = mPastNameLookupTable.Count();
+ for (auto iter = mPastNameLookupTable.Iter(); !iter.Done(); iter.Next()) {
+ if (aElement == iter.Data()) {
+ iter.Remove();
+ }
+ }
+ if (oldCount != mPastNameLookupTable.Count()) {
+ ++mExpandoAndGeneration.generation;
+ }
+}
+
+JSObject* HTMLFormElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLFormElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+int32_t HTMLFormElement::GetFormNumberForStateKey() {
+ if (mFormNumber == -1) {
+ mFormNumber = OwnerDoc()->GetNextFormNumber();
+ }
+ return mFormNumber;
+}
+
+void HTMLFormElement::NodeInfoChanged(Document* aOldDoc) {
+ nsGenericHTMLElement::NodeInfoChanged(aOldDoc);
+
+ // When a <form> element is adopted into a new document, we want any state
+ // keys generated from it to no longer consider this element to be parser
+ // inserted, and so have state keys based on the position of the <form>
+ // element in the document, rather than the order it was inserted in.
+ //
+ // This is not strictly necessary, since we only ever look at the form number
+ // for parser inserted form controls, and we do that at the time the form
+ // control element is inserted into its original document by the parser.
+ mFormNumber = -1;
+}
+
+bool HTMLFormElement::IsSubmitting() const {
+ bool loading = mTargetContext && !mTargetContext->IsDiscarded() &&
+ mCurrentLoadId &&
+ mTargetContext->IsLoadingIdentifier(*mCurrentLoadId);
+ return loading;
+}
+
+void HTMLFormElement::MaybeFireFormRemoved() {
+ // We want this event to be fired only when the form is removed from the DOM
+ // tree, not when it is released (ex, tab is closed). So don't fire an event
+ // when the form doesn't have a docshell.
+ Document* doc = GetComposedDoc();
+ nsIDocShell* container = doc ? doc->GetDocShell() : nullptr;
+ if (!container) {
+ return;
+ }
+
+ // Right now, only the password manager and formautofill listen to the event
+ // and only listen to it under certain circumstances. So don't fire this event
+ // unless necessary.
+ if (!doc->ShouldNotifyFormOrPasswordRemoved()) {
+ return;
+ }
+
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *this, u"DOMFormRemoved"_ns, CanBubble::eNo, ChromeOnlyDispatch::eYes);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLFormElement.h b/dom/html/HTMLFormElement.h
new file mode 100644
index 0000000000..68dd627554
--- /dev/null
+++ b/dom/html/HTMLFormElement.h
@@ -0,0 +1,596 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLFormElement_h
+#define mozilla_dom_HTMLFormElement_h
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/PopupBlocker.h"
+#include "mozilla/dom/RadioGroupContainer.h"
+#include "nsIFormControl.h"
+#include "nsGenericHTMLElement.h"
+#include "nsThreadUtils.h"
+#include "nsInterfaceHashtable.h"
+#include "js/friend/DOMProxy.h" // JS::ExpandoAndGeneration
+
+class nsIMutableArray;
+class nsIURI;
+
+namespace mozilla {
+class EventChainPostVisitor;
+class EventChainPreVisitor;
+namespace dom {
+class DialogFormSubmission;
+class HTMLFormControlsCollection;
+class HTMLFormSubmission;
+class HTMLImageElement;
+class FormData;
+
+class HTMLFormElement final : public nsGenericHTMLElement {
+ friend class HTMLFormControlsCollection;
+
+ public:
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLFormElement, form)
+
+ explicit HTMLFormElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ enum { FORM_CONTROL_LIST_HASHTABLE_LENGTH = 8 };
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ int32_t IndexOfContent(nsIContent* aContent);
+ nsGenericHTMLFormElement* GetDefaultSubmitElement() const;
+ bool IsDefaultSubmitElement(nsGenericHTMLFormElement* aElement) const {
+ return aElement == mDefaultSubmitElement;
+ }
+
+ // EventTarget
+ void AsyncEventRunning(AsyncEventDispatcher* aEvent) override;
+
+ /** Whether we already dispatched a DOMFormHasPassword event or not */
+ bool mHasPendingPasswordEvent = false;
+ /** Whether we already dispatched a DOMFormHasPossibleUsername event or not */
+ bool mHasPendingPossibleUsernameEvent = false;
+
+ // nsIContent
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+ void WillHandleEvent(EventChainPostVisitor& aVisitor) override;
+ nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+ void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+
+ /**
+ * Forget all information about the current submission (and the fact that we
+ * are currently submitting at all).
+ */
+ void ForgetCurrentSubmission();
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLFormElement,
+ nsGenericHTMLElement)
+
+ /**
+ * Remove an element from this form's list of elements
+ *
+ * @param aElement the element to remove
+ * @param aUpdateValidity If true, updates the form validity.
+ * @return NS_OK if the element was successfully removed.
+ */
+ nsresult RemoveElement(nsGenericHTMLFormElement* aElement,
+ bool aUpdateValidity);
+
+ /**
+ * Remove an element from the lookup table maintained by the form.
+ * We can't fold this method into RemoveElement() because when
+ * RemoveElement() is called it doesn't know if the element is
+ * removed because the id attribute has changed, or because the
+ * name attribute has changed.
+ *
+ * @param aElement the element to remove
+ * @param aName the name or id of the element to remove
+ * @return NS_OK if the element was successfully removed.
+ */
+ nsresult RemoveElementFromTable(nsGenericHTMLFormElement* aElement,
+ const nsAString& aName);
+
+ /**
+ * Add an element to end of this form's list of elements
+ *
+ * @param aElement the element to add
+ * @param aUpdateValidity If true, the form validity will be updated.
+ * @param aNotify If true, send DocumentObserver notifications as needed.
+ * @return NS_OK if the element was successfully added
+ */
+ nsresult AddElement(nsGenericHTMLFormElement* aElement, bool aUpdateValidity,
+ bool aNotify);
+
+ /**
+ * Add an element to the lookup table maintained by the form.
+ *
+ * We can't fold this method into AddElement() because when
+ * AddElement() is called, the form control has no
+ * attributes. The name or id attributes of the form control
+ * are used as a key into the table.
+ */
+ nsresult AddElementToTable(nsGenericHTMLFormElement* aChild,
+ const nsAString& aName);
+
+ /**
+ * Remove an image element from this form's list of image elements
+ *
+ * @param aElement the image element to remove
+ * @return NS_OK if the element was successfully removed.
+ */
+ nsresult RemoveImageElement(HTMLImageElement* aElement);
+
+ /**
+ * Remove an image element from the lookup table maintained by the form.
+ * We can't fold this method into RemoveImageElement() because when
+ * RemoveImageElement() is called it doesn't know if the element is
+ * removed because the id attribute has changed, or because the
+ * name attribute has changed.
+ *
+ * @param aElement the image element to remove
+ * @param aName the name or id of the element to remove
+ * @return NS_OK if the element was successfully removed.
+ */
+ nsresult RemoveImageElementFromTable(HTMLImageElement* aElement,
+ const nsAString& aName);
+ /**
+ * Add an image element to the end of this form's list of image elements
+ *
+ * @param aElement the element to add
+ * @return NS_OK if the element was successfully added
+ */
+ nsresult AddImageElement(HTMLImageElement* aElement);
+
+ /**
+ * Add an image element to the lookup table maintained by the form.
+ *
+ * We can't fold this method into AddImageElement() because when
+ * AddImageElement() is called, the image attributes can change.
+ * The name or id attributes of the image are used as a key into the table.
+ */
+ nsresult AddImageElementToTable(HTMLImageElement* aChild,
+ const nsAString& aName);
+
+ /**
+ * Returns true if implicit submission of this form is disabled. For more
+ * on implicit submission see:
+ *
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#implicit-submission
+ */
+ bool ImplicitSubmissionIsDisabled() const;
+
+ /**
+ * Check whether a given nsGenericHTMLFormElement is the last single line
+ * input control that is not disabled. aElement is expected to not be null.
+ */
+ bool IsLastActiveElement(const nsGenericHTMLFormElement* aElement) const;
+
+ /**
+ * Flag the form to know that a button or image triggered scripted form
+ * submission. In that case the form will defer the submission until the
+ * script handler returns and the return value is known.
+ */
+ void OnSubmitClickBegin(Element* aOriginatingElement);
+ void OnSubmitClickEnd();
+
+ /**
+ * This method will update the form validity.
+ *
+ * This method has to be called by form elements whenever their validity state
+ * or status regarding constraint validation changes.
+ *
+ * @note This method isn't used for CheckValidity().
+ * @note If an element becomes barred from constraint validation, it has to be
+ * considered as valid.
+ *
+ * @param aElementValidityState the new validity state of the element
+ */
+ void UpdateValidity(bool aElementValidityState);
+
+ /**
+ * This method check the form validity and make invalid form elements send
+ * invalid event if needed.
+ *
+ * @return Whether the form is valid.
+ *
+ * @note Do not call this method if novalidate/formnovalidate is used.
+ * @note This method might disappear with bug 592124, hopefuly.
+ * @see
+ * https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#interactively-validate-the-constraints
+ */
+ bool CheckValidFormSubmission();
+
+ /**
+ * Contruct the entry list to get their data pumped into the FormData and
+ * fire a `formdata` event with the entry list in formData attribute.
+ * <https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-form-data-set>
+ *
+ * @param aFormData the form data object
+ */
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult ConstructEntryList(FormData*);
+
+ /**
+ * Implements form[name]. Returns form controls in this form with the correct
+ * value of the name attribute.
+ */
+ already_AddRefed<nsISupports> FindNamedItem(const nsAString& aName,
+ nsWrapperCache** aCache);
+
+ // WebIDL
+
+ void GetAcceptCharset(DOMString& aValue) {
+ GetHTMLAttr(nsGkAtoms::acceptcharset, aValue);
+ }
+
+ void SetAcceptCharset(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::acceptcharset, aValue, aRv);
+ }
+
+ void GetAction(nsString& aValue);
+ void SetAction(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::action, aValue, aRv);
+ }
+
+ void GetAutocomplete(nsAString& aValue);
+ void SetAutocomplete(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::autocomplete, aValue, aRv);
+ }
+
+ void GetEnctype(nsAString& aValue);
+ void SetEnctype(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::enctype, aValue, aRv);
+ }
+
+ void GetEncoding(nsAString& aValue) { GetEnctype(aValue); }
+ void SetEncoding(const nsAString& aValue, ErrorResult& aRv) {
+ SetEnctype(aValue, aRv);
+ }
+
+ void GetMethod(nsAString& aValue);
+ void SetMethod(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::method, aValue, aRv);
+ }
+
+ void GetName(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); }
+
+ void SetName(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aValue, aRv);
+ }
+
+ bool NoValidate() const { return GetBoolAttr(nsGkAtoms::novalidate); }
+
+ void SetNoValidate(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::novalidate, aValue, aRv);
+ }
+
+ void GetTarget(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::target, aValue); }
+
+ void SetTarget(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::target, aValue, aRv);
+ }
+
+ void GetRel(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::rel, aValue); }
+ void SetRel(const nsAString& aRel, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::rel, aRel, aError);
+ }
+ nsDOMTokenList* RelList();
+
+ // it's only out-of-line because the class definition is not available in the
+ // header
+ HTMLFormControlsCollection* Elements();
+
+ int32_t Length();
+
+ /**
+ * Check whether submission can proceed for this form then fire submit event.
+ * This basically implements steps 1-6 (more or less) of
+ * <https://html.spec.whatwg.org/multipage/forms.html#concept-form-submit>.
+ * @param aSubmitter If not null, is the "submitter" from that algorithm.
+ * Therefore it must be a valid submit control.
+ */
+ MOZ_CAN_RUN_SCRIPT void MaybeSubmit(Element* aSubmitter);
+ MOZ_CAN_RUN_SCRIPT void MaybeReset(Element* aSubmitter);
+ void Submit(ErrorResult& aRv);
+
+ /**
+ * Requests to submit the form. Unlike submit(), this method includes
+ * interactive constraint validation and firing a submit event,
+ * either of which can cancel submission.
+ *
+ * @param aSubmitter The submitter argument can be used to point to a specific
+ * submit button.
+ * @param aRv An ErrorResult.
+ * @see
+ * https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit
+ */
+ MOZ_CAN_RUN_SCRIPT void RequestSubmit(nsGenericHTMLElement* aSubmitter,
+ ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT void Reset();
+
+ bool CheckValidity() { return CheckFormValidity(nullptr); }
+
+ bool ReportValidity() { return CheckValidFormSubmission(); }
+
+ Element* IndexedGetter(uint32_t aIndex, bool& aFound);
+
+ already_AddRefed<nsISupports> NamedGetter(const nsAString& aName,
+ bool& aFound);
+
+ void GetSupportedNames(nsTArray<nsString>& aRetval);
+
+#ifdef DEBUG
+ static void AssertDocumentOrder(
+ const nsTArray<nsGenericHTMLFormElement*>& aControls, nsIContent* aForm);
+ static void AssertDocumentOrder(
+ const nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls,
+ nsIContent* aForm);
+#endif
+
+ JS::ExpandoAndGeneration mExpandoAndGeneration;
+
+ protected:
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ class RemoveElementRunnable;
+ friend class RemoveElementRunnable;
+
+ class RemoveElementRunnable : public Runnable {
+ public:
+ explicit RemoveElementRunnable(HTMLFormElement* aForm)
+ : Runnable("dom::HTMLFormElement::RemoveElementRunnable"),
+ mForm(aForm) {}
+
+ NS_IMETHOD Run() override {
+ mForm->HandleDefaultSubmitRemoval();
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<HTMLFormElement> mForm;
+ };
+
+ nsresult DoReset();
+
+ // Async callback to handle removal of our default submit
+ void HandleDefaultSubmitRemoval();
+
+ //
+ // Submit Helpers
+ //
+ //
+ /**
+ * Attempt to submit (submission might be deferred)
+ *
+ * @param aPresContext the presentation context
+ * @param aEvent the DOM event that was passed to us for the submit
+ */
+ nsresult DoSubmit(Event* aEvent = nullptr);
+
+ /**
+ * Prepare the submission object (called by DoSubmit)
+ *
+ * @param aFormSubmission the submission object
+ * @param aEvent the DOM event that was passed to us for the submit
+ */
+ nsresult BuildSubmission(HTMLFormSubmission** aFormSubmission, Event* aEvent);
+ /**
+ * Perform the submission (called by DoSubmit and FlushPendingSubmission)
+ *
+ * @param aFormSubmission the submission object
+ */
+ nsresult SubmitSubmission(HTMLFormSubmission* aFormSubmission);
+
+ /**
+ * Submit a form[method=dialog]
+ * @param aFormSubmission the submission object
+ */
+ nsresult SubmitDialog(DialogFormSubmission* aFormSubmission);
+
+ /**
+ * Notify any submit observers of the submit.
+ *
+ * @param aActionURL the URL being submitted to
+ * @param aCancelSubmit out param where submit observers can specify that the
+ * submit should be cancelled.
+ */
+ nsresult NotifySubmitObservers(nsIURI* aActionURL, bool* aCancelSubmit,
+ bool aEarlyNotify);
+
+ /**
+ * If this form submission is secure -> insecure, ask the user if they want
+ * to continue.
+ *
+ * @param aActionURL the URL being submitted to
+ * @param aCancelSubmit out param: will be true if the user wants to cancel
+ */
+ nsresult DoSecureToInsecureSubmitCheck(nsIURI* aActionURL,
+ bool* aCancelSubmit);
+
+ /**
+ * Find form controls in this form with the correct value in the name
+ * attribute.
+ */
+ already_AddRefed<nsISupports> DoResolveName(const nsAString& aName);
+
+ /**
+ * Check the form validity following this algorithm:
+ * https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#statically-validate-the-constraints
+ *
+ * @param aInvalidElements [out] parameter containing the list of unhandled
+ * invalid controls.
+ *
+ * @return Whether the form is currently valid.
+ */
+ bool CheckFormValidity(nsTArray<RefPtr<Element>>* aInvalidElements) const;
+
+ // Clear the mImageNameLookupTable and mImageElements.
+ void Clear();
+
+ // Insert a element into the past names map.
+ void AddToPastNamesMap(const nsAString& aName, nsISupports* aChild);
+
+ // Remove the given element from the past names map. The element must be an
+ // nsGenericHTMLFormElement or HTMLImageElement.
+ void RemoveElementFromPastNamesMap(Element* aElement);
+
+ nsresult AddElementToTableInternal(
+ nsInterfaceHashtable<nsStringHashKey, nsISupports>& aTable,
+ nsIContent* aChild, const nsAString& aName);
+
+ nsresult RemoveElementFromTableInternal(
+ nsInterfaceHashtable<nsStringHashKey, nsISupports>& aTable,
+ nsIContent* aChild, const nsAString& aName);
+
+ public:
+ /**
+ * Flush a possible pending submission. If there was a scripted submission
+ * triggered by a button or image, the submission was defered. This method
+ * forces the pending submission to be submitted. (happens when the handler
+ * returns false or there is an action/target change in the script)
+ */
+ void FlushPendingSubmission();
+
+ /**
+ * Get the full URL to submit to. Do not submit if the returned URL is null.
+ *
+ * @param aActionURL the full, unadulterated URL you'll be submitting to [OUT]
+ * @param aOriginatingElement the originating element of the form submission
+ * [IN]
+ */
+ nsresult GetActionURL(nsIURI** aActionURL, Element* aOriginatingElement);
+
+ // Returns a number for this form that is unique within its owner document.
+ // This is used by nsContentUtils::GenerateStateKey to identify form controls
+ // that are inserted into the document by the parser.
+ int32_t GetFormNumberForStateKey();
+
+ /**
+ * Called when we have been cloned and adopted, and the information of the
+ * node has been changed.
+ */
+ void NodeInfoChanged(Document* aOldDoc) override;
+
+ protected:
+ //
+ // Data members
+ //
+ /** The list of controls (form.elements as well as stuff not in elements) */
+ RefPtr<HTMLFormControlsCollection> mControls;
+
+ /** The pending submission object */
+ UniquePtr<HTMLFormSubmission> mPendingSubmission;
+
+ /** The target browsing context, if any. */
+ RefPtr<BrowsingContext> mTargetContext;
+ /** The load identifier for the pending request created for a
+ * submit, used to be able to block double submits. */
+ Maybe<uint64_t> mCurrentLoadId;
+
+ /** The default submit element -- WEAK */
+ nsGenericHTMLFormElement* mDefaultSubmitElement;
+
+ /** The first submit element in mElements -- WEAK */
+ nsGenericHTMLFormElement* mFirstSubmitInElements;
+
+ /** The first submit element in mNotInElements -- WEAK */
+ nsGenericHTMLFormElement* mFirstSubmitNotInElements;
+
+ // This array holds on to all HTMLImageElement(s).
+ // This is needed to properly clean up the bi-directional references
+ // (both weak and strong) between the form and its HTMLImageElements.
+
+ // Holds WEAK references
+ TreeOrderedArray<HTMLImageElement*> mImageElements;
+
+ // A map from an ID or NAME attribute to the HTMLImageElement(s), this
+ // hash holds strong references either to the named HTMLImageElement, or
+ // to a list of named HTMLImageElement(s), in the case where this hash
+ // holds on to a list of named HTMLImageElement(s) the list has weak
+ // references to the HTMLImageElement.
+
+ nsInterfaceHashtable<nsStringHashKey, nsISupports> mImageNameLookupTable;
+
+ // A map from names to elements that were gotten by those names from this
+ // form in that past. See "past names map" in the HTML5 specification.
+
+ nsInterfaceHashtable<nsStringHashKey, nsISupports> mPastNameLookupTable;
+
+ /** Keep track of what the popup state was when the submit was initiated */
+ PopupBlocker::PopupControlState mSubmitPopupState;
+
+ RefPtr<nsDOMTokenList> mRelList;
+
+ /**
+ * Number of invalid and candidate for constraint validation elements in the
+ * form the last time UpdateValidity has been called.
+ */
+ int32_t mInvalidElementsCount;
+
+ // See GetFormNumberForStateKey.
+ int32_t mFormNumber;
+
+ /** Whether we are currently processing a submit event or not */
+ bool mGeneratingSubmit;
+ /** Whether we are currently processing a reset event or not */
+ bool mGeneratingReset;
+ /** Whether the submission is to be deferred in case a script triggers it */
+ bool mDeferSubmission;
+ /** Whether we notified NS_FORMSUBMIT_SUBJECT listeners already */
+ bool mNotifiedObservers;
+ /** If we notified the listeners early, what was the result? */
+ bool mNotifiedObserversResult;
+ /**
+ * Whether the submission of this form has been ever prevented because of
+ * being invalid.
+ */
+ bool mEverTriedInvalidSubmit;
+ /** Whether we are constructing entry list */
+ bool mIsConstructingEntryList;
+ /** Whether we are firing submission event */
+ bool mIsFiringSubmissionEvents;
+
+ private:
+ bool IsSubmitting() const;
+
+ void SetDefaultSubmitElement(nsGenericHTMLFormElement*);
+
+ NotNull<const Encoding*> GetSubmitEncoding();
+
+ /**
+ * Fire an event when the form is removed from the DOM tree. This is now only
+ * used by the password manager and formautofill.
+ */
+ void MaybeFireFormRemoved();
+
+ MOZ_CAN_RUN_SCRIPT
+ void ReportInvalidUnfocusableElements();
+
+ ~HTMLFormElement();
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif // mozilla_dom_HTMLFormElement_h
diff --git a/dom/html/HTMLFormSubmission.cpp b/dom/html/HTMLFormSubmission.cpp
new file mode 100644
index 0000000000..fa25794274
--- /dev/null
+++ b/dom/html/HTMLFormSubmission.cpp
@@ -0,0 +1,881 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLFormSubmission.h"
+#include "HTMLFormElement.h"
+#include "HTMLFormSubmissionConstants.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsGkAtoms.h"
+#include "nsIFormControl.h"
+#include "nsError.h"
+#include "nsGenericHTMLElement.h"
+#include "nsAttrValueInlines.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsStringStream.h"
+#include "nsIURI.h"
+#include "nsIURIMutator.h"
+#include "nsIURL.h"
+#include "nsNetUtil.h"
+#include "nsLinebreakConverter.h"
+#include "nsEscape.h"
+#include "nsUnicharUtils.h"
+#include "nsIMultiplexInputStream.h"
+#include "nsIMIMEInputStream.h"
+#include "nsIScriptError.h"
+#include "nsCExternalHandlerService.h"
+#include "nsContentUtils.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/RandomNum.h"
+
+#include <tuple>
+
+namespace mozilla::dom {
+
+namespace {
+
+void SendJSWarning(Document* aDocument, const char* aWarningName,
+ const nsTArray<nsString>& aWarningArgs) {
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "HTML"_ns,
+ aDocument, nsContentUtils::eFORMS_PROPERTIES,
+ aWarningName, aWarningArgs);
+}
+
+void RetrieveFileName(Blob* aBlob, nsAString& aFilename) {
+ if (!aBlob) {
+ return;
+ }
+
+ RefPtr<File> file = aBlob->ToFile();
+ if (file) {
+ file->GetName(aFilename);
+ }
+}
+
+void RetrieveDirectoryName(Directory* aDirectory, nsAString& aDirname) {
+ MOZ_ASSERT(aDirectory);
+
+ ErrorResult rv;
+ aDirectory->GetName(aDirname, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ aDirname.Truncate();
+ }
+}
+
+// --------------------------------------------------------------------------
+
+class FSURLEncoded : public EncodingFormSubmission {
+ public:
+ /**
+ * @param aEncoding the character encoding of the form
+ * @param aMethod the method of the submit (either NS_FORM_METHOD_GET or
+ * NS_FORM_METHOD_POST).
+ */
+ FSURLEncoded(nsIURI* aActionURL, const nsAString& aTarget,
+ NotNull<const Encoding*> aEncoding, int32_t aMethod,
+ Document* aDocument, Element* aSubmitter)
+ : EncodingFormSubmission(aActionURL, aTarget, aEncoding, aSubmitter),
+ mMethod(aMethod),
+ mDocument(aDocument),
+ mWarnedFileControl(false) {}
+
+ virtual nsresult AddNameValuePair(const nsAString& aName,
+ const nsAString& aValue) override;
+
+ virtual nsresult AddNameBlobPair(const nsAString& aName,
+ Blob* aBlob) override;
+
+ virtual nsresult AddNameDirectoryPair(const nsAString& aName,
+ Directory* aDirectory) override;
+
+ virtual nsresult GetEncodedSubmission(nsIURI* aURI,
+ nsIInputStream** aPostDataStream,
+ nsCOMPtr<nsIURI>& aOutURI) override;
+
+ protected:
+ /**
+ * URL encode a Unicode string by encoding it to bytes, converting linebreaks
+ * properly, and then escaping many bytes as %xx.
+ *
+ * @param aStr the string to encode
+ * @param aEncoded the encoded string [OUT]
+ * @throws NS_ERROR_OUT_OF_MEMORY if we run out of memory
+ */
+ nsresult URLEncode(const nsAString& aStr, nsACString& aEncoded);
+
+ private:
+ /**
+ * The method of the submit (either NS_FORM_METHOD_GET or
+ * NS_FORM_METHOD_POST).
+ */
+ int32_t mMethod;
+
+ /** The query string so far (the part after the ?) */
+ nsCString mQueryString;
+
+ /** The document whose URI to use when reporting errors */
+ nsCOMPtr<Document> mDocument;
+
+ /** Whether or not we have warned about a file control not being submitted */
+ bool mWarnedFileControl;
+};
+
+nsresult FSURLEncoded::AddNameValuePair(const nsAString& aName,
+ const nsAString& aValue) {
+ // Encode value
+ nsCString convValue;
+ nsresult rv = URLEncode(aValue, convValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Encode name
+ nsAutoCString convName;
+ rv = URLEncode(aName, convName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Append data to string
+ if (mQueryString.IsEmpty()) {
+ mQueryString += convName + "="_ns + convValue;
+ } else {
+ mQueryString += "&"_ns + convName + "="_ns + convValue;
+ }
+
+ return NS_OK;
+}
+
+nsresult FSURLEncoded::AddNameBlobPair(const nsAString& aName, Blob* aBlob) {
+ if (!mWarnedFileControl) {
+ SendJSWarning(mDocument, "ForgotFileEnctypeWarning", nsTArray<nsString>());
+ mWarnedFileControl = true;
+ }
+
+ nsAutoString filename;
+ RetrieveFileName(aBlob, filename);
+ return AddNameValuePair(aName, filename);
+}
+
+nsresult FSURLEncoded::AddNameDirectoryPair(const nsAString& aName,
+ Directory* aDirectory) {
+ // No warning about because Directory objects are never sent via form.
+
+ nsAutoString dirname;
+ RetrieveDirectoryName(aDirectory, dirname);
+ return AddNameValuePair(aName, dirname);
+}
+
+void HandleMailtoSubject(nsCString& aPath) {
+ // Walk through the string and see if we have a subject already.
+ bool hasSubject = false;
+ bool hasParams = false;
+ int32_t paramSep = aPath.FindChar('?');
+ while (paramSep != kNotFound && paramSep < (int32_t)aPath.Length()) {
+ hasParams = true;
+
+ // Get the end of the name at the = op. If it is *after* the next &,
+ // assume that someone made a parameter without an = in it
+ int32_t nameEnd = aPath.FindChar('=', paramSep + 1);
+ int32_t nextParamSep = aPath.FindChar('&', paramSep + 1);
+ if (nextParamSep == kNotFound) {
+ nextParamSep = aPath.Length();
+ }
+
+ // If the = op is after the &, this parameter is a name without value.
+ // If there is no = op, same thing.
+ if (nameEnd == kNotFound || nextParamSep < nameEnd) {
+ nameEnd = nextParamSep;
+ }
+
+ if (nameEnd != kNotFound) {
+ if (Substring(aPath, paramSep + 1, nameEnd - (paramSep + 1))
+ .LowerCaseEqualsLiteral("subject")) {
+ hasSubject = true;
+ break;
+ }
+ }
+
+ paramSep = nextParamSep;
+ }
+
+ // If there is no subject, append a preformed subject to the mailto line
+ if (!hasSubject) {
+ if (hasParams) {
+ aPath.Append('&');
+ } else {
+ aPath.Append('?');
+ }
+
+ // Get the default subject
+ nsAutoString brandName;
+ nsresult rv = nsContentUtils::GetLocalizedString(
+ nsContentUtils::eBRAND_PROPERTIES, "brandShortName", brandName);
+ if (NS_FAILED(rv)) return;
+ nsAutoString subjectStr;
+ rv = nsContentUtils::FormatLocalizedString(
+ subjectStr, nsContentUtils::eFORMS_PROPERTIES, "DefaultFormSubject",
+ brandName);
+ if (NS_FAILED(rv)) return;
+ aPath.AppendLiteral("subject=");
+ nsCString subjectStrEscaped;
+ rv = NS_EscapeURL(NS_ConvertUTF16toUTF8(subjectStr), esc_Query,
+ subjectStrEscaped, mozilla::fallible);
+ if (NS_FAILED(rv)) return;
+
+ aPath.Append(subjectStrEscaped);
+ }
+}
+
+nsresult FSURLEncoded::GetEncodedSubmission(nsIURI* aURI,
+ nsIInputStream** aPostDataStream,
+ nsCOMPtr<nsIURI>& aOutURI) {
+ nsresult rv = NS_OK;
+ aOutURI = aURI;
+
+ *aPostDataStream = nullptr;
+
+ if (mMethod == NS_FORM_METHOD_POST) {
+ if (aURI->SchemeIs("mailto")) {
+ nsAutoCString path;
+ rv = aURI->GetPathQueryRef(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ HandleMailtoSubject(path);
+
+ // Append the body to and force-plain-text args to the mailto line
+ nsAutoCString escapedBody;
+ if (NS_WARN_IF(!NS_Escape(mQueryString, escapedBody, url_XAlphas))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ path += "&force-plain-text=Y&body="_ns + escapedBody;
+
+ return NS_MutateURI(aURI).SetPathQueryRef(path).Finalize(aOutURI);
+ } else {
+ nsCOMPtr<nsIInputStream> dataStream;
+ rv = NS_NewCStringInputStream(getter_AddRefs(dataStream),
+ std::move(mQueryString));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mQueryString.Truncate();
+
+ nsCOMPtr<nsIMIMEInputStream> mimeStream(
+ do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mimeStream->AddHeader("Content-Type",
+ "application/x-www-form-urlencoded");
+ mimeStream->SetData(dataStream);
+
+ mimeStream.forget(aPostDataStream);
+ }
+
+ } else {
+ // Get the full query string
+ if (aURI->SchemeIs("javascript")) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
+ if (url) {
+ // Make sure that we end up with a query component in the URL. If
+ // mQueryString is empty, nsIURI::SetQuery() will remove the query
+ // component, which is not what we want.
+ rv = NS_MutateURI(aURI)
+ .SetQuery(mQueryString.IsEmpty() ? "?"_ns : mQueryString)
+ .Finalize(aOutURI);
+ } else {
+ nsAutoCString path;
+ rv = aURI->GetPathQueryRef(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Bug 42616: Trim off named anchor and save it to add later
+ int32_t namedAnchorPos = path.FindChar('#');
+ nsAutoCString namedAnchor;
+ if (kNotFound != namedAnchorPos) {
+ path.Right(namedAnchor, (path.Length() - namedAnchorPos));
+ path.Truncate(namedAnchorPos);
+ }
+
+ // Chop off old query string (bug 25330, 57333)
+ // Only do this for GET not POST (bug 41585)
+ int32_t queryStart = path.FindChar('?');
+ if (kNotFound != queryStart) {
+ path.Truncate(queryStart);
+ }
+
+ path.Append('?');
+ // Bug 42616: Add named anchor to end after query string
+ path.Append(mQueryString + namedAnchor);
+
+ rv = NS_MutateURI(aURI).SetPathQueryRef(path).Finalize(aOutURI);
+ }
+ }
+
+ return rv;
+}
+
+// i18n helper routines
+nsresult FSURLEncoded::URLEncode(const nsAString& aStr, nsACString& aEncoded) {
+ nsAutoCString encodedBuf;
+ // We encode with eValueEncode because the urlencoded format needs the newline
+ // normalizations but percent-escapes characters that eNameEncode doesn't,
+ // so calling NS_Escape would still be needed.
+ nsresult rv = EncodeVal(aStr, encodedBuf, EncodeType::eValueEncode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (NS_WARN_IF(!NS_Escape(encodedBuf, aEncoded, url_XPAlphas))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+// --------------------------------------------------------------------------
+
+FSMultipartFormData::FSMultipartFormData(nsIURI* aActionURL,
+ const nsAString& aTarget,
+ NotNull<const Encoding*> aEncoding,
+ Element* aSubmitter)
+ : EncodingFormSubmission(aActionURL, aTarget, aEncoding, aSubmitter) {
+ mPostData = do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1");
+
+ nsCOMPtr<nsIInputStream> inputStream = do_QueryInterface(mPostData);
+ MOZ_ASSERT(SameCOMIdentity(mPostData, inputStream));
+ mPostDataStream = inputStream;
+
+ mTotalLength = 0;
+
+ mBoundary.AssignLiteral("---------------------------");
+ mBoundary.AppendInt(static_cast<uint32_t>(mozilla::RandomUint64OrDie()));
+ mBoundary.AppendInt(static_cast<uint32_t>(mozilla::RandomUint64OrDie()));
+ mBoundary.AppendInt(static_cast<uint32_t>(mozilla::RandomUint64OrDie()));
+}
+
+FSMultipartFormData::~FSMultipartFormData() {
+ NS_ASSERTION(mPostDataChunk.IsEmpty(), "Left unsubmitted data");
+}
+
+nsIInputStream* FSMultipartFormData::GetSubmissionBody(
+ uint64_t* aContentLength) {
+ // Finish data
+ mPostDataChunk += "--"_ns + mBoundary + nsLiteralCString("--" CRLF);
+
+ // Add final data input stream
+ AddPostDataStream();
+
+ *aContentLength = mTotalLength;
+ return mPostDataStream;
+}
+
+nsresult FSMultipartFormData::AddNameValuePair(const nsAString& aName,
+ const nsAString& aValue) {
+ nsAutoCString encodedVal;
+ nsresult rv = EncodeVal(aValue, encodedVal, EncodeType::eValueEncode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString nameStr;
+ rv = EncodeVal(aName, nameStr, EncodeType::eNameEncode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Make MIME block for name/value pair
+
+ mPostDataChunk += "--"_ns + mBoundary + nsLiteralCString(CRLF) +
+ "Content-Disposition: form-data; name=\""_ns + nameStr +
+ nsLiteralCString("\"" CRLF CRLF) + encodedVal +
+ nsLiteralCString(CRLF);
+
+ return NS_OK;
+}
+
+nsresult FSMultipartFormData::AddNameBlobPair(const nsAString& aName,
+ Blob* aBlob) {
+ MOZ_ASSERT(aBlob);
+
+ // Encode the control name
+ nsAutoCString nameStr;
+ nsresult rv = EncodeVal(aName, nameStr, EncodeType::eNameEncode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ErrorResult error;
+
+ uint64_t size = 0;
+ nsAutoCString filename;
+ nsAutoCString contentType;
+ nsCOMPtr<nsIInputStream> fileStream;
+ nsAutoString filename16;
+
+ RefPtr<File> file = aBlob->ToFile();
+ if (file) {
+ nsAutoString relativePath;
+ file->GetRelativePath(relativePath);
+ if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ !relativePath.IsEmpty()) {
+ filename16 = relativePath;
+ }
+
+ if (filename16.IsEmpty()) {
+ RetrieveFileName(aBlob, filename16);
+ }
+ }
+
+ rv = EncodeVal(filename16, filename, EncodeType::eFilenameEncode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get content type
+ nsAutoString contentType16;
+ aBlob->GetType(contentType16);
+ if (contentType16.IsEmpty()) {
+ contentType16.AssignLiteral("application/octet-stream");
+ }
+
+ NS_ConvertUTF16toUTF8 contentType8(contentType16);
+ int32_t convertedBufLength = 0;
+ char* convertedBuf = nsLinebreakConverter::ConvertLineBreaks(
+ contentType8.get(), nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakSpace, contentType8.Length(),
+ &convertedBufLength);
+ contentType.Adopt(convertedBuf, convertedBufLength);
+
+ // Get input stream
+ aBlob->CreateInputStream(getter_AddRefs(fileStream), error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+
+ // Get size
+ size = aBlob->GetSize(error);
+ if (error.Failed()) {
+ error.SuppressException();
+ fileStream = nullptr;
+ }
+
+ if (fileStream) {
+ // Create buffered stream (for efficiency)
+ nsCOMPtr<nsIInputStream> bufferedStream;
+ rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream),
+ fileStream.forget(), 8192);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ fileStream = bufferedStream;
+ }
+
+ AddDataChunk(nameStr, filename, contentType, fileStream, size);
+ return NS_OK;
+}
+
+nsresult FSMultipartFormData::AddNameDirectoryPair(const nsAString& aName,
+ Directory* aDirectory) {
+ if (!StaticPrefs::dom_webkitBlink_dirPicker_enabled()) {
+ return NS_OK;
+ }
+
+ // Encode the control name
+ nsAutoCString nameStr;
+ nsresult rv = EncodeVal(aName, nameStr, EncodeType::eNameEncode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString dirname;
+ nsAutoString dirname16;
+
+ ErrorResult error;
+ nsAutoString path;
+ aDirectory->GetPath(path, error);
+ if (NS_WARN_IF(error.Failed())) {
+ error.SuppressException();
+ } else {
+ dirname16 = path;
+ }
+
+ if (dirname16.IsEmpty()) {
+ RetrieveDirectoryName(aDirectory, dirname16);
+ }
+
+ rv = EncodeVal(dirname16, dirname, EncodeType::eFilenameEncode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AddDataChunk(nameStr, dirname, "application/octet-stream"_ns, nullptr, 0);
+ return NS_OK;
+}
+
+void FSMultipartFormData::AddDataChunk(const nsACString& aName,
+ const nsACString& aFilename,
+ const nsACString& aContentType,
+ nsIInputStream* aInputStream,
+ uint64_t aInputStreamSize) {
+ //
+ // Make MIME block for name/value pair
+ //
+ // more appropriate than always using binary?
+ mPostDataChunk += "--"_ns + mBoundary + nsLiteralCString(CRLF);
+ mPostDataChunk += "Content-Disposition: form-data; name=\""_ns + aName +
+ "\"; filename=\""_ns + aFilename +
+ nsLiteralCString("\"" CRLF) + "Content-Type: "_ns +
+ aContentType + nsLiteralCString(CRLF CRLF);
+
+ // We should not try to append an invalid stream. That will happen for example
+ // if we try to update a file that actually do not exist.
+ if (aInputStream) {
+ // We need to dump the data up to this point into the POST data stream
+ // here, since we're about to add the file input stream
+ AddPostDataStream();
+
+ mPostData->AppendStream(aInputStream);
+ mTotalLength += aInputStreamSize;
+ }
+
+ // CRLF after file
+ mPostDataChunk.AppendLiteral(CRLF);
+}
+
+nsresult FSMultipartFormData::GetEncodedSubmission(
+ nsIURI* aURI, nsIInputStream** aPostDataStream, nsCOMPtr<nsIURI>& aOutURI) {
+ nsresult rv;
+ aOutURI = aURI;
+
+ // Make header
+ nsCOMPtr<nsIMIMEInputStream> mimeStream =
+ do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString contentType;
+ GetContentType(contentType);
+ mimeStream->AddHeader("Content-Type", contentType.get());
+
+ uint64_t bodySize;
+ mimeStream->SetData(GetSubmissionBody(&bodySize));
+
+ mimeStream.forget(aPostDataStream);
+
+ return NS_OK;
+}
+
+nsresult FSMultipartFormData::AddPostDataStream() {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIInputStream> postDataChunkStream;
+ rv = NS_NewCStringInputStream(getter_AddRefs(postDataChunkStream),
+ mPostDataChunk);
+ NS_ASSERTION(postDataChunkStream, "Could not open a stream for POST!");
+ if (postDataChunkStream) {
+ mPostData->AppendStream(postDataChunkStream);
+ mTotalLength += mPostDataChunk.Length();
+ }
+
+ mPostDataChunk.Truncate();
+
+ return rv;
+}
+
+// --------------------------------------------------------------------------
+
+namespace {
+
+class FSTextPlain : public EncodingFormSubmission {
+ public:
+ FSTextPlain(nsIURI* aActionURL, const nsAString& aTarget,
+ NotNull<const Encoding*> aEncoding, Element* aSubmitter)
+ : EncodingFormSubmission(aActionURL, aTarget, aEncoding, aSubmitter) {}
+
+ virtual nsresult AddNameValuePair(const nsAString& aName,
+ const nsAString& aValue) override;
+
+ virtual nsresult AddNameBlobPair(const nsAString& aName,
+ Blob* aBlob) override;
+
+ virtual nsresult AddNameDirectoryPair(const nsAString& aName,
+ Directory* aDirectory) override;
+
+ virtual nsresult GetEncodedSubmission(nsIURI* aURI,
+ nsIInputStream** aPostDataStream,
+ nsCOMPtr<nsIURI>& aOutURI) override;
+
+ private:
+ nsString mBody;
+};
+
+nsresult FSTextPlain::AddNameValuePair(const nsAString& aName,
+ const nsAString& aValue) {
+ // XXX This won't work well with a name like "a=b" or "a\nb" but I suppose
+ // text/plain doesn't care about that. Parsers aren't built for escaped
+ // values so we'll have to live with it.
+ mBody.Append(aName + u"="_ns + aValue + NS_LITERAL_STRING_FROM_CSTRING(CRLF));
+
+ return NS_OK;
+}
+
+nsresult FSTextPlain::AddNameBlobPair(const nsAString& aName, Blob* aBlob) {
+ nsAutoString filename;
+ RetrieveFileName(aBlob, filename);
+ AddNameValuePair(aName, filename);
+ return NS_OK;
+}
+
+nsresult FSTextPlain::AddNameDirectoryPair(const nsAString& aName,
+ Directory* aDirectory) {
+ nsAutoString dirname;
+ RetrieveDirectoryName(aDirectory, dirname);
+ AddNameValuePair(aName, dirname);
+ return NS_OK;
+}
+
+nsresult FSTextPlain::GetEncodedSubmission(nsIURI* aURI,
+ nsIInputStream** aPostDataStream,
+ nsCOMPtr<nsIURI>& aOutURI) {
+ nsresult rv = NS_OK;
+ aOutURI = aURI;
+
+ *aPostDataStream = nullptr;
+
+ // XXX HACK We are using the standard URL mechanism to give the body to the
+ // mailer instead of passing the post data stream to it, since that sounds
+ // hard.
+ if (aURI->SchemeIs("mailto")) {
+ nsAutoCString path;
+ rv = aURI->GetPathQueryRef(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ HandleMailtoSubject(path);
+
+ // Append the body to and force-plain-text args to the mailto line
+ nsAutoCString escapedBody;
+ if (NS_WARN_IF(!NS_Escape(NS_ConvertUTF16toUTF8(mBody), escapedBody,
+ url_XAlphas))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ path += "&force-plain-text=Y&body="_ns + escapedBody;
+
+ rv = NS_MutateURI(aURI).SetPathQueryRef(path).Finalize(aOutURI);
+ } else {
+ // Create data stream.
+ // We use eValueEncode to send the data through the charset encoder and to
+ // normalize linebreaks to use the "standard net" format (\r\n), but not
+ // perform any other escaping. This means that names and values which
+ // contain '=' or newlines are potentially ambiguously encoded, but that is
+ // how text/plain is specced.
+ nsCString cbody;
+ EncodeVal(mBody, cbody, EncodeType::eValueEncode);
+
+ nsCOMPtr<nsIInputStream> bodyStream;
+ rv = NS_NewCStringInputStream(getter_AddRefs(bodyStream), std::move(cbody));
+ if (!bodyStream) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Create mime stream with headers and such
+ nsCOMPtr<nsIMIMEInputStream> mimeStream =
+ do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mimeStream->AddHeader("Content-Type", "text/plain");
+ mimeStream->SetData(bodyStream);
+ mimeStream.forget(aPostDataStream);
+ }
+
+ return rv;
+}
+
+} // anonymous namespace
+
+// --------------------------------------------------------------------------
+
+HTMLFormSubmission::HTMLFormSubmission(
+ nsIURI* aActionURL, const nsAString& aTarget,
+ mozilla::NotNull<const mozilla::Encoding*> aEncoding)
+ : mActionURL(aActionURL),
+ mTarget(aTarget),
+ mEncoding(aEncoding),
+ mInitiatedFromUserInput(UserActivation::IsHandlingUserInput()) {
+ MOZ_COUNT_CTOR(HTMLFormSubmission);
+}
+
+EncodingFormSubmission::EncodingFormSubmission(
+ nsIURI* aActionURL, const nsAString& aTarget,
+ NotNull<const Encoding*> aEncoding, Element* aSubmitter)
+ : HTMLFormSubmission(aActionURL, aTarget, aEncoding) {
+ if (!aEncoding->CanEncodeEverything()) {
+ nsAutoCString name;
+ aEncoding->Name(name);
+ AutoTArray<nsString, 1> args;
+ CopyUTF8toUTF16(name, *args.AppendElement());
+ SendJSWarning(aSubmitter ? aSubmitter->GetOwnerDocument() : nullptr,
+ "CannotEncodeAllUnicode", args);
+ }
+}
+
+EncodingFormSubmission::~EncodingFormSubmission() = default;
+
+// i18n helper routines
+nsresult EncodingFormSubmission::EncodeVal(const nsAString& aStr,
+ nsCString& aOut,
+ EncodeType aEncodeType) {
+ nsresult rv;
+ std::tie(rv, std::ignore) = mEncoding->Encode(aStr, aOut);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (aEncodeType != EncodeType::eFilenameEncode) {
+ // Normalize newlines
+ int32_t convertedBufLength = 0;
+ char* convertedBuf = nsLinebreakConverter::ConvertLineBreaks(
+ aOut.get(), nsLinebreakConverter::eLinebreakAny,
+ nsLinebreakConverter::eLinebreakNet, (int32_t)aOut.Length(),
+ &convertedBufLength);
+ aOut.Adopt(convertedBuf, convertedBufLength);
+ }
+
+ if (aEncodeType != EncodeType::eValueEncode) {
+ // Percent-escape LF, CR and double quotes.
+ int32_t offset = 0;
+ while ((offset = aOut.FindCharInSet("\n\r\"", offset)) != kNotFound) {
+ if (aOut[offset] == '\n') {
+ aOut.ReplaceLiteral(offset, 1, "%0A");
+ } else if (aOut[offset] == '\r') {
+ aOut.ReplaceLiteral(offset, 1, "%0D");
+ } else if (aOut[offset] == '"') {
+ aOut.ReplaceLiteral(offset, 1, "%22");
+ } else {
+ MOZ_ASSERT(false);
+ offset++;
+ continue;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// --------------------------------------------------------------------------
+
+namespace {
+
+void GetEnumAttr(nsGenericHTMLElement* aContent, nsAtom* atom,
+ int32_t* aValue) {
+ const nsAttrValue* value = aContent->GetParsedAttr(atom);
+ if (value && value->Type() == nsAttrValue::eEnum) {
+ *aValue = value->GetEnumValue();
+ }
+}
+
+} // anonymous namespace
+
+/* static */
+nsresult HTMLFormSubmission::GetFromForm(HTMLFormElement* aForm,
+ nsGenericHTMLElement* aSubmitter,
+ NotNull<const Encoding*>& aEncoding,
+ HTMLFormSubmission** aFormSubmission) {
+ // Get all the information necessary to encode the form data
+ NS_ASSERTION(aForm->GetComposedDoc(),
+ "Should have doc if we're building submission!");
+
+ nsresult rv;
+
+ // Get method (default: GET)
+ int32_t method = NS_FORM_METHOD_GET;
+ if (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formmethod)) {
+ GetEnumAttr(aSubmitter, nsGkAtoms::formmethod, &method);
+ } else {
+ GetEnumAttr(aForm, nsGkAtoms::method, &method);
+ }
+
+ if (method == NS_FORM_METHOD_DIALOG) {
+ HTMLDialogElement* dialog = aForm->FirstAncestorOfType<HTMLDialogElement>();
+
+ // If there isn't one, do nothing.
+ if (!dialog) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoString result;
+ if (aSubmitter) {
+ aSubmitter->ResultForDialogSubmit(result);
+ }
+ *aFormSubmission = new DialogFormSubmission(result, aEncoding, dialog);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(method != NS_FORM_METHOD_DIALOG);
+
+ // Get action
+ nsCOMPtr<nsIURI> actionURL;
+ rv = aForm->GetActionURL(getter_AddRefs(actionURL), aSubmitter);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check if CSP allows this form-action
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aForm->GetCsp();
+ if (csp) {
+ bool permitsFormAction = true;
+
+ // form-action is only enforced if explicitly defined in the
+ // policy - do *not* consult default-src, see:
+ // http://www.w3.org/TR/CSP2/#directive-default-src
+ rv = csp->Permits(aForm, nullptr /* nsICSPEventListener */, actionURL,
+ nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE,
+ true /* aSpecific */, true /* aSendViolationReports */,
+ &permitsFormAction);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!permitsFormAction) {
+ return NS_ERROR_CSP_FORM_ACTION_VIOLATION;
+ }
+ }
+
+ // Get target
+ // The target is the submitter element formtarget attribute if the element
+ // is a submit control and has such an attribute.
+ // Otherwise, the target is the form owner's target attribute,
+ // if it has such an attribute.
+ // Finally, if one of the child nodes of the head element is a base element
+ // with a target attribute, then the value of the target attribute of the
+ // first such base element; or, if there is no such element, the empty string.
+ nsAutoString target;
+ if (!(aSubmitter && aSubmitter->GetAttr(nsGkAtoms::formtarget, target)) &&
+ !aForm->GetAttr(nsGkAtoms::target, target)) {
+ aForm->GetBaseTarget(target);
+ }
+
+ // Get encoding type (default: urlencoded)
+ int32_t enctype = NS_FORM_ENCTYPE_URLENCODED;
+ if (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formenctype)) {
+ GetEnumAttr(aSubmitter, nsGkAtoms::formenctype, &enctype);
+ } else {
+ GetEnumAttr(aForm, nsGkAtoms::enctype, &enctype);
+ }
+
+ // Choose encoder
+ if (method == NS_FORM_METHOD_POST && enctype == NS_FORM_ENCTYPE_MULTIPART) {
+ *aFormSubmission =
+ new FSMultipartFormData(actionURL, target, aEncoding, aSubmitter);
+ } else if (method == NS_FORM_METHOD_POST &&
+ enctype == NS_FORM_ENCTYPE_TEXTPLAIN) {
+ *aFormSubmission =
+ new FSTextPlain(actionURL, target, aEncoding, aSubmitter);
+ } else {
+ Document* doc = aForm->OwnerDoc();
+ if (enctype == NS_FORM_ENCTYPE_MULTIPART ||
+ enctype == NS_FORM_ENCTYPE_TEXTPLAIN) {
+ AutoTArray<nsString, 1> args;
+ nsString& enctypeStr = *args.AppendElement();
+ if (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formenctype)) {
+ aSubmitter->GetAttr(nsGkAtoms::formenctype, enctypeStr);
+ } else {
+ aForm->GetAttr(nsGkAtoms::enctype, enctypeStr);
+ }
+
+ SendJSWarning(doc, "ForgotPostWarning", args);
+ }
+ *aFormSubmission =
+ new FSURLEncoded(actionURL, target, aEncoding, method, doc, aSubmitter);
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLFormSubmission.h b/dom/html/HTMLFormSubmission.h
new file mode 100644
index 0000000000..7e4d3a0e4e
--- /dev/null
+++ b/dom/html/HTMLFormSubmission.h
@@ -0,0 +1,291 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLFormSubmission_h
+#define mozilla_dom_HTMLFormSubmission_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/HTMLDialogElement.h"
+#include "nsCOMPtr.h"
+#include "mozilla/Encoding.h"
+#include "nsString.h"
+
+class nsIURI;
+class nsIInputStream;
+class nsGenericHTMLElement;
+class nsIMultiplexInputStream;
+
+namespace mozilla::dom {
+
+class Blob;
+class DialogFormSubmission;
+class Directory;
+class Element;
+class HTMLFormElement;
+
+/**
+ * Class for form submissions; encompasses the function to call to submit as
+ * well as the form submission name/value pairs
+ */
+class HTMLFormSubmission {
+ public:
+ /**
+ * Get a submission object based on attributes in the form (ENCTYPE and
+ * METHOD)
+ *
+ * @param aForm the form to get a submission object based on
+ * @param aSubmitter the submitter element (can be null)
+ * @param aEncoding the submiter element's encoding
+ * @param aFormSubmission the form submission object (out param)
+ */
+ static nsresult GetFromForm(HTMLFormElement* aForm,
+ nsGenericHTMLElement* aSubmitter,
+ NotNull<const Encoding*>& aEncoding,
+ HTMLFormSubmission** aFormSubmission);
+
+ MOZ_COUNTED_DTOR_VIRTUAL(HTMLFormSubmission)
+
+ /**
+ * Submit a name/value pair
+ *
+ * @param aName the name of the parameter
+ * @param aValue the value of the parameter
+ */
+ virtual nsresult AddNameValuePair(const nsAString& aName,
+ const nsAString& aValue) = 0;
+
+ /**
+ * Submit a name/blob pair
+ *
+ * @param aName the name of the parameter
+ * @param aBlob the blob to submit. The file's name will be used if the Blob
+ * is actually a File, otherwise 'blob' string is used instead. Must not be
+ * null.
+ */
+ virtual nsresult AddNameBlobPair(const nsAString& aName, Blob* aBlob) = 0;
+
+ /**
+ * Submit a name/directory pair
+ *
+ * @param aName the name of the parameter
+ * @param aBlob the directory to submit.
+ */
+ virtual nsresult AddNameDirectoryPair(const nsAString& aName,
+ Directory* aDirectory) = 0;
+
+ /**
+ * Given a URI and the current submission, create the final URI and data
+ * stream that will be submitted. Subclasses *must* implement this.
+ *
+ * @param aURI the URI being submitted to [IN]
+ * @param aPostDataStream a data stream for POST data [OUT]
+ * @param aOutURI the resulting URI. May be the same as aURI [OUT]
+ */
+ virtual nsresult GetEncodedSubmission(nsIURI* aURI,
+ nsIInputStream** aPostDataStream,
+ nsCOMPtr<nsIURI>& aOutURI) = 0;
+
+ /**
+ * Get the charset that will be used for submission.
+ */
+ void GetCharset(nsACString& aCharset) { mEncoding->Name(aCharset); }
+
+ /**
+ * Get the action URI that will be used for submission.
+ */
+ nsIURI* GetActionURL() const { return mActionURL; }
+
+ /**
+ * Get the target that will be used for submission.
+ */
+ void GetTarget(nsAString& aTarget) { aTarget = mTarget; }
+
+ /**
+ * Return true if this form submission was user-initiated.
+ */
+ bool IsInitiatedFromUserInput() const { return mInitiatedFromUserInput; }
+
+ virtual DialogFormSubmission* GetAsDialogSubmission() { return nullptr; }
+
+ protected:
+ /**
+ * Can only be constructed by subclasses.
+ *
+ * @param aEncoding the character encoding of the form
+ */
+ HTMLFormSubmission(nsIURI* aActionURL, const nsAString& aTarget,
+ mozilla::NotNull<const mozilla::Encoding*> aEncoding);
+
+ // The action url.
+ nsCOMPtr<nsIURI> mActionURL;
+
+ // The target.
+ nsString mTarget;
+
+ // The character encoding of this form submission
+ mozilla::NotNull<const mozilla::Encoding*> mEncoding;
+
+ // Keep track of whether this form submission was user-initiated or not
+ bool mInitiatedFromUserInput;
+};
+
+class EncodingFormSubmission : public HTMLFormSubmission {
+ public:
+ EncodingFormSubmission(nsIURI* aActionURL, const nsAString& aTarget,
+ mozilla::NotNull<const mozilla::Encoding*> aEncoding,
+ Element* aSubmitter);
+
+ virtual ~EncodingFormSubmission();
+
+ // Indicates the type of newline normalization and escaping to perform in
+ // `EncodeVal`, in addition to encoding the string into bytes.
+ enum EncodeType {
+ // Normalizes newlines to CRLF and then escapes for use in
+ // `Content-Disposition`. (Useful for `multipart/form-data` entry names.)
+ eNameEncode,
+ // Escapes for use in `Content-Disposition`. (Useful for
+ // `multipart/form-data` filenames.)
+ eFilenameEncode,
+ // Normalizes newlines to CRLF.
+ eValueEncode,
+ };
+
+ /**
+ * Encode a Unicode string to bytes, additionally performing escapes or
+ * normalizations.
+ * @param aStr the string to encode
+ * @param aOut the encoded string [OUT]
+ * @param aEncodeType The type of escapes or normalizations to perform on the
+ * encoded string.
+ * @throws an error if UnicodeToNewBytes fails
+ */
+ nsresult EncodeVal(const nsAString& aStr, nsCString& aOut,
+ EncodeType aEncodeType);
+};
+
+class DialogFormSubmission final : public HTMLFormSubmission {
+ public:
+ DialogFormSubmission(nsAString& aResult, NotNull<const Encoding*> aEncoding,
+ HTMLDialogElement* aDialogElement)
+ : HTMLFormSubmission(nullptr, u""_ns, aEncoding),
+ mDialogElement(aDialogElement),
+ mReturnValue(aResult) {}
+ nsresult AddNameValuePair(const nsAString& aName,
+ const nsAString& aValue) override {
+ MOZ_CRASH("This method should not be called");
+ return NS_OK;
+ }
+
+ nsresult AddNameBlobPair(const nsAString& aName, Blob* aBlob) override {
+ MOZ_CRASH("This method should not be called");
+ return NS_OK;
+ }
+
+ nsresult AddNameDirectoryPair(const nsAString& aName,
+ Directory* aDirectory) override {
+ MOZ_CRASH("This method should not be called");
+ return NS_OK;
+ }
+
+ nsresult GetEncodedSubmission(nsIURI* aURI, nsIInputStream** aPostDataStream,
+ nsCOMPtr<nsIURI>& aOutURI) override {
+ MOZ_CRASH("This method should not be called");
+ return NS_OK;
+ }
+
+ DialogFormSubmission* GetAsDialogSubmission() override { return this; }
+
+ HTMLDialogElement* DialogElement() { return mDialogElement; }
+
+ nsString& ReturnValue() { return mReturnValue; }
+
+ private:
+ const RefPtr<HTMLDialogElement> mDialogElement;
+ nsString mReturnValue;
+};
+
+/**
+ * Handle multipart/form-data encoding, which does files as well as normal
+ * inputs. This always does POST.
+ */
+class FSMultipartFormData : public EncodingFormSubmission {
+ public:
+ /**
+ * @param aEncoding the character encoding of the form
+ */
+ FSMultipartFormData(nsIURI* aActionURL, const nsAString& aTarget,
+ mozilla::NotNull<const mozilla::Encoding*> aEncoding,
+ Element* aSubmitter);
+ ~FSMultipartFormData();
+
+ virtual nsresult AddNameValuePair(const nsAString& aName,
+ const nsAString& aValue) override;
+
+ virtual nsresult AddNameBlobPair(const nsAString& aName,
+ Blob* aBlob) override;
+
+ virtual nsresult AddNameDirectoryPair(const nsAString& aName,
+ Directory* aDirectory) override;
+
+ virtual nsresult GetEncodedSubmission(nsIURI* aURI,
+ nsIInputStream** aPostDataStream,
+ nsCOMPtr<nsIURI>& aOutURI) override;
+
+ void GetContentType(nsACString& aContentType) {
+ aContentType = "multipart/form-data; boundary="_ns + mBoundary;
+ }
+
+ nsIInputStream* GetSubmissionBody(uint64_t* aContentLength);
+
+ protected:
+ /**
+ * Roll up the data we have so far and add it to the multiplexed data stream.
+ */
+ nsresult AddPostDataStream();
+
+ private:
+ void AddDataChunk(const nsACString& aName, const nsACString& aFilename,
+ const nsACString& aContentType,
+ nsIInputStream* aInputStream, uint64_t aInputStreamSize);
+ /**
+ * The post data stream as it is so far. This is a collection of smaller
+ * chunks--string streams and file streams interleaved to make one big POST
+ * stream.
+ */
+ nsCOMPtr<nsIMultiplexInputStream> mPostData;
+
+ /**
+ * The same stream, but as an nsIInputStream.
+ * Raw pointers because it is just QI of mInputStream.
+ */
+ nsIInputStream* mPostDataStream;
+
+ /**
+ * The current string chunk. When a file is hit, the string chunk gets
+ * wrapped up into an input stream and put into mPostDataStream so that the
+ * file input stream can then be appended and everything is in the right
+ * order. Then the string chunk gets appended to again as we process more
+ * name/value pairs.
+ */
+ nsCString mPostDataChunk;
+
+ /**
+ * The boundary string to use after each "part" (the boundary that marks the
+ * end of a value). This is computed randomly and is different for each
+ * submission.
+ */
+ nsCString mBoundary;
+
+ /**
+ * The total length in bytes of the streams that make up mPostDataStream
+ */
+ uint64_t mTotalLength;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLFormSubmission_h */
diff --git a/dom/html/HTMLFormSubmissionConstants.h b/dom/html/HTMLFormSubmissionConstants.h
new file mode 100644
index 0000000000..c6e2436472
--- /dev/null
+++ b/dom/html/HTMLFormSubmissionConstants.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLFormSubmissionConstants_h
+#define mozilla_dom_HTMLFormSubmissionConstants_h
+
+#define NS_FORM_METHOD_GET 0
+#define NS_FORM_METHOD_POST 1
+#define NS_FORM_METHOD_DIALOG 2
+#define NS_FORM_ENCTYPE_URLENCODED 0
+#define NS_FORM_ENCTYPE_MULTIPART 1
+#define NS_FORM_ENCTYPE_TEXTPLAIN 2
+
+static const nsAttrValue::EnumTable kFormMethodTable[] = {
+ {"get", NS_FORM_METHOD_GET},
+ {"post", NS_FORM_METHOD_POST},
+ {"dialog", NS_FORM_METHOD_DIALOG},
+ {nullptr, 0}};
+
+// Default method is 'get'.
+static const nsAttrValue::EnumTable* kFormDefaultMethod = &kFormMethodTable[0];
+
+static const nsAttrValue::EnumTable kFormEnctypeTable[] = {
+ {"multipart/form-data", NS_FORM_ENCTYPE_MULTIPART},
+ {"application/x-www-form-urlencoded", NS_FORM_ENCTYPE_URLENCODED},
+ {"text/plain", NS_FORM_ENCTYPE_TEXTPLAIN},
+ {nullptr, 0}};
+
+// Default method is 'application/x-www-form-urlencoded'.
+static const nsAttrValue::EnumTable* kFormDefaultEnctype =
+ &kFormEnctypeTable[1];
+
+#endif // mozilla_dom_HTMLFormSubmissionConstants_h
diff --git a/dom/html/HTMLFrameElement.cpp b/dom/html/HTMLFrameElement.cpp
new file mode 100644
index 0000000000..3e2f145e88
--- /dev/null
+++ b/dom/html/HTMLFrameElement.cpp
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLFrameElement.h"
+#include "mozilla/dom/HTMLFrameElementBinding.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Frame)
+
+namespace mozilla::dom {
+
+HTMLFrameElement::HTMLFrameElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : nsGenericHTMLFrameElement(std::move(aNodeInfo), aFromParser) {}
+
+HTMLFrameElement::~HTMLFrameElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLFrameElement)
+
+bool HTMLFrameElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::bordercolor) {
+ return aResult.ParseColor(aValue);
+ }
+ if (aAttribute == nsGkAtoms::frameborder) {
+ return ParseFrameborderValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::marginwidth) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::marginheight) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::scrolling) {
+ return ParseScrollingValue(aValue, aResult);
+ }
+ }
+
+ return nsGenericHTMLFrameElement::ParseAttribute(
+ aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult);
+}
+
+JSObject* HTMLFrameElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLFrameElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLFrameElement.h b/dom/html/HTMLFrameElement.h
new file mode 100644
index 0000000000..82e6d57b94
--- /dev/null
+++ b/dom/html/HTMLFrameElement.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLFrameElement_h
+#define mozilla_dom_HTMLFrameElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLFrameElement.h"
+#include "nsGkAtoms.h"
+
+namespace mozilla::dom {
+
+class HTMLFrameElement final : public nsGenericHTMLFrameElement {
+ public:
+ using nsGenericHTMLFrameElement::SwapFrameLoaders;
+
+ explicit HTMLFrameElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser = NOT_FROM_PARSER);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLFrameElement,
+ nsGenericHTMLFrameElement)
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLFrameElement, frame)
+
+ // nsIContent
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // WebIDL API
+ void GetFrameBorder(DOMString& aFrameBorder) const {
+ GetHTMLAttr(nsGkAtoms::frameborder, aFrameBorder);
+ }
+ void SetFrameBorder(const nsAString& aFrameBorder, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::frameborder, aFrameBorder, aError);
+ }
+
+ void GetLongDesc(nsAString& aLongDesc) const {
+ GetURIAttr(nsGkAtoms::longdesc, nullptr, aLongDesc);
+ }
+ void SetLongDesc(const nsAString& aLongDesc, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::longdesc, aLongDesc);
+ }
+
+ void GetMarginHeight(DOMString& aMarginHeight) const {
+ GetHTMLAttr(nsGkAtoms::marginheight, aMarginHeight);
+ }
+ void SetMarginHeight(const nsAString& aMarginHeight, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::marginheight, aMarginHeight, aError);
+ }
+
+ void GetMarginWidth(DOMString& aMarginWidth) const {
+ GetHTMLAttr(nsGkAtoms::marginwidth, aMarginWidth);
+ }
+ void SetMarginWidth(const nsAString& aMarginWidth, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::marginwidth, aMarginWidth, aError);
+ }
+
+ void GetName(DOMString& aName) const { GetHTMLAttr(nsGkAtoms::name, aName); }
+ void SetName(const nsAString& aName, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aError);
+ }
+
+ bool NoResize() const { return GetBoolAttr(nsGkAtoms::noresize); }
+ void SetNoResize(bool& aNoResize, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::noresize, aNoResize, aError);
+ }
+
+ void GetScrolling(DOMString& aScrolling) const {
+ GetHTMLAttr(nsGkAtoms::scrolling, aScrolling);
+ }
+ void SetScrolling(const nsAString& aScrolling, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::scrolling, aScrolling, aError);
+ }
+
+ void GetSrc(nsString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); }
+ void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal,
+ ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aError);
+ }
+
+ using nsGenericHTMLFrameElement::GetContentDocument;
+ using nsGenericHTMLFrameElement::GetContentWindow;
+
+ protected:
+ virtual ~HTMLFrameElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLFrameElement_h
diff --git a/dom/html/HTMLFrameSetElement.cpp b/dom/html/HTMLFrameSetElement.cpp
new file mode 100644
index 0000000000..d6a794698b
--- /dev/null
+++ b/dom/html/HTMLFrameSetElement.cpp
@@ -0,0 +1,316 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLFrameSetElement.h"
+#include "mozilla/Try.h"
+#include "mozilla/dom/HTMLFrameSetElementBinding.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/EventHandlerBinding.h"
+#include "nsGlobalWindowInner.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(FrameSet)
+
+namespace mozilla::dom {
+
+HTMLFrameSetElement::~HTMLFrameSetElement() = default;
+
+JSObject* HTMLFrameSetElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLFrameSetElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLFrameSetElement)
+
+void HTMLFrameSetElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ bool aNotify) {
+ /* The main goal here is to see whether the _number_ of rows or
+ * columns has changed. If it has, we need to reframe; otherwise
+ * we want to reflow.
+ * Ideally, the style hint would be changed back to reflow after the reframe
+ * has been performed. Unfortunately, however, the reframe will be performed
+ * by the call to MutationObservers::AttributeChanged, which occurs *after*
+ * AfterSetAttr is called, leaving us with no convenient way of changing the
+ * value back to reflow afterwards. However,
+ * MutationObservers::AttributeChanged is effectively the only consumer of
+ * this value, so as long as we always set the value correctly here, we should
+ * be fine.
+ */
+ mCurrentRowColHint = NS_STYLE_HINT_REFLOW;
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::rows) {
+ if (aValue) {
+ int32_t oldRows = mNumRows;
+ ParseRowCol(*aValue, mNumRows, &mRowSpecs);
+ if (mNumRows != oldRows) {
+ mCurrentRowColHint = nsChangeHint_ReconstructFrame;
+ }
+ }
+ } else if (aName == nsGkAtoms::cols) {
+ if (aValue) {
+ int32_t oldCols = mNumCols;
+ ParseRowCol(*aValue, mNumCols, &mColSpecs);
+ if (mNumCols != oldCols) {
+ mCurrentRowColHint = nsChangeHint_ReconstructFrame;
+ }
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue,
+ aNotify);
+}
+
+nsresult HTMLFrameSetElement::GetRowSpec(int32_t* aNumValues,
+ const nsFramesetSpec** aSpecs) {
+ MOZ_ASSERT(aNumValues, "Must have a pointer to an integer here!");
+ MOZ_ASSERT(aSpecs, "Must have a pointer to an array of nsFramesetSpecs");
+ *aNumValues = 0;
+ *aSpecs = nullptr;
+
+ if (!mRowSpecs) {
+ if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::rows)) {
+ MOZ_TRY(ParseRowCol(*value, mNumRows, &mRowSpecs));
+ }
+
+ if (!mRowSpecs) { // we may not have had an attr or had an empty attr
+ mRowSpecs = MakeUnique<nsFramesetSpec[]>(1);
+ mNumRows = 1;
+ mRowSpecs[0].mUnit = eFramesetUnit_Relative;
+ mRowSpecs[0].mValue = 1;
+ }
+ }
+
+ *aSpecs = mRowSpecs.get();
+ *aNumValues = mNumRows;
+ return NS_OK;
+}
+
+nsresult HTMLFrameSetElement::GetColSpec(int32_t* aNumValues,
+ const nsFramesetSpec** aSpecs) {
+ MOZ_ASSERT(aNumValues, "Must have a pointer to an integer here!");
+ MOZ_ASSERT(aSpecs, "Must have a pointer to an array of nsFramesetSpecs");
+ *aNumValues = 0;
+ *aSpecs = nullptr;
+
+ if (!mColSpecs) {
+ if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::cols)) {
+ MOZ_TRY(ParseRowCol(*value, mNumCols, &mColSpecs));
+ }
+
+ if (!mColSpecs) { // we may not have had an attr or had an empty attr
+ mColSpecs = MakeUnique<nsFramesetSpec[]>(1);
+ mNumCols = 1;
+ mColSpecs[0].mUnit = eFramesetUnit_Relative;
+ mColSpecs[0].mValue = 1;
+ }
+ }
+
+ *aSpecs = mColSpecs.get();
+ *aNumValues = mNumCols;
+ return NS_OK;
+}
+
+bool HTMLFrameSetElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::bordercolor) {
+ return aResult.ParseColor(aValue);
+ }
+ if (aAttribute == nsGkAtoms::frameborder) {
+ return nsGenericHTMLElement::ParseFrameborderValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::border) {
+ return aResult.ParseIntWithBounds(aValue, 0, 100);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+nsChangeHint HTMLFrameSetElement::GetAttributeChangeHint(
+ const nsAtom* aAttribute, int32_t aModType) const {
+ nsChangeHint retval =
+ nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType);
+ if (aAttribute == nsGkAtoms::rows || aAttribute == nsGkAtoms::cols) {
+ retval |= mCurrentRowColHint;
+ }
+ return retval;
+}
+
+/**
+ * Translate a "rows" or "cols" spec into an array of nsFramesetSpecs
+ */
+nsresult HTMLFrameSetElement::ParseRowCol(const nsAttrValue& aValue,
+ int32_t& aNumSpecs,
+ UniquePtr<nsFramesetSpec[]>* aSpecs) {
+ if (aValue.IsEmptyString()) {
+ aNumSpecs = 0;
+ *aSpecs = nullptr;
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aValue.Type() == nsAttrValue::eString);
+
+ static const char16_t sAster('*');
+ static const char16_t sPercent('%');
+ static const char16_t sComma(',');
+
+ nsAutoString spec(aValue.GetStringValue());
+ // remove whitespace (Bug 33699) and quotation marks (bug 224598)
+ // also remove leading/trailing commas (bug 31482)
+ spec.StripChars(u" \n\r\t\"\'");
+ spec.Trim(",");
+
+ // Count the commas. Don't count more than X commas (bug 576447).
+ static_assert(NS_MAX_FRAMESET_SPEC_COUNT * sizeof(nsFramesetSpec) < (1 << 30),
+ "Too many frameset specs allowed to allocate");
+ int32_t commaX = spec.FindChar(sComma);
+ int32_t count = 1;
+ while (commaX != kNotFound && count < NS_MAX_FRAMESET_SPEC_COUNT) {
+ count++;
+ commaX = spec.FindChar(sComma, commaX + 1);
+ }
+
+ auto specs = MakeUniqueFallible<nsFramesetSpec[]>(count);
+ if (!specs) {
+ *aSpecs = nullptr;
+ aNumSpecs = 0;
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Pre-grab the compat mode; we may need it later in the loop.
+ bool isInQuirks = InNavQuirksMode(OwnerDoc());
+
+ // Parse each comma separated token
+
+ int32_t start = 0;
+ int32_t specLen = spec.Length();
+
+ for (int32_t i = 0; i < count; i++) {
+ // Find our comma
+ commaX = spec.FindChar(sComma, start);
+ NS_ASSERTION(i == count - 1 || commaX != kNotFound,
+ "Failed to find comma, somehow");
+ int32_t end = (commaX == kNotFound) ? specLen : commaX;
+
+ // Note: If end == start then it means that the token has no
+ // data in it other than a terminating comma (or the end of the spec).
+ // So default to a fixed width of 0.
+ specs[i].mUnit = eFramesetUnit_Fixed;
+ specs[i].mValue = 0;
+ if (end > start) {
+ int32_t numberEnd = end;
+ char16_t ch = spec.CharAt(numberEnd - 1);
+ if (sAster == ch) {
+ specs[i].mUnit = eFramesetUnit_Relative;
+ numberEnd--;
+ } else if (sPercent == ch) {
+ specs[i].mUnit = eFramesetUnit_Percent;
+ numberEnd--;
+ // check for "*%"
+ if (numberEnd > start) {
+ ch = spec.CharAt(numberEnd - 1);
+ if (sAster == ch) {
+ specs[i].mUnit = eFramesetUnit_Relative;
+ numberEnd--;
+ }
+ }
+ }
+
+ // Translate value to an integer
+ nsAutoString token;
+ spec.Mid(token, start, numberEnd - start);
+
+ // Treat * as 1*
+ if ((eFramesetUnit_Relative == specs[i].mUnit) && (0 == token.Length())) {
+ specs[i].mValue = 1;
+ } else {
+ // Otherwise just convert to integer.
+ nsresult err;
+ specs[i].mValue = token.ToInteger(&err);
+ if (NS_FAILED(err)) {
+ specs[i].mValue = 0;
+ }
+ }
+
+ // Treat 0* as 1* in quirks mode (bug 40383)
+ if (isInQuirks) {
+ if ((eFramesetUnit_Relative == specs[i].mUnit) &&
+ (0 == specs[i].mValue)) {
+ specs[i].mValue = 1;
+ }
+ }
+
+ // Catch zero and negative frame sizes for Nav compatibility
+ // Nav resized absolute and relative frames to "1" and
+ // percent frames to an even percentage of the width
+ //
+ // if (isInQuirks && (specs[i].mValue <= 0)) {
+ // if (eFramesetUnit_Percent == specs[i].mUnit) {
+ // specs[i].mValue = 100 / count;
+ // } else {
+ // specs[i].mValue = 1;
+ // }
+ //} else {
+
+ // In standards mode, just set negative sizes to zero
+ if (specs[i].mValue < 0) {
+ specs[i].mValue = 0;
+ }
+ start = end + 1;
+ }
+ }
+
+ aNumSpecs = count;
+ // Transfer ownership to caller here
+ *aSpecs = std::move(specs);
+
+ return NS_OK;
+}
+
+bool HTMLFrameSetElement::IsEventAttributeNameInternal(nsAtom* aName) {
+ return nsContentUtils::IsEventAttributeName(
+ aName, EventNameType_HTML | EventNameType_HTMLBodyOrFramesetOnly);
+}
+
+#define EVENT(name_, id_, type_, struct_) /* nothing; handled by the shim */
+// nsGenericHTMLElement::GetOnError returns
+// already_AddRefed<EventHandlerNonNull> while other getters return
+// EventHandlerNonNull*, so allow passing in the type to use here.
+#define WINDOW_EVENT_HELPER(name_, type_) \
+ type_* HTMLFrameSetElement::GetOn##name_() { \
+ if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \
+ nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \
+ return globalWin->GetOn##name_(); \
+ } \
+ return nullptr; \
+ } \
+ void HTMLFrameSetElement::SetOn##name_(type_* handler) { \
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \
+ if (!win) { \
+ return; \
+ } \
+ \
+ nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \
+ return globalWin->SetOn##name_(handler); \
+ }
+#define WINDOW_EVENT(name_, id_, type_, struct_) \
+ WINDOW_EVENT_HELPER(name_, EventHandlerNonNull)
+#define BEFOREUNLOAD_EVENT(name_, id_, type_, struct_) \
+ WINDOW_EVENT_HELPER(name_, OnBeforeUnloadEventHandlerNonNull)
+#include "mozilla/EventNameList.h" // IWYU pragma: keep
+#undef BEFOREUNLOAD_EVENT
+#undef WINDOW_EVENT
+#undef WINDOW_EVENT_HELPER
+#undef EVENT
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLFrameSetElement.h b/dom/html/HTMLFrameSetElement.h
new file mode 100644
index 0000000000..d0c735621a
--- /dev/null
+++ b/dom/html/HTMLFrameSetElement.h
@@ -0,0 +1,153 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HTMLFrameSetElement_h
+#define HTMLFrameSetElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsGenericHTMLElement.h"
+
+/**
+ * The nsFramesetUnit enum is used to denote the type of each entry
+ * in the row or column spec.
+ */
+enum nsFramesetUnit {
+ eFramesetUnit_Fixed = 0,
+ eFramesetUnit_Percent,
+ eFramesetUnit_Relative
+};
+
+/**
+ * The nsFramesetSpec struct is used to hold a single entry in the
+ * row or column spec.
+ */
+struct nsFramesetSpec {
+ nsFramesetUnit mUnit;
+ nscoord mValue;
+};
+
+/**
+ * The maximum number of entries allowed in the frame set element row
+ * or column spec.
+ */
+#define NS_MAX_FRAMESET_SPEC_COUNT 16000
+
+//----------------------------------------------------------------------
+
+namespace mozilla::dom {
+
+class OnBeforeUnloadEventHandlerNonNull;
+
+class HTMLFrameSetElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLFrameSetElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)),
+ mNumRows(0),
+ mNumCols(0),
+ mCurrentRowColHint(NS_STYLE_HINT_REFLOW) {
+ SetHasWeirdParserInsertionMode();
+ }
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLFrameSetElement, frameset)
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLFrameSetElement,
+ nsGenericHTMLElement)
+
+ void GetCols(DOMString& aCols) { GetHTMLAttr(nsGkAtoms::cols, aCols); }
+ void SetCols(const nsAString& aCols, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::cols, aCols, aError);
+ }
+ void GetRows(DOMString& aRows) { GetHTMLAttr(nsGkAtoms::rows, aRows); }
+ void SetRows(const nsAString& aRows, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::rows, aRows, aError);
+ }
+
+ bool IsEventAttributeNameInternal(nsAtom* aName) override;
+
+ // Event listener stuff; we need to declare only the ones we need to
+ // forward to window that don't come from nsIDOMHTMLFrameSetElement.
+#define EVENT(name_, id_, type_, \
+ struct_) /* nothing; handled by the superclass */
+#define WINDOW_EVENT_HELPER(name_, type_) \
+ type_* GetOn##name_(); \
+ void SetOn##name_(type_* handler);
+#define WINDOW_EVENT(name_, id_, type_, struct_) \
+ WINDOW_EVENT_HELPER(name_, EventHandlerNonNull)
+#define BEFOREUNLOAD_EVENT(name_, id_, type_, struct_) \
+ WINDOW_EVENT_HELPER(name_, OnBeforeUnloadEventHandlerNonNull)
+#include "mozilla/EventNameList.h" // IWYU pragma: keep
+#undef BEFOREUNLOAD_EVENT
+#undef WINDOW_EVENT
+#undef WINDOW_EVENT_HELPER
+#undef EVENT
+
+ /**
+ * GetRowSpec is used to get the "rows" spec.
+ * @param out int32_t aNumValues The number of row sizes specified.
+ * @param out nsFramesetSpec* aSpecs The array of size specifications.
+ This is _not_ owned by the caller, but by the nsFrameSetElement
+ implementation. DO NOT DELETE IT.
+ */
+ nsresult GetRowSpec(int32_t* aNumValues, const nsFramesetSpec** aSpecs);
+ /**
+ * GetColSpec is used to get the "cols" spec
+ * @param out int32_t aNumValues The number of row sizes specified.
+ * @param out nsFramesetSpec* aSpecs The array of size specifications.
+ This is _not_ owned by the caller, but by the nsFrameSetElement
+ implementation. DO NOT DELETE IT.
+ */
+ nsresult GetColSpec(int32_t* aNumValues, const nsFramesetSpec** aSpecs);
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLFrameSetElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+
+ private:
+ nsresult ParseRowCol(const nsAttrValue& aValue, int32_t& aNumSpecs,
+ UniquePtr<nsFramesetSpec[]>* aSpecs);
+
+ /**
+ * The number of size specs in our "rows" attr
+ */
+ int32_t mNumRows;
+ /**
+ * The number of size specs in our "cols" attr
+ */
+ int32_t mNumCols;
+ /**
+ * The style hint to return for the rows/cols attrs in
+ * GetAttributeChangeHint
+ */
+ nsChangeHint mCurrentRowColHint;
+ /**
+ * The parsed representation of the "rows" attribute
+ */
+ UniquePtr<nsFramesetSpec[]> mRowSpecs; // parsed, non-computed dimensions
+ /**
+ * The parsed representation of the "cols" attribute
+ */
+ UniquePtr<nsFramesetSpec[]> mColSpecs; // parsed, non-computed dimensions
+};
+
+} // namespace mozilla::dom
+
+#endif // HTMLFrameSetElement_h
diff --git a/dom/html/HTMLHRElement.cpp b/dom/html/HTMLHRElement.cpp
new file mode 100644
index 0000000000..e433478a87
--- /dev/null
+++ b/dom/html/HTMLHRElement.cpp
@@ -0,0 +1,193 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLHRElement.h"
+#include "mozilla/dom/HTMLHRElementBinding.h"
+
+#include "nsCSSProps.h"
+#include "nsStyleConsts.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(HR)
+
+namespace mozilla::dom {
+
+HTMLHRElement::HTMLHRElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLHRElement::~HTMLHRElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLHRElement)
+
+bool HTMLHRElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ static const nsAttrValue::EnumTable kAlignTable[] = {
+ {"left", StyleTextAlign::Left},
+ {"right", StyleTextAlign::Right},
+ {"center", StyleTextAlign::Center},
+ {nullptr, 0}};
+
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::width) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::size) {
+ return aResult.ParseIntWithBounds(aValue, 1, 1000);
+ }
+ if (aAttribute == nsGkAtoms::align) {
+ return aResult.ParseEnumValue(aValue, kAlignTable, false);
+ }
+ if (aAttribute == nsGkAtoms::color) {
+ return aResult.ParseColor(aValue);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLHRElement::MapAttributesIntoRule(MappedDeclarationsBuilder& aBuilder) {
+ bool noshade = false;
+
+ const nsAttrValue* colorValue = aBuilder.GetAttr(nsGkAtoms::color);
+ nscolor color;
+ bool colorIsSet = colorValue && colorValue->GetColorValue(color);
+
+ if (colorIsSet) {
+ noshade = true;
+ } else {
+ noshade = !!aBuilder.GetAttr(nsGkAtoms::noshade);
+ }
+
+ // align: enum
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align);
+ if (value && value->Type() == nsAttrValue::eEnum) {
+ // Map align attribute into auto side margins
+ switch (StyleTextAlign(value->GetEnumValue())) {
+ case StyleTextAlign::Left:
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_left, 0.0f);
+ aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_right);
+ break;
+ case StyleTextAlign::Right:
+ aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_left);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_right, 0.0f);
+ break;
+ case StyleTextAlign::Center:
+ aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_left);
+ aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_right);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown <hr align> value");
+ break;
+ }
+ }
+ if (!aBuilder.PropertyIsSet(eCSSProperty_height)) {
+ // size: integer
+ if (noshade) {
+ // noshade case: size is set using the border
+ aBuilder.SetAutoValue(eCSSProperty_height);
+ } else {
+ // normal case
+ // the height includes the top and bottom borders that are initially 1px.
+ // for size=1, html.css has a special case rule that makes this work by
+ // removing all but the top border.
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::size);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ aBuilder.SetPixelValue(eCSSProperty_height,
+ (float)value->GetIntegerValue());
+ } // else use default value from html.css
+ }
+ }
+
+ // if not noshade, border styles are dealt with by html.css
+ if (noshade) {
+ // size: integer
+ // if a size is set, use half of it per side, otherwise, use 1px per side
+ float sizePerSide;
+ bool allSides = true;
+ value = aBuilder.GetAttr(nsGkAtoms::size);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ sizePerSide = (float)value->GetIntegerValue() / 2.0f;
+ if (sizePerSide < 1.0f) {
+ // XXX When the pixel bug is fixed, all the special casing for
+ // subpixel borders should be removed.
+ // In the meantime, this makes http://www.microsoft.com/ look right.
+ sizePerSide = 1.0f;
+ allSides = false;
+ }
+ } else {
+ sizePerSide = 1.0f; // default to a 2px high line
+ }
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_top_width, sizePerSide);
+ if (allSides) {
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_right_width,
+ sizePerSide);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_bottom_width,
+ sizePerSide);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_left_width,
+ sizePerSide);
+ }
+
+ if (!aBuilder.PropertyIsSet(eCSSProperty_border_top_style)) {
+ aBuilder.SetKeywordValue(eCSSProperty_border_top_style,
+ StyleBorderStyle::Solid);
+ }
+ if (allSides) {
+ aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_right_style,
+ StyleBorderStyle::Solid);
+ aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_bottom_style,
+ StyleBorderStyle::Solid);
+ aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_left_style,
+ StyleBorderStyle::Solid);
+
+ // If it would be noticeable, set the border radius to
+ // 10000px on all corners; this triggers the clamping to make
+ // circular ends. This assumes the <hr> isn't larger than
+ // that in *both* dimensions.
+ for (const nsCSSPropertyID* props =
+ nsCSSProps::SubpropertyEntryFor(eCSSProperty_border_radius);
+ *props != eCSSProperty_UNKNOWN; ++props) {
+ aBuilder.SetPixelValueIfUnset(*props, 10000.0f);
+ }
+ }
+ }
+ // color: a color
+ // (we got the color attribute earlier)
+ if (colorIsSet) {
+ aBuilder.SetColorValueIfUnset(eCSSProperty_color, color);
+ }
+ MapWidthAttributeInto(aBuilder);
+ MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLHRElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::align}, {nsGkAtoms::width}, {nsGkAtoms::size},
+ {nsGkAtoms::color}, {nsGkAtoms::noshade}, {nullptr},
+ };
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLHRElement::GetAttributeMappingFunction() const {
+ return &MapAttributesIntoRule;
+}
+
+JSObject* HTMLHRElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLHRElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLHRElement.h b/dom/html/HTMLHRElement.h
new file mode 100644
index 0000000000..9b8d33279e
--- /dev/null
+++ b/dom/html/HTMLHRElement.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLHRElement_h
+#define mozilla_dom_HTMLHRElement_h
+
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLHRElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLHRElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLHRElement, nsGenericHTMLElement)
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // WebIDL API
+ void GetAlign(nsAString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::align, aValue);
+ }
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+
+ void GetColor(nsAString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::color, aValue);
+ }
+ void SetColor(const nsAString& aColor, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::color, aColor, aError);
+ }
+
+ bool NoShade() const { return GetBoolAttr(nsGkAtoms::noshade); }
+ void SetNoShade(bool aNoShade, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::noshade, aNoShade, aError);
+ }
+
+ void GetSize(nsAString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::size, aValue);
+ }
+ void SetSize(const nsAString& aSize, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::size, aSize, aError);
+ }
+
+ void GetWidth(nsAString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::width, aValue);
+ }
+ void SetWidth(const nsAString& aWidth, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::width, aWidth, aError);
+ }
+
+ protected:
+ virtual ~HTMLHRElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLHRElement_h
diff --git a/dom/html/HTMLHeadingElement.cpp b/dom/html/HTMLHeadingElement.cpp
new file mode 100644
index 0000000000..e3274d44b3
--- /dev/null
+++ b/dom/html/HTMLHeadingElement.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLHeadingElement.h"
+#include "mozilla/dom/HTMLHeadingElementBinding.h"
+
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsGkAtoms.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Heading)
+
+namespace mozilla::dom {
+
+HTMLHeadingElement::~HTMLHeadingElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLHeadingElement)
+
+JSObject* HTMLHeadingElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLHeadingElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool HTMLHeadingElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aAttribute == nsGkAtoms::align && aNamespaceID == kNameSpaceID_None) {
+ return ParseDivAlignValue(aValue, aResult);
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLHeadingElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLHeadingElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry* const map[] = {sDivAlignAttributeMap,
+ sCommonAttributeMap};
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLHeadingElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLHeadingElement.h b/dom/html/HTMLHeadingElement.h
new file mode 100644
index 0000000000..43d718b5a3
--- /dev/null
+++ b/dom/html/HTMLHeadingElement.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLHeadingElement_h
+#define mozilla_dom_HTMLHeadingElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLHeadingElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLHeadingElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ MOZ_ASSERT(IsHTMLHeadingElement());
+ }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ return SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+ void GetAlign(DOMString& aAlign) const {
+ return GetHTMLAttr(nsGkAtoms::align, aAlign);
+ }
+
+ int32_t AccessibilityLevel() const {
+ nsAtom* name = NodeInfo()->NameAtom();
+ if (name == nsGkAtoms::h1) {
+ return 1;
+ }
+ if (name == nsGkAtoms::h2) {
+ return 2;
+ }
+ if (name == nsGkAtoms::h3) {
+ return 3;
+ }
+ if (name == nsGkAtoms::h4) {
+ return 4;
+ }
+ if (name == nsGkAtoms::h5) {
+ return 5;
+ }
+ MOZ_ASSERT(name == nsGkAtoms::h6);
+ return 6;
+ }
+
+ NS_IMPL_FROMNODE_HELPER(HTMLHeadingElement, IsHTMLHeadingElement())
+
+ protected:
+ virtual ~HTMLHeadingElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLHeadingElement_h
diff --git a/dom/html/HTMLIFrameElement.cpp b/dom/html/HTMLIFrameElement.cpp
new file mode 100644
index 0000000000..97363ccbff
--- /dev/null
+++ b/dom/html/HTMLIFrameElement.cpp
@@ -0,0 +1,381 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/DOMIntersectionObserver.h"
+#include "mozilla/dom/HTMLIFrameElement.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLIFrameElementBinding.h"
+#include "mozilla/dom/FeaturePolicy.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsSubDocumentFrame.h"
+#include "nsError.h"
+#include "nsContentUtils.h"
+#include "nsSandboxFlags.h"
+#include "nsNetUtil.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(IFrame)
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLIFrameElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLIFrameElement,
+ nsGenericHTMLFrameElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFeaturePolicy)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSandbox)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLIFrameElement,
+ nsGenericHTMLFrameElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mFeaturePolicy)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSandbox)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ADDREF_INHERITED(HTMLIFrameElement, nsGenericHTMLFrameElement)
+NS_IMPL_RELEASE_INHERITED(HTMLIFrameElement, nsGenericHTMLFrameElement)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLIFrameElement)
+NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLFrameElement)
+
+// static
+const DOMTokenListSupportedToken HTMLIFrameElement::sSupportedSandboxTokens[] =
+ {
+#define SANDBOX_KEYWORD(string, atom, flags) string,
+#include "IframeSandboxKeywordList.h"
+#undef SANDBOX_KEYWORD
+ nullptr};
+
+HTMLIFrameElement::HTMLIFrameElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : nsGenericHTMLFrameElement(std::move(aNodeInfo), aFromParser) {
+ // We always need a featurePolicy, even if not exposed.
+ mFeaturePolicy = new mozilla::dom::FeaturePolicy(this);
+ nsCOMPtr<nsIPrincipal> origin = GetFeaturePolicyDefaultOrigin();
+ MOZ_ASSERT(origin);
+ mFeaturePolicy->SetDefaultOrigin(origin);
+}
+
+HTMLIFrameElement::~HTMLIFrameElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLIFrameElement)
+
+void HTMLIFrameElement::BindToBrowsingContext(BrowsingContext*) {
+ RefreshFeaturePolicy(true /* parse the feature policy attribute */);
+}
+
+bool HTMLIFrameElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::marginwidth) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::marginheight) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::width) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::height) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::frameborder) {
+ return ParseFrameborderValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::scrolling) {
+ return ParseScrollingValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::sandbox) {
+ aResult.ParseAtomArray(aValue);
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::loading) {
+ return ParseLoadingAttribute(aValue, aResult);
+ }
+ }
+
+ return nsGenericHTMLFrameElement::ParseAttribute(
+ aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLIFrameElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ // frameborder: 0 | 1 (| NO | YES in quirks mode)
+ // If frameborder is 0 or No, set border to 0
+ // else leave it as the value set in html.css
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::frameborder);
+ if (value && value->Type() == nsAttrValue::eEnum) {
+ auto frameborder = static_cast<FrameBorderProperty>(value->GetEnumValue());
+ if (FrameBorderProperty::No == frameborder ||
+ FrameBorderProperty::Zero == frameborder) {
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_top_width, 0.0f);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_right_width, 0.0f);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_bottom_width, 0.0f);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_left_width, 0.0f);
+ }
+ }
+
+ nsGenericHTMLElement::MapImageSizeAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapImageAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLIFrameElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::width},
+ {nsGkAtoms::height},
+ {nsGkAtoms::frameborder},
+ {nullptr},
+ };
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sImageAlignAttributeMap,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLIFrameElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+void HTMLIFrameElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNameSpaceID, aName, aNotify);
+
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::loading) {
+ if (aValue && Loading(aValue->GetEnumValue()) == Loading::Lazy) {
+ SetLazyLoading();
+ } else if (aOldValue &&
+ Loading(aOldValue->GetEnumValue()) == Loading::Lazy) {
+ StopLazyLoading();
+ }
+ }
+
+ // If lazy loading and src set, set lazy loading again as we are doing a new
+ // load (lazy loading is unset after a load is complete).
+ if ((aName == nsGkAtoms::src || aName == nsGkAtoms::srcdoc) &&
+ LoadingState() == Loading::Lazy) {
+ SetLazyLoading();
+ }
+
+ if (aName == nsGkAtoms::sandbox) {
+ if (mFrameLoader) {
+ // If we have an nsFrameLoader, apply the new sandbox flags.
+ // Since this is called after the setter, the sandbox flags have
+ // alreay been updated.
+ mFrameLoader->ApplySandboxFlags(GetSandboxFlags());
+ }
+ }
+
+ if (aName == nsGkAtoms::allow || aName == nsGkAtoms::src ||
+ aName == nsGkAtoms::srcdoc || aName == nsGkAtoms::sandbox) {
+ RefreshFeaturePolicy(true /* parse the feature policy attribute */);
+ } else if (aName == nsGkAtoms::allowfullscreen) {
+ RefreshFeaturePolicy(false /* parse the feature policy attribute */);
+ }
+ }
+
+ return nsGenericHTMLFrameElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void HTMLIFrameElement::OnAttrSetButNotChanged(
+ int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+
+ return nsGenericHTMLFrameElement::OnAttrSetButNotChanged(aNamespaceID, aName,
+ aValue, aNotify);
+}
+
+void HTMLIFrameElement::AfterMaybeChangeAttr(int32_t aNamespaceID,
+ nsAtom* aName, bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::srcdoc) {
+ // Don't propagate errors from LoadSrc. The attribute was successfully
+ // set/unset, that's what we should reflect.
+ LoadSrc();
+ }
+ }
+}
+
+uint32_t HTMLIFrameElement::GetSandboxFlags() const {
+ const nsAttrValue* sandboxAttr = GetParsedAttr(nsGkAtoms::sandbox);
+ // No sandbox attribute, no sandbox flags.
+ if (!sandboxAttr) {
+ return SANDBOXED_NONE;
+ }
+ return nsContentUtils::ParseSandboxAttributeToFlags(sandboxAttr);
+}
+
+JSObject* HTMLIFrameElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLIFrameElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+mozilla::dom::FeaturePolicy* HTMLIFrameElement::FeaturePolicy() const {
+ return mFeaturePolicy;
+}
+
+void HTMLIFrameElement::MaybeStoreCrossOriginFeaturePolicy() {
+ if (!mFrameLoader) {
+ return;
+ }
+
+ // If the browsingContext is not ready (because docshell is dead), don't try
+ // to create one.
+ if (!mFrameLoader->IsRemoteFrame() && !mFrameLoader->GetExistingDocShell()) {
+ return;
+ }
+
+ RefPtr<BrowsingContext> browsingContext = mFrameLoader->GetBrowsingContext();
+
+ if (!browsingContext || !browsingContext->IsContentSubframe()) {
+ return;
+ }
+
+ if (ContentChild* cc = ContentChild::GetSingleton()) {
+ Unused << cc->SendSetContainerFeaturePolicy(browsingContext,
+ mFeaturePolicy);
+ }
+}
+
+already_AddRefed<nsIPrincipal>
+HTMLIFrameElement::GetFeaturePolicyDefaultOrigin() const {
+ nsCOMPtr<nsIPrincipal> principal;
+
+ if (HasAttr(nsGkAtoms::srcdoc)) {
+ principal = NodePrincipal();
+ return principal.forget();
+ }
+
+ nsCOMPtr<nsIURI> nodeURI;
+ if (GetURIAttr(nsGkAtoms::src, nullptr, getter_AddRefs(nodeURI)) && nodeURI) {
+ principal = BasePrincipal::CreateContentPrincipal(
+ nodeURI, BasePrincipal::Cast(NodePrincipal())->OriginAttributesRef());
+ }
+
+ if (!principal) {
+ principal = NodePrincipal();
+ }
+
+ return principal.forget();
+}
+
+void HTMLIFrameElement::RefreshFeaturePolicy(bool aParseAllowAttribute) {
+ if (aParseAllowAttribute) {
+ mFeaturePolicy->ResetDeclaredPolicy();
+
+ // The origin can change if 'src' and 'srcdoc' attributes change.
+ nsCOMPtr<nsIPrincipal> origin = GetFeaturePolicyDefaultOrigin();
+ MOZ_ASSERT(origin);
+ mFeaturePolicy->SetDefaultOrigin(origin);
+
+ nsAutoString allow;
+ GetAttr(nsGkAtoms::allow, allow);
+
+ if (!allow.IsEmpty()) {
+ // Set or reset the FeaturePolicy directives.
+ mFeaturePolicy->SetDeclaredPolicy(OwnerDoc(), allow, NodePrincipal(),
+ origin);
+ }
+ }
+
+ if (AllowFullscreen()) {
+ mFeaturePolicy->MaybeSetAllowedPolicy(u"fullscreen"_ns);
+ }
+
+ mFeaturePolicy->InheritPolicy(OwnerDoc()->FeaturePolicy());
+ MaybeStoreCrossOriginFeaturePolicy();
+}
+
+void HTMLIFrameElement::UpdateLazyLoadState() {
+ // Store current base URI and referrer policy in the lazy load state.
+ mLazyLoadState.mBaseURI = GetBaseURI();
+ mLazyLoadState.mReferrerPolicy = GetReferrerPolicyAsEnum();
+}
+
+nsresult HTMLIFrameElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ // Update lazy load state on bind to tree again if lazy loading, as the
+ // loading attribute could be set before others.
+ if (mLazyLoading) {
+ UpdateLazyLoadState();
+ }
+
+ return nsGenericHTMLFrameElement::BindToTree(aContext, aParent);
+}
+
+void HTMLIFrameElement::SetLazyLoading() {
+ if (mLazyLoading) {
+ return;
+ }
+
+ if (!StaticPrefs::dom_iframe_lazy_loading_enabled()) {
+ return;
+ }
+
+ // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#will-lazy-load-element-steps
+ // "If scripting is disabled for element, then return false."
+ Document* doc = OwnerDoc();
+ if (!doc->IsScriptEnabled() || doc->IsStaticDocument()) {
+ return;
+ }
+
+ doc->EnsureLazyLoadObserver().Observe(*this);
+ mLazyLoading = true;
+
+ UpdateLazyLoadState();
+}
+
+void HTMLIFrameElement::StopLazyLoading() {
+ if (!mLazyLoading) {
+ return;
+ }
+
+ mLazyLoading = false;
+
+ Document* doc = OwnerDoc();
+ if (auto* obs = doc->GetLazyLoadObserver()) {
+ obs->Unobserve(*this);
+ }
+
+ LoadSrc();
+
+ mLazyLoadState.Clear();
+ if (nsSubDocumentFrame* ourFrame = do_QueryFrame(GetPrimaryFrame())) {
+ ourFrame->ResetFrameLoader(nsSubDocumentFrame::RetainPaintData::No);
+ }
+}
+
+void HTMLIFrameElement::NodeInfoChanged(Document* aOldDoc) {
+ nsGenericHTMLElement::NodeInfoChanged(aOldDoc);
+
+ if (mLazyLoading) {
+ aOldDoc->GetLazyLoadObserver()->Unobserve(*this);
+ mLazyLoading = false;
+ SetLazyLoading();
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLIFrameElement.h b/dom/html/HTMLIFrameElement.h
new file mode 100644
index 0000000000..8a87c02472
--- /dev/null
+++ b/dom/html/HTMLIFrameElement.h
@@ -0,0 +1,234 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLIFrameElement_h
+#define mozilla_dom_HTMLIFrameElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGenericHTMLFrameElement.h"
+#include "nsDOMTokenList.h"
+
+namespace mozilla::dom {
+
+class FeaturePolicy;
+
+class HTMLIFrameElement final : public nsGenericHTMLFrameElement {
+ public:
+ explicit HTMLIFrameElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser = NOT_FROM_PARSER);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLIFrameElement, iframe)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLIFrameElement,
+ nsGenericHTMLFrameElement)
+
+ // Element
+ virtual bool IsInteractiveHTMLContent() const override { return true; }
+
+ // nsIContent
+ virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ virtual nsMapRuleToAttributesFunc GetAttributeMappingFunction()
+ const override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ void NodeInfoChanged(Document* aOldDoc) override;
+
+ void BindToBrowsingContext(BrowsingContext* aBrowsingContext);
+
+ uint32_t GetSandboxFlags() const;
+
+ // Web IDL binding methods
+ void GetSrc(nsString& aSrc) const {
+ GetURIAttr(nsGkAtoms::src, nullptr, aSrc);
+ }
+ void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal,
+ ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aError);
+ }
+ void GetSrcdoc(DOMString& aSrcdoc) {
+ GetHTMLAttr(nsGkAtoms::srcdoc, aSrcdoc);
+ }
+ void SetSrcdoc(const nsAString& aSrcdoc, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::srcdoc, aSrcdoc, aError);
+ }
+ void GetName(DOMString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); }
+ void SetName(const nsAString& aName, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aError);
+ }
+ nsDOMTokenList* Sandbox() {
+ if (!mSandbox) {
+ mSandbox =
+ new nsDOMTokenList(this, nsGkAtoms::sandbox, sSupportedSandboxTokens);
+ }
+ return mSandbox;
+ }
+
+ bool AllowFullscreen() const {
+ return GetBoolAttr(nsGkAtoms::allowfullscreen);
+ }
+
+ void SetAllowFullscreen(bool aAllow, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::allowfullscreen, aAllow, aError);
+ }
+
+ void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); }
+ void SetWidth(const nsAString& aWidth, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::width, aWidth, aError);
+ }
+ void GetHeight(DOMString& aHeight) {
+ GetHTMLAttr(nsGkAtoms::height, aHeight);
+ }
+ void SetHeight(const nsAString& aHeight, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::height, aHeight, aError);
+ }
+ using nsGenericHTMLFrameElement::GetContentDocument;
+ using nsGenericHTMLFrameElement::GetContentWindow;
+ void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); }
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+ void GetAllow(DOMString& aAllow) { GetHTMLAttr(nsGkAtoms::allow, aAllow); }
+ void SetAllow(const nsAString& aAllow, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::allow, aAllow, aError);
+ }
+ void GetScrolling(DOMString& aScrolling) {
+ GetHTMLAttr(nsGkAtoms::scrolling, aScrolling);
+ }
+ void SetScrolling(const nsAString& aScrolling, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::scrolling, aScrolling, aError);
+ }
+ void GetFrameBorder(DOMString& aFrameBorder) {
+ GetHTMLAttr(nsGkAtoms::frameborder, aFrameBorder);
+ }
+ void SetFrameBorder(const nsAString& aFrameBorder, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::frameborder, aFrameBorder, aError);
+ }
+ void GetLongDesc(nsAString& aLongDesc) const {
+ GetURIAttr(nsGkAtoms::longdesc, nullptr, aLongDesc);
+ }
+ void SetLongDesc(const nsAString& aLongDesc, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::longdesc, aLongDesc, aError);
+ }
+ void GetMarginWidth(DOMString& aMarginWidth) {
+ GetHTMLAttr(nsGkAtoms::marginwidth, aMarginWidth);
+ }
+ void SetMarginWidth(const nsAString& aMarginWidth, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::marginwidth, aMarginWidth, aError);
+ }
+ void GetMarginHeight(DOMString& aMarginHeight) {
+ GetHTMLAttr(nsGkAtoms::marginheight, aMarginHeight);
+ }
+ void SetMarginHeight(const nsAString& aMarginHeight, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::marginheight, aMarginHeight, aError);
+ }
+ void SetReferrerPolicy(const nsAString& aReferrer, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::referrerpolicy, aReferrer, aError);
+ }
+ void GetReferrerPolicy(nsAString& aReferrer) {
+ GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrer);
+ }
+ Document* GetSVGDocument(nsIPrincipal& aSubjectPrincipal) {
+ return GetContentDocument(aSubjectPrincipal);
+ }
+ bool Mozbrowser() const { return GetBoolAttr(nsGkAtoms::mozbrowser); }
+ void SetMozbrowser(bool aAllow, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::mozbrowser, aAllow, aError);
+ }
+ using nsGenericHTMLFrameElement::SetMozbrowser;
+ // nsGenericHTMLFrameElement::GetFrameLoader is fine
+ // nsGenericHTMLFrameElement::GetAppManifestURL is fine
+
+ // The fullscreen flag is set to true only when requestFullscreen is
+ // explicitly called on this <iframe> element. In case this flag is
+ // set, the fullscreen state of this element will not be reverted
+ // automatically when its subdocument exits fullscreen.
+ bool FullscreenFlag() const { return mFullscreenFlag; }
+ void SetFullscreenFlag(bool aValue) { mFullscreenFlag = aValue; }
+
+ mozilla::dom::FeaturePolicy* FeaturePolicy() const;
+
+ void SetLoading(const nsAString& aLoading, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::loading, aLoading, aError);
+ }
+
+ void SetLazyLoading();
+ void StopLazyLoading();
+
+ const LazyLoadFrameResumptionState& GetLazyLoadFrameResumptionState() const {
+ return mLazyLoadState;
+ }
+
+ protected:
+ virtual ~HTMLIFrameElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+ virtual void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) override;
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+
+ static const DOMTokenListSupportedToken sSupportedSandboxTokens[];
+
+ void RefreshFeaturePolicy(bool aParseAllowAttribute);
+
+ // If this iframe has a 'srcdoc' attribute, the document's origin will be
+ // returned. Otherwise, if this iframe has a 'src' attribute, the origin will
+ // be the parsing of its value as URL. If the URL is invalid, or 'src'
+ // attribute doesn't exist, the origin will be the document's origin.
+ already_AddRefed<nsIPrincipal> GetFeaturePolicyDefaultOrigin() const;
+
+ /**
+ * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
+ * This function will be called by AfterSetAttr whether the attribute is being
+ * set or unset.
+ *
+ * @param aNamespaceID the namespace of the attr being set
+ * @param aName the localname of the attribute being set
+ * @param aNotify Whether we plan to notify document observers.
+ */
+ void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify);
+
+ /**
+ * Feature policy inheritance is broken in cross process model, so we may
+ * have to store feature policy in browsingContext when neccesary.
+ */
+ void MaybeStoreCrossOriginFeaturePolicy();
+
+ RefPtr<dom::FeaturePolicy> mFeaturePolicy;
+ RefPtr<nsDOMTokenList> mSandbox;
+
+ /**
+ * Current lazy load resumption state (base URI and referrer policy).
+ * https://html.spec.whatwg.org/#lazy-load-resumption-steps
+ */
+ LazyLoadFrameResumptionState mLazyLoadState;
+
+ // Update lazy load state internally
+ void UpdateLazyLoadState();
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/html/HTMLImageElement.cpp b/dom/html/HTMLImageElement.cpp
new file mode 100644
index 0000000000..1aa23cdea8
--- /dev/null
+++ b/dom/html/HTMLImageElement.cpp
@@ -0,0 +1,1380 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLImageElement.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/HTMLImageElementBinding.h"
+#include "mozilla/dom/NameSpaceConstants.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsPresContext.h"
+#include "nsSize.h"
+#include "mozilla/dom/Document.h"
+#include "nsImageFrame.h"
+#include "nsIScriptContext.h"
+#include "nsContentUtils.h"
+#include "nsContainerFrame.h"
+#include "nsNodeInfoManager.h"
+#include "mozilla/MouseEvents.h"
+#include "nsContentPolicyUtils.h"
+#include "nsFocusManager.h"
+#include "mozilla/dom/DOMIntersectionObserver.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/dom/UserActivation.h"
+#include "nsAttrValueOrString.h"
+#include "imgLoader.h"
+#include "Image.h"
+
+// Responsive images!
+#include "mozilla/dom/HTMLSourceElement.h"
+#include "mozilla/dom/ResponsiveImageSelector.h"
+
+#include "imgINotificationObserver.h"
+#include "imgRequestProxy.h"
+
+#include "mozilla/CycleCollectedJSContext.h"
+
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RestyleManager.h"
+
+#include "nsLayoutUtils.h"
+
+using namespace mozilla::net;
+using mozilla::Maybe;
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Image)
+
+#ifdef DEBUG
+// Is aSubject a previous sibling of aNode.
+static bool IsPreviousSibling(const nsINode* aSubject, const nsINode* aNode) {
+ if (aSubject == aNode) {
+ return false;
+ }
+
+ nsINode* parent = aSubject->GetParentNode();
+ if (parent && parent == aNode->GetParentNode()) {
+ const Maybe<uint32_t> indexOfSubject = parent->ComputeIndexOf(aSubject);
+ const Maybe<uint32_t> indexOfNode = parent->ComputeIndexOf(aNode);
+ if (MOZ_LIKELY(indexOfSubject.isSome() && indexOfNode.isSome())) {
+ return *indexOfSubject < *indexOfNode;
+ }
+ // XXX Keep the odd traditional behavior for now.
+ return indexOfSubject.isNothing() && indexOfNode.isSome();
+ }
+
+ return false;
+}
+#endif
+
+namespace mozilla::dom {
+
+// Calls LoadSelectedImage on host element unless it has been superseded or
+// canceled -- this is the synchronous section of "update the image data".
+// https://html.spec.whatwg.org/multipage/embedded-content.html#update-the-image-data
+class ImageLoadTask final : public MicroTaskRunnable {
+ public:
+ ImageLoadTask(HTMLImageElement* aElement, bool aAlwaysLoad,
+ bool aUseUrgentStartForChannel)
+ : mElement(aElement),
+ mAlwaysLoad(aAlwaysLoad),
+ mUseUrgentStartForChannel(aUseUrgentStartForChannel) {
+ mDocument = aElement->OwnerDoc();
+ mDocument->BlockOnload();
+ }
+
+ void Run(AutoSlowOperation& aAso) override {
+ if (mElement->mPendingImageLoadTask == this) {
+ mElement->mPendingImageLoadTask = nullptr;
+ mElement->mUseUrgentStartForChannel = mUseUrgentStartForChannel;
+ mElement->LoadSelectedImage(true, true, mAlwaysLoad);
+ }
+ mDocument->UnblockOnload(false);
+ }
+
+ bool Suppressed() override {
+ nsIGlobalObject* global = mElement->GetOwnerGlobal();
+ return global && global->IsInSyncOperation();
+ }
+
+ bool AlwaysLoad() const { return mAlwaysLoad; }
+
+ private:
+ ~ImageLoadTask() = default;
+ RefPtr<HTMLImageElement> mElement;
+ nsCOMPtr<Document> mDocument;
+ bool mAlwaysLoad;
+
+ // True if we want to set nsIClassOfService::UrgentStart to the channel to
+ // get the response ASAP for better user responsiveness.
+ bool mUseUrgentStartForChannel;
+};
+
+HTMLImageElement::HTMLImageElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ // We start out broken
+ AddStatesSilently(ElementState::BROKEN);
+}
+
+HTMLImageElement::~HTMLImageElement() { nsImageLoadingContent::Destroy(); }
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLImageElement, nsGenericHTMLElement,
+ mResponsiveSelector)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLImageElement,
+ nsGenericHTMLElement,
+ nsIImageLoadingContent,
+ imgINotificationObserver)
+
+NS_IMPL_ELEMENT_CLONE(HTMLImageElement)
+
+bool HTMLImageElement::IsInteractiveHTMLContent() const {
+ return HasAttr(nsGkAtoms::usemap) ||
+ nsGenericHTMLElement::IsInteractiveHTMLContent();
+}
+
+void HTMLImageElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) {
+ nsImageLoadingContent::AsyncEventRunning(aEvent);
+}
+
+void HTMLImageElement::GetCurrentSrc(nsAString& aValue) {
+ nsCOMPtr<nsIURI> currentURI;
+ GetCurrentURI(getter_AddRefs(currentURI));
+ if (currentURI) {
+ nsAutoCString spec;
+ currentURI->GetSpec(spec);
+ CopyUTF8toUTF16(spec, aValue);
+ } else {
+ SetDOMStringToNull(aValue);
+ }
+}
+
+bool HTMLImageElement::Draggable() const {
+ // images may be dragged unless the draggable attribute is false
+ return !AttrValueIs(kNameSpaceID_None, nsGkAtoms::draggable,
+ nsGkAtoms::_false, eIgnoreCase);
+}
+
+bool HTMLImageElement::Complete() {
+ // It is still not clear what value should img.complete return in various
+ // cases, see https://github.com/whatwg/html/issues/4884
+
+ if (!HasAttr(nsGkAtoms::srcset) && !HasNonEmptyAttr(nsGkAtoms::src)) {
+ return true;
+ }
+
+ if (!mCurrentRequest || mPendingRequest) {
+ return false;
+ }
+
+ uint32_t status;
+ mCurrentRequest->GetImageStatus(&status);
+ return (status &
+ (imgIRequest::STATUS_LOAD_COMPLETE | imgIRequest::STATUS_ERROR)) != 0;
+}
+
+CSSIntPoint HTMLImageElement::GetXY() {
+ nsIFrame* frame = GetPrimaryFrame(FlushType::Layout);
+ if (!frame) {
+ return CSSIntPoint(0, 0);
+ }
+ return CSSIntPoint::FromAppUnitsRounded(
+ frame->GetOffsetTo(frame->PresShell()->GetRootFrame()));
+}
+
+int32_t HTMLImageElement::X() { return GetXY().x; }
+
+int32_t HTMLImageElement::Y() { return GetXY().y; }
+
+void HTMLImageElement::GetDecoding(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::decoding, kDecodingTableDefault->tag, aValue);
+}
+
+already_AddRefed<Promise> HTMLImageElement::Decode(ErrorResult& aRv) {
+ return nsImageLoadingContent::QueueDecodeAsync(aRv);
+}
+
+bool HTMLImageElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::crossorigin) {
+ ParseCORSValue(aValue, aResult);
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::decoding) {
+ return aResult.ParseEnumValue(aValue, kDecodingTable,
+ /* aCaseSensitive = */ false,
+ kDecodingTableDefault);
+ }
+ if (aAttribute == nsGkAtoms::loading) {
+ return ParseLoadingAttribute(aValue, aResult);
+ }
+ if (ParseImageAttribute(aAttribute, aValue, aResult)) {
+ return true;
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLImageElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapImageAlignAttributeInto(aBuilder);
+ MapImageBorderAttributeInto(aBuilder);
+ MapImageMarginAttributeInto(aBuilder);
+ MapImageSizeAttributesInto(aBuilder, MapAspectRatio::Yes);
+ MapCommonAttributesInto(aBuilder);
+}
+
+nsChangeHint HTMLImageElement::GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const {
+ nsChangeHint retval =
+ nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType);
+ if (aAttribute == nsGkAtoms::usemap || aAttribute == nsGkAtoms::ismap) {
+ retval |= nsChangeHint_ReconstructFrame;
+ } else if (aAttribute == nsGkAtoms::alt) {
+ if (aModType == MutationEvent_Binding::ADDITION ||
+ aModType == MutationEvent_Binding::REMOVAL) {
+ retval |= nsChangeHint_ReconstructFrame;
+ }
+ }
+ return retval;
+}
+
+NS_IMETHODIMP_(bool)
+HTMLImageElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry* const map[] = {
+ sCommonAttributeMap, sImageMarginSizeAttributeMap,
+ sImageBorderAttributeMap, sImageAlignAttributeMap};
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLImageElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+void HTMLImageElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && mForm &&
+ (aName == nsGkAtoms::name || aName == nsGkAtoms::id)) {
+ // remove the image from the hashtable as needed
+ if (const auto* old = GetParsedAttr(aName); old && !old->IsEmptyString()) {
+ mForm->RemoveImageElementFromTable(
+ this, nsDependentAtomString(old->GetAtomValue()));
+ }
+ }
+
+ return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue,
+ aNotify);
+}
+
+void HTMLImageElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID != kNameSpaceID_None) {
+ return nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName, aValue,
+ aOldValue,
+ aMaybeScriptedPrincipal, aNotify);
+ }
+
+ nsAttrValueOrString attrVal(aValue);
+ if (aName == nsGkAtoms::src) {
+ mSrcURI = nullptr;
+ if (aValue && !aValue->IsEmptyString()) {
+ StringToURI(attrVal.String(), OwnerDoc(), getter_AddRefs(mSrcURI));
+ }
+ }
+
+ if (aValue) {
+ AfterMaybeChangeAttr(aNameSpaceID, aName, attrVal, aOldValue,
+ aMaybeScriptedPrincipal, aNotify);
+ }
+
+ if (mForm && (aName == nsGkAtoms::name || aName == nsGkAtoms::id) && aValue &&
+ !aValue->IsEmptyString()) {
+ // add the image to the hashtable as needed
+ MOZ_ASSERT(aValue->Type() == nsAttrValue::eAtom,
+ "Expected atom value for name/id");
+ mForm->AddImageElementToTable(
+ this, nsDependentAtomString(aValue->GetAtomValue()));
+ }
+
+ bool forceReload = false;
+
+ if (aName == nsGkAtoms::loading && !mLoading) {
+ if (aValue && Loading(aValue->GetEnumValue()) == Loading::Lazy) {
+ SetLazyLoading();
+ } else if (aOldValue &&
+ Loading(aOldValue->GetEnumValue()) == Loading::Lazy) {
+ StopLazyLoading(StartLoading::Yes);
+ }
+ } else if (aName == nsGkAtoms::src && !aValue) {
+ // NOTE: regular src value changes are handled in AfterMaybeChangeAttr, so
+ // this only needs to handle unsetting the src attribute.
+ // Mark channel as urgent-start before load image if the image load is
+ // initaiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ // AfterMaybeChangeAttr handles setting src since it needs to catch
+ // img.src = img.src, so we only need to handle the unset case
+ if (InResponsiveMode()) {
+ if (mResponsiveSelector && mResponsiveSelector->Content() == this) {
+ mResponsiveSelector->SetDefaultSource(VoidString());
+ }
+ UpdateSourceSyncAndQueueImageTask(true);
+ } else {
+ // Bug 1076583 - We still behave synchronously in the non-responsive case
+ CancelImageRequests(aNotify);
+ }
+ } else if (aName == nsGkAtoms::srcset) {
+ // Mark channel as urgent-start before load image if the image load is
+ // initaiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ mSrcsetTriggeringPrincipal = aMaybeScriptedPrincipal;
+
+ PictureSourceSrcsetChanged(this, attrVal.String(), aNotify);
+ } else if (aName == nsGkAtoms::sizes) {
+ // Mark channel as urgent-start before load image if the image load is
+ // initiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ PictureSourceSizesChanged(this, attrVal.String(), aNotify);
+ } else if (aName == nsGkAtoms::decoding) {
+ // Request sync or async image decoding.
+ SetSyncDecodingHint(
+ aValue && static_cast<ImageDecodingType>(aValue->GetEnumValue()) ==
+ ImageDecodingType::Sync);
+ } else if (aName == nsGkAtoms::referrerpolicy) {
+ ReferrerPolicy referrerPolicy = GetReferrerPolicyAsEnum();
+ // FIXME(emilio): Why only when not in responsive mode? Also see below for
+ // aNotify.
+ forceReload = aNotify && !InResponsiveMode() &&
+ referrerPolicy != ReferrerPolicy::_empty &&
+ referrerPolicy != ReferrerPolicyFromAttr(aOldValue);
+ } else if (aName == nsGkAtoms::crossorigin) {
+ // FIXME(emilio): The aNotify bit seems a bit suspicious, but it is useful
+ // to avoid extra sync loads, specially in non-responsive mode. Ideally we
+ // can unify the responsive and non-responsive code paths (bug 1076583), and
+ // simplify this a bit.
+ forceReload = aNotify && GetCORSMode() != AttrValueToCORSMode(aOldValue);
+ }
+
+ if (forceReload) {
+ // Because we load image synchronously in non-responsive-mode, we need to do
+ // reload after the attribute has been set if the reload is triggered by
+ // cross origin / referrer policy changing.
+ //
+ // Mark channel as urgent-start before load image if the image load is
+ // initiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+ if (InResponsiveMode()) {
+ // Per spec, full selection runs when this changes, even though
+ // it doesn't directly affect the source selection
+ UpdateSourceSyncAndQueueImageTask(true);
+ } else if (ShouldLoadImage()) {
+ // Bug 1076583 - We still use the older synchronous algorithm in
+ // non-responsive mode. Force a new load of the image with the
+ // new cross origin policy
+ ForceReload(aNotify, IgnoreErrors());
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void HTMLImageElement::OnAttrSetButNotChanged(int32_t aNamespaceID,
+ nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, aValue, nullptr, nullptr, aNotify);
+ return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName,
+ aValue, aNotify);
+}
+
+void HTMLImageElement::AfterMaybeChangeAttr(
+ int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue,
+ const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::src) {
+ return;
+ }
+
+ // We need to force our image to reload. This must be done here, not in
+ // AfterSetAttr or BeforeSetAttr, because we want to do it even if the attr is
+ // being set to its existing value, which is normally optimized away as a
+ // no-op.
+ //
+ // If we are in responsive mode, we drop the forced reload behavior,
+ // but still trigger a image load task for img.src = img.src per
+ // spec.
+ //
+ // Both cases handle unsetting src in AfterSetAttr
+ // Mark channel as urgent-start before load image if the image load is
+ // initaiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue.String(), aMaybeScriptedPrincipal);
+
+ if (InResponsiveMode()) {
+ if (mResponsiveSelector && mResponsiveSelector->Content() == this) {
+ mResponsiveSelector->SetDefaultSource(mSrcURI, mSrcTriggeringPrincipal);
+ }
+ UpdateSourceSyncAndQueueImageTask(true);
+ } else if (aNotify && ShouldLoadImage()) {
+ // If aNotify is false, we are coming from the parser or some such place;
+ // we'll get bound after all the attributes have been set, so we'll do the
+ // sync image load from BindToTree. Skip the LoadImage call in that case.
+
+ // Note that this sync behavior is partially removed from the spec, bug
+ // 1076583
+
+ // A hack to get animations to reset. See bug 594771.
+ mNewRequestsWillNeedAnimationReset = true;
+
+ // Force image loading here, so that we'll try to load the image from
+ // network if it's set to be not cacheable.
+ // Potentially, false could be passed here rather than aNotify since
+ // UpdateState will be called by SetAttrAndNotify, but there are two
+ // obstacles to this: 1) LoadImage will end up calling
+ // UpdateState(aNotify), and we do not want it to call UpdateState(false)
+ // when aNotify is true, and 2) When this function is called by
+ // OnAttrSetButNotChanged, SetAttrAndNotify will not subsequently call
+ // UpdateState.
+ LoadSelectedImage(/* aForce = */ true, aNotify,
+ /* aAlwaysLoad = */ true);
+
+ mNewRequestsWillNeedAnimationReset = false;
+ }
+}
+
+void HTMLImageElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ // We handle image element with attribute ismap in its corresponding frame
+ // element. Set mMultipleActionsPrevented here to prevent the click event
+ // trigger the behaviors in Element::PostHandleEventForLinks
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ if (mouseEvent && mouseEvent->IsLeftClickEvent() && IsMap()) {
+ mouseEvent->mFlags.mMultipleActionsPrevented = true;
+ }
+ nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+}
+
+nsINode* HTMLImageElement::GetScopeChainParent() const {
+ if (mForm) {
+ return mForm;
+ }
+ return nsGenericHTMLElement::GetScopeChainParent();
+}
+
+bool HTMLImageElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ int32_t tabIndex = TabIndex();
+
+ if (IsInComposedDoc() && FindImageMap()) {
+ // Use tab index on individual map areas.
+ *aTabIndex = (sTabFocusModel & eTabFocus_linksMask) ? 0 : -1;
+ // Image map is not focusable itself, but flag as tabbable
+ // so that image map areas get walked into.
+ *aIsFocusable = false;
+ return false;
+ }
+
+ // Can be in tab order if tabindex >=0 and form controls are tabbable.
+ *aTabIndex = (sTabFocusModel & eTabFocus_formElementsMask) ? tabIndex : -1;
+ *aIsFocusable = IsFormControlDefaultFocusable(aWithMouse) &&
+ (tabIndex >= 0 || GetTabIndexAttrValue().isSome());
+
+ return false;
+}
+
+nsresult HTMLImageElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsImageLoadingContent::BindToTree(aContext, aParent);
+
+ UpdateFormOwner();
+
+ if (HaveSrcsetOrInPicture()) {
+ if (IsInComposedDoc() && !mInDocResponsiveContent) {
+ aContext.OwnerDoc().AddResponsiveContent(this);
+ mInDocResponsiveContent = true;
+ }
+
+ // Mark channel as urgent-start before load image if the image load is
+ // initaiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ // Run selection algorithm when an img element is inserted into a document
+ // in order to react to changes in the environment. See note of
+ // https://html.spec.whatwg.org/multipage/embedded-content.html#img-environment-changes
+ //
+ // We also do this in PictureSourceAdded() if it is in <picture>, so here
+ // we only need to do if its parent is not <picture>, even if there is no
+ // <source>.
+ if (!IsInPicture()) {
+ UpdateSourceSyncAndQueueImageTask(false);
+ }
+ } else if (!InResponsiveMode() && HasAttr(nsGkAtoms::src)) {
+ // We skip loading when our attributes were set from parser land,
+ // so trigger a aForce=false load now to check if things changed.
+ // This isn't necessary for responsive mode, since creating the
+ // image load task is asynchronous we don't need to take special
+ // care to avoid doing so when being filled by the parser.
+
+ // Mark channel as urgent-start before load image if the image load is
+ // initaiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ // We still act synchronously for the non-responsive case (Bug
+ // 1076583), but still need to delay if it is unsafe to run
+ // script.
+
+ // If loading is temporarily disabled, don't even launch MaybeLoadImage.
+ // Otherwise MaybeLoadImage may run later when someone has reenabled
+ // loading.
+ if (LoadingEnabled() && ShouldLoadImage()) {
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod<bool>("dom::HTMLImageElement::MaybeLoadImage", this,
+ &HTMLImageElement::MaybeLoadImage, false));
+ }
+ }
+
+ return rv;
+}
+
+void HTMLImageElement::UnbindFromTree(bool aNullParent) {
+ if (mForm) {
+ if (aNullParent || !FindAncestorForm(mForm)) {
+ ClearForm(true);
+ } else {
+ UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT);
+ }
+ }
+
+ if (mInDocResponsiveContent) {
+ OwnerDoc()->RemoveResponsiveContent(this);
+ mInDocResponsiveContent = false;
+ }
+
+ nsImageLoadingContent::UnbindFromTree(aNullParent);
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+void HTMLImageElement::UpdateFormOwner() {
+ if (!mForm) {
+ mForm = FindAncestorForm();
+ }
+
+ if (mForm && !HasFlag(ADDED_TO_FORM)) {
+ // Now we need to add ourselves to the form
+ nsAutoString nameVal, idVal;
+ GetAttr(nsGkAtoms::name, nameVal);
+ GetAttr(nsGkAtoms::id, idVal);
+
+ SetFlags(ADDED_TO_FORM);
+
+ mForm->AddImageElement(this);
+
+ if (!nameVal.IsEmpty()) {
+ mForm->AddImageElementToTable(this, nameVal);
+ }
+
+ if (!idVal.IsEmpty()) {
+ mForm->AddImageElementToTable(this, idVal);
+ }
+ }
+}
+
+void HTMLImageElement::MaybeLoadImage(bool aAlwaysForceLoad) {
+ // Our base URI may have changed, or we may have had responsive parameters
+ // change while not bound to the tree. However, at this moment, we should have
+ // updated the responsive source in other places, so we don't have to re-parse
+ // src/srcset here. Just need to LoadImage.
+
+ // Note, check LoadingEnabled() after LoadImage call.
+
+ LoadSelectedImage(aAlwaysForceLoad, /* aNotify */ true, aAlwaysForceLoad);
+
+ if (!LoadingEnabled()) {
+ CancelImageRequests(true);
+ }
+}
+
+void HTMLImageElement::NodeInfoChanged(Document* aOldDoc) {
+ nsGenericHTMLElement::NodeInfoChanged(aOldDoc);
+
+ // Reparse the URI if needed. Note that we can't check whether we already have
+ // a parsed URI, because it might be null even if we have a valid src
+ // attribute, if we tried to parse with a different base.
+ mSrcURI = nullptr;
+ nsAutoString src;
+ if (GetAttr(nsGkAtoms::src, src) && !src.IsEmpty()) {
+ StringToURI(src, OwnerDoc(), getter_AddRefs(mSrcURI));
+ }
+
+ if (mLazyLoading) {
+ aOldDoc->GetLazyLoadObserver()->Unobserve(*this);
+ mLazyLoading = false;
+ SetLazyLoading();
+ }
+
+ // Run selection algorithm synchronously when an img element's adopting steps
+ // are run, in order to react to changes in the environment, per spec,
+ // https://html.spec.whatwg.org/multipage/images.html#reacting-to-dom-mutations,
+ // and
+ // https://html.spec.whatwg.org/multipage/images.html#reacting-to-environment-changes.
+ if (InResponsiveMode()) {
+ UpdateResponsiveSource();
+ }
+
+ // Force reload image if adoption steps are run.
+ // If loading is temporarily disabled, don't even launch script runner.
+ // Otherwise script runner may run later when someone has reenabled loading.
+ StartLoadingIfNeeded();
+}
+
+// static
+already_AddRefed<HTMLImageElement> HTMLImageElement::Image(
+ const GlobalObject& aGlobal, const Optional<uint32_t>& aWidth,
+ const Optional<uint32_t>& aHeight, ErrorResult& aError) {
+ nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
+ Document* doc;
+ if (!win || !(doc = win->GetExtantDoc())) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo = doc->NodeInfoManager()->GetNodeInfo(
+ nsGkAtoms::img, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE);
+
+ auto* nim = nodeInfo->NodeInfoManager();
+ RefPtr<HTMLImageElement> img = new (nim) HTMLImageElement(nodeInfo.forget());
+
+ if (aWidth.WasPassed()) {
+ img->SetWidth(aWidth.Value(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ if (aHeight.WasPassed()) {
+ img->SetHeight(aHeight.Value(), aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ }
+ }
+
+ return img.forget();
+}
+
+uint32_t HTMLImageElement::Height() { return GetWidthHeightForImage().height; }
+
+uint32_t HTMLImageElement::Width() { return GetWidthHeightForImage().width; }
+
+nsIntSize HTMLImageElement::NaturalSize() {
+ if (!mCurrentRequest) {
+ return {};
+ }
+
+ nsCOMPtr<imgIContainer> image;
+ mCurrentRequest->GetImage(getter_AddRefs(image));
+ if (!image) {
+ return {};
+ }
+
+ nsIntSize size;
+ Unused << image->GetHeight(&size.height);
+ Unused << image->GetWidth(&size.width);
+
+ ImageResolution resolution = image->GetResolution();
+ // NOTE(emilio): What we implement here matches the image-set() spec, but it's
+ // unclear whether this is the right thing to do, see
+ // https://github.com/whatwg/html/pull/5574#issuecomment-826335244.
+ if (mResponsiveSelector) {
+ float density = mResponsiveSelector->GetSelectedImageDensity();
+ MOZ_ASSERT(density >= 0.0);
+ resolution.ScaleBy(density);
+ }
+
+ resolution.ApplyTo(size.width, size.height);
+ return size;
+}
+
+nsresult HTMLImageElement::CopyInnerTo(HTMLImageElement* aDest) {
+ nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // In SetAttr (called from nsGenericHTMLElement::CopyInnerTo), aDest skipped
+ // doing the image load because we passed in false for aNotify. But we
+ // really do want it to do the load, so set it up to happen once the cloning
+ // reaches a stable state.
+ if (!aDest->InResponsiveMode() && aDest->HasAttr(nsGkAtoms::src) &&
+ aDest->ShouldLoadImage()) {
+ // Mark channel as urgent-start before load image if the image load is
+ // initaiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod<bool>("dom::HTMLImageElement::MaybeLoadImage", aDest,
+ &HTMLImageElement::MaybeLoadImage, false));
+ }
+
+ return NS_OK;
+}
+
+CORSMode HTMLImageElement::GetCORSMode() {
+ return AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin));
+}
+
+JSObject* HTMLImageElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLImageElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+#ifdef DEBUG
+HTMLFormElement* HTMLImageElement::GetForm() const { return mForm; }
+#endif
+
+void HTMLImageElement::SetForm(HTMLFormElement* aForm) {
+ MOZ_ASSERT(aForm, "Don't pass null here");
+ NS_ASSERTION(!mForm,
+ "We don't support switching from one non-null form to another.");
+
+ mForm = aForm;
+}
+
+void HTMLImageElement::ClearForm(bool aRemoveFromForm) {
+ NS_ASSERTION((mForm != nullptr) == HasFlag(ADDED_TO_FORM),
+ "Form control should have had flag set correctly");
+
+ if (!mForm) {
+ return;
+ }
+
+ if (aRemoveFromForm) {
+ nsAutoString nameVal, idVal;
+ GetAttr(nsGkAtoms::name, nameVal);
+ GetAttr(nsGkAtoms::id, idVal);
+
+ mForm->RemoveImageElement(this);
+
+ if (!nameVal.IsEmpty()) {
+ mForm->RemoveImageElementFromTable(this, nameVal);
+ }
+
+ if (!idVal.IsEmpty()) {
+ mForm->RemoveImageElementFromTable(this, idVal);
+ }
+ }
+
+ UnsetFlags(ADDED_TO_FORM);
+ mForm = nullptr;
+}
+
+void HTMLImageElement::UpdateSourceSyncAndQueueImageTask(
+ bool aAlwaysLoad, const HTMLSourceElement* aSkippedSource) {
+ // Per spec, when updating the image data or reacting to environment
+ // changes, we always run the full selection (including selecting the source
+ // element and the best fit image from srcset) even if it doesn't directly
+ // affect the source selection.
+ //
+ // However, in the spec of updating the image data, the selection of image
+ // source URL is in the asynchronous part (i.e. in a microtask), and so this
+ // doesn't guarantee that the image style is correct after we flush the style
+ // synchornously. So here we update the responsive source synchronously always
+ // to make sure the image source is always up-to-date after each DOM mutation.
+ // Spec issue: https://github.com/whatwg/html/issues/8207.
+ const bool changed = UpdateResponsiveSource(aSkippedSource);
+
+ // If loading is temporarily disabled, we don't want to queue tasks
+ // that may then run when loading is re-enabled.
+ if (!LoadingEnabled() || !ShouldLoadImage()) {
+ return;
+ }
+
+ // Ensure that we don't overwrite a previous load request that requires
+ // a complete load to occur.
+ bool alwaysLoad = aAlwaysLoad;
+ if (mPendingImageLoadTask) {
+ alwaysLoad = alwaysLoad || mPendingImageLoadTask->AlwaysLoad();
+ }
+
+ if (!changed && !alwaysLoad) {
+ return;
+ }
+
+ QueueImageLoadTask(alwaysLoad);
+}
+
+bool HTMLImageElement::HaveSrcsetOrInPicture() {
+ if (HasAttr(nsGkAtoms::srcset)) {
+ return true;
+ }
+
+ return IsInPicture();
+}
+
+bool HTMLImageElement::InResponsiveMode() {
+ // When we lose srcset or leave a <picture> element, the fallback to img.src
+ // will happen from the microtask, and we should behave responsively in the
+ // interim
+ return mResponsiveSelector || mPendingImageLoadTask ||
+ HaveSrcsetOrInPicture();
+}
+
+bool HTMLImageElement::SelectedSourceMatchesLast(nsIURI* aSelectedSource) {
+ // If there was no selected source previously, we don't want to short-circuit
+ // the load. Similarly for if there is no newly selected source.
+ if (!mLastSelectedSource || !aSelectedSource) {
+ return false;
+ }
+ bool equal = false;
+ return NS_SUCCEEDED(mLastSelectedSource->Equals(aSelectedSource, &equal)) &&
+ equal;
+}
+
+nsresult HTMLImageElement::LoadSelectedImage(bool aForce, bool aNotify,
+ bool aAlwaysLoad) {
+ // In responsive mode, we have to make sure we ran the full selection algrithm
+ // before loading the selected image.
+ // Use this assertion to catch any cases we missed.
+ MOZ_ASSERT(!UpdateResponsiveSource(),
+ "The image source should be the same because we update the "
+ "responsive source synchronously");
+
+ // The density is default to 1.0 for the src attribute case.
+ double currentDensity = mResponsiveSelector
+ ? mResponsiveSelector->GetSelectedImageDensity()
+ : 1.0;
+
+ nsCOMPtr<nsIURI> selectedSource;
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal;
+ ImageLoadType type = eImageLoadType_Normal;
+ bool hasSrc = false;
+ if (mResponsiveSelector) {
+ selectedSource = mResponsiveSelector->GetSelectedImageURL();
+ triggeringPrincipal =
+ mResponsiveSelector->GetSelectedImageTriggeringPrincipal();
+ type = eImageLoadType_Imageset;
+ } else if (mSrcURI || HasAttr(nsGkAtoms::src)) {
+ hasSrc = true;
+ if (mSrcURI) {
+ selectedSource = mSrcURI;
+ if (HaveSrcsetOrInPicture()) {
+ // If we have a srcset attribute or are in a <picture> element, we
+ // always use the Imageset load type, even if we parsed no valid
+ // responsive sources from either, per spec.
+ type = eImageLoadType_Imageset;
+ }
+ triggeringPrincipal = mSrcTriggeringPrincipal;
+ }
+ }
+
+ if (!aAlwaysLoad && SelectedSourceMatchesLast(selectedSource)) {
+ // Update state when only density may have changed (i.e., the source to load
+ // hasn't changed, and we don't do any request at all). We need (apart from
+ // updating our internal state) to tell the image frame because its
+ // intrinsic size may have changed.
+ //
+ // In the case we actually trigger a new load, that load will trigger a call
+ // to nsImageFrame::NotifyNewCurrentRequest, which takes care of that for
+ // us.
+ SetDensity(currentDensity);
+ return NS_OK;
+ }
+
+ // Before we actually defer the lazy-loading
+ if (mLazyLoading) {
+ if (!selectedSource ||
+ !nsContentUtils::IsImageAvailable(this, selectedSource,
+ triggeringPrincipal, GetCORSMode())) {
+ return NS_OK;
+ }
+ StopLazyLoading(StartLoading::No);
+ }
+
+ nsresult rv = NS_ERROR_FAILURE;
+
+ // src triggers an error event on invalid URI, unlike other loads.
+ if (selectedSource || hasSrc) {
+ rv = LoadImage(selectedSource, aForce, aNotify, type, triggeringPrincipal);
+ }
+
+ mLastSelectedSource = selectedSource;
+ mCurrentDensity = currentDensity;
+
+ if (NS_FAILED(rv)) {
+ CancelImageRequests(aNotify);
+ }
+ return rv;
+}
+
+void HTMLImageElement::PictureSourceSrcsetChanged(nsIContent* aSourceNode,
+ const nsAString& aNewValue,
+ bool aNotify) {
+ MOZ_ASSERT(aSourceNode == this || IsPreviousSibling(aSourceNode, this),
+ "Should not be getting notifications for non-previous-siblings");
+
+ nsIContent* currentSrc =
+ mResponsiveSelector ? mResponsiveSelector->Content() : nullptr;
+
+ if (aSourceNode == currentSrc) {
+ // We're currently using this node as our responsive selector
+ // source.
+ nsCOMPtr<nsIPrincipal> principal;
+ if (aSourceNode == this) {
+ principal = mSrcsetTriggeringPrincipal;
+ } else if (auto* source = HTMLSourceElement::FromNode(aSourceNode)) {
+ principal = source->GetSrcsetTriggeringPrincipal();
+ }
+ mResponsiveSelector->SetCandidatesFromSourceSet(aNewValue, principal);
+ }
+
+ if (!mInDocResponsiveContent && IsInComposedDoc()) {
+ OwnerDoc()->AddResponsiveContent(this);
+ mInDocResponsiveContent = true;
+ }
+
+ // This always triggers the image update steps per the spec, even if
+ // we are not using this source.
+ UpdateSourceSyncAndQueueImageTask(true);
+}
+
+void HTMLImageElement::PictureSourceSizesChanged(nsIContent* aSourceNode,
+ const nsAString& aNewValue,
+ bool aNotify) {
+ MOZ_ASSERT(aSourceNode == this || IsPreviousSibling(aSourceNode, this),
+ "Should not be getting notifications for non-previous-siblings");
+
+ nsIContent* currentSrc =
+ mResponsiveSelector ? mResponsiveSelector->Content() : nullptr;
+
+ if (aSourceNode == currentSrc) {
+ // We're currently using this node as our responsive selector
+ // source.
+ mResponsiveSelector->SetSizesFromDescriptor(aNewValue);
+ }
+
+ // This always triggers the image update steps per the spec, even if
+ // we are not using this source.
+ UpdateSourceSyncAndQueueImageTask(true);
+}
+
+void HTMLImageElement::PictureSourceMediaOrTypeChanged(nsIContent* aSourceNode,
+ bool aNotify) {
+ MOZ_ASSERT(IsPreviousSibling(aSourceNode, this),
+ "Should not be getting notifications for non-previous-siblings");
+
+ // This always triggers the image update steps per the spec, even if
+ // we are not switching to/from this source
+ UpdateSourceSyncAndQueueImageTask(true);
+}
+
+void HTMLImageElement::PictureSourceDimensionChanged(
+ HTMLSourceElement* aSourceNode, bool aNotify) {
+ MOZ_ASSERT(IsPreviousSibling(aSourceNode, this),
+ "Should not be getting notifications for non-previous-siblings");
+
+ // "width" and "height" affect the dimension of images, but they don't have
+ // impact on the selection of <source> elements. In other words,
+ // UpdateResponsiveSource doesn't change the source, so all we need to do is
+ // just request restyle.
+ if (mResponsiveSelector && mResponsiveSelector->Content() == aSourceNode) {
+ InvalidateAttributeMapping();
+ }
+}
+
+void HTMLImageElement::PictureSourceAdded(HTMLSourceElement* aSourceNode) {
+ MOZ_ASSERT(!aSourceNode || IsPreviousSibling(aSourceNode, this),
+ "Should not be getting notifications for non-previous-siblings");
+
+ UpdateSourceSyncAndQueueImageTask(true);
+}
+
+void HTMLImageElement::PictureSourceRemoved(HTMLSourceElement* aSourceNode) {
+ MOZ_ASSERT(!aSourceNode || IsPreviousSibling(aSourceNode, this),
+ "Should not be getting notifications for non-previous-siblings");
+
+ UpdateSourceSyncAndQueueImageTask(true, aSourceNode);
+}
+
+bool HTMLImageElement::UpdateResponsiveSource(
+ const HTMLSourceElement* aSkippedSource) {
+ bool hadSelector = !!mResponsiveSelector;
+
+ nsIContent* currentSource =
+ mResponsiveSelector ? mResponsiveSelector->Content() : nullptr;
+
+ // Walk source nodes previous to ourselves if IsInPicture().
+ nsINode* candidateSource =
+ IsInPicture() ? GetParentElement()->GetFirstChild() : this;
+
+ // Initialize this as nullptr so we don't have to nullify it when runing out
+ // of siblings without finding ourself, e.g. XBL magic.
+ RefPtr<ResponsiveImageSelector> newResponsiveSelector = nullptr;
+
+ for (; candidateSource; candidateSource = candidateSource->GetNextSibling()) {
+ if (aSkippedSource == candidateSource) {
+ continue;
+ }
+
+ if (candidateSource == currentSource) {
+ // found no better source before current, re-run selection on
+ // that and keep it if it's still usable.
+ bool changed = mResponsiveSelector->SelectImage(true);
+ if (mResponsiveSelector->NumCandidates()) {
+ bool isUsableCandidate = true;
+
+ // an otherwise-usable source element may still have a media query that
+ // may not match any more.
+ if (candidateSource->IsHTMLElement(nsGkAtoms::source) &&
+ !SourceElementMatches(candidateSource->AsElement())) {
+ isUsableCandidate = false;
+ }
+
+ if (isUsableCandidate) {
+ // We are still using the current source, but the selected image may
+ // be changed, so always set the density from the selected image.
+ SetDensity(mResponsiveSelector->GetSelectedImageDensity());
+ return changed;
+ }
+ }
+
+ // no longer valid
+ newResponsiveSelector = nullptr;
+ if (candidateSource == this) {
+ // No further possibilities
+ break;
+ }
+ } else if (candidateSource == this) {
+ // We are the last possible source
+ newResponsiveSelector =
+ TryCreateResponsiveSelector(candidateSource->AsElement());
+ break;
+ } else if (auto* source = HTMLSourceElement::FromNode(candidateSource)) {
+ if (RefPtr<ResponsiveImageSelector> selector =
+ TryCreateResponsiveSelector(source)) {
+ newResponsiveSelector = selector.forget();
+ // This led to a valid source, stop
+ break;
+ }
+ }
+ }
+
+ // If we reach this point, either:
+ // - there was no selector originally, and there is not one now
+ // - there was no selector originally, and there is one now
+ // - there was a selector, and there is a different one now
+ // - there was a selector, and there is not one now
+ SetResponsiveSelector(std::move(newResponsiveSelector));
+ return hadSelector || mResponsiveSelector;
+}
+
+/*static */
+bool HTMLImageElement::SupportedPictureSourceType(const nsAString& aType) {
+ nsAutoString type;
+ nsAutoString params;
+
+ nsContentUtils::SplitMimeType(aType, type, params);
+ if (type.IsEmpty()) {
+ return true;
+ }
+
+ return imgLoader::SupportImageWithMimeType(
+ NS_ConvertUTF16toUTF8(type), AcceptedMimeTypes::IMAGES_AND_DOCUMENTS);
+}
+
+bool HTMLImageElement::SourceElementMatches(Element* aSourceElement) {
+ MOZ_ASSERT(aSourceElement->IsHTMLElement(nsGkAtoms::source));
+
+ MOZ_ASSERT(IsInPicture());
+ MOZ_ASSERT(IsPreviousSibling(aSourceElement, this));
+
+ // Check media and type
+ auto* src = static_cast<HTMLSourceElement*>(aSourceElement);
+ if (!src->MatchesCurrentMedia()) {
+ return false;
+ }
+
+ nsAutoString type;
+ return !src->GetAttr(nsGkAtoms::type, type) ||
+ SupportedPictureSourceType(type);
+}
+
+already_AddRefed<ResponsiveImageSelector>
+HTMLImageElement::TryCreateResponsiveSelector(Element* aSourceElement) {
+ nsCOMPtr<nsIPrincipal> principal;
+
+ // Skip if this is not a <source> with matching media query
+ bool isSourceTag = aSourceElement->IsHTMLElement(nsGkAtoms::source);
+ if (isSourceTag) {
+ if (!SourceElementMatches(aSourceElement)) {
+ return nullptr;
+ }
+ auto* source = HTMLSourceElement::FromNode(aSourceElement);
+ principal = source->GetSrcsetTriggeringPrincipal();
+ } else if (aSourceElement->IsHTMLElement(nsGkAtoms::img)) {
+ // Otherwise this is the <img> tag itself
+ MOZ_ASSERT(aSourceElement == this);
+ principal = mSrcsetTriggeringPrincipal;
+ }
+
+ // Skip if has no srcset or an empty srcset
+ nsString srcset;
+ if (!aSourceElement->GetAttr(nsGkAtoms::srcset, srcset)) {
+ return nullptr;
+ }
+
+ if (srcset.IsEmpty()) {
+ return nullptr;
+ }
+
+ // Try to parse
+ RefPtr<ResponsiveImageSelector> sel =
+ new ResponsiveImageSelector(aSourceElement);
+ if (!sel->SetCandidatesFromSourceSet(srcset, principal)) {
+ // No possible candidates, don't need to bother parsing sizes
+ return nullptr;
+ }
+
+ nsAutoString sizes;
+ aSourceElement->GetAttr(nsGkAtoms::sizes, sizes);
+ sel->SetSizesFromDescriptor(sizes);
+
+ // If this is the <img> tag, also pull in src as the default source
+ if (!isSourceTag) {
+ MOZ_ASSERT(aSourceElement == this);
+ if (mSrcURI) {
+ sel->SetDefaultSource(mSrcURI, mSrcTriggeringPrincipal);
+ }
+ }
+
+ return sel.forget();
+}
+
+/* static */
+bool HTMLImageElement::SelectSourceForTagWithAttrs(
+ Document* aDocument, bool aIsSourceTag, const nsAString& aSrcAttr,
+ const nsAString& aSrcsetAttr, const nsAString& aSizesAttr,
+ const nsAString& aTypeAttr, const nsAString& aMediaAttr,
+ nsAString& aResult) {
+ MOZ_ASSERT(aIsSourceTag || (aTypeAttr.IsEmpty() && aMediaAttr.IsEmpty()),
+ "Passing type or media attrs makes no sense without aIsSourceTag");
+ MOZ_ASSERT(!aIsSourceTag || aSrcAttr.IsEmpty(),
+ "Passing aSrcAttr makes no sense with aIsSourceTag set");
+
+ if (aSrcsetAttr.IsEmpty()) {
+ if (!aIsSourceTag) {
+ // For an <img> with no srcset, we would always select the src attr.
+ aResult.Assign(aSrcAttr);
+ return true;
+ }
+ // Otherwise, a <source> without srcset is never selected
+ return false;
+ }
+
+ // Would not consider source tags with unsupported media or type
+ if (aIsSourceTag &&
+ ((!aMediaAttr.IsVoid() && !HTMLSourceElement::WouldMatchMediaForDocument(
+ aMediaAttr, aDocument)) ||
+ (!aTypeAttr.IsVoid() && !SupportedPictureSourceType(aTypeAttr)))) {
+ return false;
+ }
+
+ // Using srcset or picture <source>, build a responsive selector for this tag.
+ RefPtr<ResponsiveImageSelector> sel = new ResponsiveImageSelector(aDocument);
+
+ sel->SetCandidatesFromSourceSet(aSrcsetAttr);
+ if (!aSizesAttr.IsEmpty()) {
+ sel->SetSizesFromDescriptor(aSizesAttr);
+ }
+ if (!aIsSourceTag) {
+ sel->SetDefaultSource(aSrcAttr);
+ }
+
+ if (sel->GetSelectedImageURLSpec(aResult)) {
+ return true;
+ }
+
+ if (!aIsSourceTag) {
+ // <img> tag with no match would definitively load nothing.
+ aResult.Truncate();
+ return true;
+ }
+
+ // <source> tags with no match would leave source yet-undetermined.
+ return false;
+}
+
+void HTMLImageElement::DestroyContent() {
+ // Clear mPendingImageLoadTask to avoid running LoadSelectedImage() after
+ // getting destroyed.
+ mPendingImageLoadTask = nullptr;
+
+ mResponsiveSelector = nullptr;
+
+ nsImageLoadingContent::Destroy();
+ nsGenericHTMLElement::DestroyContent();
+}
+
+void HTMLImageElement::MediaFeatureValuesChanged() {
+ UpdateSourceSyncAndQueueImageTask(false);
+}
+
+bool HTMLImageElement::ShouldLoadImage() const {
+ return OwnerDoc()->ShouldLoadImages();
+}
+
+void HTMLImageElement::SetLazyLoading() {
+ if (mLazyLoading) {
+ return;
+ }
+
+ // If scripting is disabled don't do lazy load.
+ // https://whatpr.org/html/3752/images.html#updating-the-image-data
+ //
+ // Same for printing.
+ Document* doc = OwnerDoc();
+ if (!doc->IsScriptEnabled() || doc->IsStaticDocument()) {
+ return;
+ }
+
+ doc->EnsureLazyLoadObserver().Observe(*this);
+ mLazyLoading = true;
+ UpdateImageState(true);
+}
+
+void HTMLImageElement::StartLoadingIfNeeded() {
+ if (!LoadingEnabled() || !ShouldLoadImage()) {
+ return;
+ }
+
+ // Use script runner for the case the adopt is from appendChild.
+ // Bug 1076583 - We still behave synchronously in the non-responsive case
+ nsContentUtils::AddScriptRunner(
+ InResponsiveMode()
+ ? NewRunnableMethod<bool>("dom::HTMLImageElement::QueueImageLoadTask",
+ this, &HTMLImageElement::QueueImageLoadTask,
+ true)
+ : NewRunnableMethod<bool>("dom::HTMLImageElement::MaybeLoadImage",
+ this, &HTMLImageElement::MaybeLoadImage,
+ true));
+}
+
+void HTMLImageElement::StopLazyLoading(StartLoading aStartLoading) {
+ if (!mLazyLoading) {
+ return;
+ }
+ mLazyLoading = false;
+ Document* doc = OwnerDoc();
+ if (auto* obs = doc->GetLazyLoadObserver()) {
+ obs->Unobserve(*this);
+ }
+
+ if (aStartLoading == StartLoading::Yes) {
+ StartLoadingIfNeeded();
+ }
+}
+
+const StyleLockedDeclarationBlock*
+HTMLImageElement::GetMappedAttributesFromSource() const {
+ if (!IsInPicture() || !mResponsiveSelector) {
+ return nullptr;
+ }
+
+ const auto* source =
+ HTMLSourceElement::FromNodeOrNull(mResponsiveSelector->Content());
+ if (!source) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(IsPreviousSibling(source, this),
+ "Incorrect or out-of-date source");
+ return source->GetAttributesMappedForImage();
+}
+
+void HTMLImageElement::InvalidateAttributeMapping() {
+ if (!IsInPicture()) {
+ return;
+ }
+
+ nsPresContext* presContext = nsContentUtils::GetContextForContent(this);
+ if (!presContext) {
+ return;
+ }
+
+ // Note: Unfortunately, we have to use RESTYLE_SELF, instead of using
+ // RESTYLE_STYLE_ATTRIBUTE or other ways, to avoid re-selector-match because
+ // we are using Gecko_GetExtraContentStyleDeclarations() to retrieve the
+ // extra declaration block from |this|'s width and height attributes, and
+ // other restyle hints seems not enough.
+ // FIXME: We may refine this together with the restyle for presentation
+ // attributes in RestyleManger::AttributeChagned()
+ presContext->RestyleManager()->PostRestyleEvent(
+ this, RestyleHint::RESTYLE_SELF, nsChangeHint(0));
+}
+
+void HTMLImageElement::SetResponsiveSelector(
+ RefPtr<ResponsiveImageSelector>&& aSource) {
+ if (mResponsiveSelector == aSource) {
+ return;
+ }
+
+ mResponsiveSelector = std::move(aSource);
+
+ // Invalidate the style if needed.
+ InvalidateAttributeMapping();
+
+ // Update density.
+ SetDensity(mResponsiveSelector
+ ? mResponsiveSelector->GetSelectedImageDensity()
+ : 1.0);
+}
+
+void HTMLImageElement::SetDensity(double aDensity) {
+ if (mCurrentDensity == aDensity) {
+ return;
+ }
+
+ mCurrentDensity = aDensity;
+
+ // Invalidate the reflow.
+ if (nsImageFrame* f = do_QueryFrame(GetPrimaryFrame())) {
+ f->ResponsiveContentDensityChanged();
+ }
+}
+
+void HTMLImageElement::QueueImageLoadTask(bool aAlwaysLoad) {
+ RefPtr<ImageLoadTask> task =
+ new ImageLoadTask(this, aAlwaysLoad, mUseUrgentStartForChannel);
+ // The task checks this to determine if it was the last
+ // queued event, and so earlier tasks are implicitly canceled.
+ mPendingImageLoadTask = task;
+ CycleCollectedJSContext::Get()->DispatchToMicroTask(task.forget());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLImageElement.h b/dom/html/HTMLImageElement.h
new file mode 100644
index 0000000000..8e10c6867e
--- /dev/null
+++ b/dom/html/HTMLImageElement.h
@@ -0,0 +1,428 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLImageElement_h
+#define mozilla_dom_HTMLImageElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsImageLoadingContent.h"
+#include "Units.h"
+#include "nsCycleCollectionParticipant.h"
+
+namespace mozilla {
+class EventChainPreVisitor;
+namespace dom {
+
+class ImageLoadTask;
+
+class ResponsiveImageSelector;
+class HTMLImageElement final : public nsGenericHTMLElement,
+ public nsImageLoadingContent {
+ friend class HTMLSourceElement;
+ friend class HTMLPictureElement;
+ friend class ImageLoadTask;
+
+ public:
+ explicit HTMLImageElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ static already_AddRefed<HTMLImageElement> Image(
+ const GlobalObject& aGlobal, const Optional<uint32_t>& aWidth,
+ const Optional<uint32_t>& aHeight, ErrorResult& aError);
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLImageElement,
+ nsGenericHTMLElement)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ bool Draggable() const override;
+
+ ResponsiveImageSelector* GetResponsiveImageSelector() {
+ return mResponsiveSelector.get();
+ }
+
+ // Element
+ bool IsInteractiveHTMLContent() const override;
+
+ // EventTarget
+ void AsyncEventRunning(AsyncEventDispatcher* aEvent) override;
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLImageElement, img)
+
+ // override from nsImageLoadingContent
+ CORSMode GetCORSMode() override;
+
+ // nsIContent
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+ nsINode* GetScopeChainParent() const override;
+
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent) override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ void NodeInfoChanged(Document* aOldDoc) override;
+
+ nsresult CopyInnerTo(HTMLImageElement* aDest);
+
+ void MaybeLoadImage(bool aAlwaysForceLoad);
+
+ bool IsMap() { return GetBoolAttr(nsGkAtoms::ismap); }
+ void SetIsMap(bool aIsMap, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::ismap, aIsMap, aError);
+ }
+ MOZ_CAN_RUN_SCRIPT uint32_t Width();
+ void SetWidth(uint32_t aWidth, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::width, aWidth, 0, aError);
+ }
+ MOZ_CAN_RUN_SCRIPT uint32_t Height();
+ void SetHeight(uint32_t aHeight, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::height, aHeight, 0, aError);
+ }
+
+ nsIntSize NaturalSize();
+ uint32_t NaturalHeight() { return NaturalSize().height; }
+ uint32_t NaturalWidth() { return NaturalSize().width; }
+
+ bool Complete();
+ uint32_t Hspace() {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::hspace, 0);
+ }
+ void SetHspace(uint32_t aHspace, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::hspace, aHspace, 0, aError);
+ }
+ uint32_t Vspace() {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::vspace, 0);
+ }
+ void SetVspace(uint32_t aVspace, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::vspace, aVspace, 0, aError);
+ }
+
+ void GetAlt(nsAString& aAlt) { GetHTMLAttr(nsGkAtoms::alt, aAlt); }
+ void SetAlt(const nsAString& aAlt, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::alt, aAlt, aError);
+ }
+ void GetSrc(nsAString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); }
+ void SetSrc(const nsAString& aSrc, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aError);
+ }
+ void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal,
+ ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aError);
+ }
+ void GetSrcset(nsAString& aSrcset) {
+ GetHTMLAttr(nsGkAtoms::srcset, aSrcset);
+ }
+ void SetSrcset(const nsAString& aSrcset, nsIPrincipal* aTriggeringPrincipal,
+ ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::srcset, aSrcset, aTriggeringPrincipal, aError);
+ }
+ void GetCrossOrigin(nsAString& aResult) {
+ // Null for both missing and invalid defaults is ok, since we
+ // always parse to an enum value, so we don't need an invalid
+ // default, and we _want_ the missing default to be null.
+ GetEnumAttr(nsGkAtoms::crossorigin, nullptr, aResult);
+ }
+ void SetCrossOrigin(const nsAString& aCrossOrigin, ErrorResult& aError) {
+ SetOrRemoveNullableStringAttr(nsGkAtoms::crossorigin, aCrossOrigin, aError);
+ }
+ void GetUseMap(nsAString& aUseMap) {
+ GetHTMLAttr(nsGkAtoms::usemap, aUseMap);
+ }
+ void SetUseMap(const nsAString& aUseMap, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::usemap, aUseMap, aError);
+ }
+ void GetName(nsAString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); }
+ void SetName(const nsAString& aName, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aError);
+ }
+ void GetAlign(nsAString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); }
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+ void GetLongDesc(nsAString& aLongDesc) {
+ GetURIAttr(nsGkAtoms::longdesc, nullptr, aLongDesc);
+ }
+ void SetLongDesc(const nsAString& aLongDesc, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::longdesc, aLongDesc, aError);
+ }
+ void GetSizes(nsAString& aSizes) { GetHTMLAttr(nsGkAtoms::sizes, aSizes); }
+ void SetSizes(const nsAString& aSizes, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::sizes, aSizes, aError);
+ }
+ void GetCurrentSrc(nsAString& aValue);
+ void GetBorder(nsAString& aBorder) {
+ GetHTMLAttr(nsGkAtoms::border, aBorder);
+ }
+ void SetBorder(const nsAString& aBorder, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::border, aBorder, aError);
+ }
+ void SetReferrerPolicy(const nsAString& aReferrer, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::referrerpolicy, aReferrer, aError);
+ }
+ void GetReferrerPolicy(nsAString& aReferrer) {
+ GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrer);
+ }
+ void SetDecoding(const nsAString& aDecoding, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::decoding, aDecoding, aError);
+ }
+ void GetDecoding(nsAString& aValue);
+
+ void SetLoading(const nsAString& aLoading, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::loading, aLoading, aError);
+ }
+
+ bool IsAwaitingLoadOrLazyLoading() const {
+ return mLazyLoading || mPendingImageLoadTask;
+ }
+
+ bool IsLazyLoading() const { return mLazyLoading; }
+
+ already_AddRefed<Promise> Decode(ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT int32_t X();
+ MOZ_CAN_RUN_SCRIPT int32_t Y();
+ void GetLowsrc(nsAString& aLowsrc) {
+ GetURIAttr(nsGkAtoms::lowsrc, nullptr, aLowsrc);
+ }
+ void SetLowsrc(const nsAString& aLowsrc, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::lowsrc, aLowsrc, aError);
+ }
+
+#ifdef DEBUG
+ HTMLFormElement* GetForm() const;
+#endif
+ void SetForm(HTMLFormElement* aForm);
+ void ClearForm(bool aRemoveFromForm);
+
+ void DestroyContent() override;
+
+ void MediaFeatureValuesChanged();
+
+ /**
+ * Given a hypothetical <img> or <source> tag with the given parameters,
+ * return what URI we would attempt to use, if any. Used by the preloader to
+ * resolve sources prior to DOM creation.
+ *
+ * @param aDocument The document this image would be for, for referencing
+ * viewport width and DPI/zoom
+ * @param aIsSourceTag If these parameters are for a <source> tag (as in a
+ * <picture>) rather than an <img> tag. Note that some attrs are unused
+ * when this is true an vice versa
+ * @param aSrcAttr [ignored if aIsSourceTag] The src attr for this image.
+ * @param aSrcsetAttr The srcset attr for this image/source
+ * @param aSizesAttr The sizes attr for this image/source
+ * @param aTypeAttr [ignored if !aIsSourceTag] The type attr for this source.
+ * Should be a void string to differentiate no type attribute
+ * from an empty one.
+ * @param aMediaAttr [ignored if !aIsSourceTag] The media attr for this
+ * source. Should be a void string to differentiate no
+ * media attribute from an empty one.
+ * @param aResult A reference to store the resulting URL spec in if we
+ * selected a source. This value is not guaranteed to parse to
+ * a valid URL, merely the URL that the tag would attempt to
+ * resolve and load (which may be the empty string). This
+ * parameter is not modified if return value is false.
+ * @return True if we were able to select a final source, false if further
+ * sources would be considered. It follows that this always returns
+ * true if !aIsSourceTag.
+ *
+ * Note that the return value may be true with an empty string as the result,
+ * which implies that the parameters provided describe a tag that would select
+ * no source. This is distinct from a return of false which implies that
+ * further <source> or <img> tags would be considered.
+ */
+ static bool SelectSourceForTagWithAttrs(
+ Document* aDocument, bool aIsSourceTag, const nsAString& aSrcAttr,
+ const nsAString& aSrcsetAttr, const nsAString& aSizesAttr,
+ const nsAString& aTypeAttr, const nsAString& aMediaAttr,
+ nsAString& aResult);
+
+ enum class FromIntersectionObserver : bool { No, Yes };
+ enum class StartLoading : bool { No, Yes };
+ void StopLazyLoading(StartLoading);
+
+ // This is used when restyling, for retrieving the extra style from the source
+ // element.
+ const StyleLockedDeclarationBlock* GetMappedAttributesFromSource() const;
+
+ protected:
+ virtual ~HTMLImageElement();
+
+ // Update the responsive source synchronously and queues a task to run
+ // LoadSelectedImage pending stable state.
+ //
+ // Pending Bug 1076583 this is only used by the responsive image
+ // algorithm (InResponsiveMode()) -- synchronous actions when just
+ // using img.src will bypass this, and update source and kick off
+ // image load synchronously.
+ void UpdateSourceSyncAndQueueImageTask(
+ bool aAlwaysLoad, const HTMLSourceElement* aSkippedSource = nullptr);
+
+ // True if we have a srcset attribute or a <picture> parent, regardless of if
+ // any valid responsive sources were parsed from either.
+ bool HaveSrcsetOrInPicture();
+
+ // True if we are using the newer image loading algorithm. This will be the
+ // only mode after Bug 1076583
+ bool InResponsiveMode();
+
+ // True if the given URL equals the last URL that was loaded by this element.
+ bool SelectedSourceMatchesLast(nsIURI* aSelectedSource);
+
+ // Load the current mResponsiveSelector (responsive mode) or src attr image.
+ // Note: This doesn't run the full selection for the responsive selector.
+ nsresult LoadSelectedImage(bool aForce, bool aNotify, bool aAlwaysLoad);
+
+ // True if this string represents a type we would support on <source type>
+ static bool SupportedPictureSourceType(const nsAString& aType);
+
+ // Update/create/destroy mResponsiveSelector
+ void PictureSourceSrcsetChanged(nsIContent* aSourceNode,
+ const nsAString& aNewValue, bool aNotify);
+ void PictureSourceSizesChanged(nsIContent* aSourceNode,
+ const nsAString& aNewValue, bool aNotify);
+ // As we re-run the source selection on these mutations regardless,
+ // we don't actually care which changed or to what
+ void PictureSourceMediaOrTypeChanged(nsIContent* aSourceNode, bool aNotify);
+
+ // This is called when we update "width" or "height" attribute of source
+ // element.
+ void PictureSourceDimensionChanged(HTMLSourceElement* aSourceNode,
+ bool aNotify);
+
+ void PictureSourceAdded(HTMLSourceElement* aSourceNode = nullptr);
+ // This should be called prior to the unbind, such that nextsibling works
+ void PictureSourceRemoved(HTMLSourceElement* aSourceNode = nullptr);
+
+ // Re-evaluates all source nodes (picture <source>,<img>) and finds
+ // the best source set for mResponsiveSelector. If a better source
+ // is found, creates a new selector and feeds the source to it. If
+ // the current ResponsiveSelector is not changed, runs
+ // SelectImage(true) to re-evaluate its candidates.
+ //
+ // Because keeping the existing selector is the common case (and we
+ // often do no-op reselections), this does not re-parse values for
+ // the existing mResponsiveSelector, meaning you need to update its
+ // parameters as appropriate before calling (or null it out to force
+ // recreation)
+ //
+ // if |aSkippedSource| is non-null, we will skip it when running the
+ // algorithm. This is used when we need to update the source when we are
+ // removing the source element.
+ //
+ // Returns true if the source has changed, and false otherwise.
+ bool UpdateResponsiveSource(
+ const HTMLSourceElement* aSkippedSource = nullptr);
+
+ // Given a <source> node that is a previous sibling *or* ourselves, try to
+ // create a ResponsiveSelector.
+
+ // If the node's srcset/sizes make for an invalid selector, returns
+ // nullptr. This does not guarantee the resulting selector matches an image,
+ // only that it is valid.
+ already_AddRefed<ResponsiveImageSelector> TryCreateResponsiveSelector(
+ Element* aSourceElement);
+
+ MOZ_CAN_RUN_SCRIPT CSSIntPoint GetXY();
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+ void UpdateFormOwner();
+
+ void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+ void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) override;
+
+ // Override for nsImageLoadingContent.
+ nsIContent* AsContent() override { return this; }
+
+ // Created when we're tracking responsive image state
+ RefPtr<ResponsiveImageSelector> mResponsiveSelector;
+
+ // This is a weak reference that this element and the HTMLFormElement
+ // cooperate in maintaining.
+ HTMLFormElement* mForm = nullptr;
+
+ private:
+ bool SourceElementMatches(Element* aSourceElement);
+
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+ /**
+ * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
+ * It will not be called if the value is being unset.
+ *
+ * @param aNamespaceID the namespace of the attr being set
+ * @param aName the localname of the attribute being set
+ * @param aValue the value it's being set to represented as either a string or
+ * a parsed nsAttrValue.
+ * @param aOldValue the value previously set. Will be null if no value was
+ * previously set. This value should only be used when
+ * aValueMaybeChanged is true; when aValueMaybeChanged is false,
+ * aOldValue should be considered unreliable.
+ * @param aNotify Whether we plan to notify document observers.
+ */
+ void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify);
+
+ bool ShouldLoadImage() const;
+
+ // Set this image as a lazy load image due to loading="lazy".
+ void SetLazyLoading();
+
+ void StartLoadingIfNeeded();
+
+ bool IsInPicture() const {
+ return GetParentElement() &&
+ GetParentElement()->IsHTMLElement(nsGkAtoms::picture);
+ }
+
+ void InvalidateAttributeMapping();
+
+ void SetResponsiveSelector(RefPtr<ResponsiveImageSelector>&& aSource);
+ void SetDensity(double aDensity);
+
+ // Queue an image load task (via microtask).
+ void QueueImageLoadTask(bool aAlwaysLoad);
+
+ RefPtr<ImageLoadTask> mPendingImageLoadTask;
+ nsCOMPtr<nsIURI> mSrcURI;
+ nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal;
+ nsCOMPtr<nsIPrincipal> mSrcsetTriggeringPrincipal;
+
+ // Last URL that was attempted to load by this element.
+ nsCOMPtr<nsIURI> mLastSelectedSource;
+ // Last pixel density that was selected.
+ double mCurrentDensity = 1.0;
+ bool mInDocResponsiveContent = false;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_HTMLImageElement_h */
diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp
new file mode 100644
index 0000000000..4e7241ec7a
--- /dev/null
+++ b/dom/html/HTMLInputElement.cpp
@@ -0,0 +1,7407 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLInputElement.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/AutocompleteInfoBinding.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/Directory.h"
+#include "mozilla/dom/DocumentOrShadowRoot.h"
+#include "mozilla/dom/ElementBinding.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/FormData.h"
+#include "mozilla/dom/GetFilesHelper.h"
+#include "mozilla/dom/NumericInputTypes.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/InputType.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/dom/WheelEventBinding.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_signon.h"
+#include "mozilla/TextUtils.h"
+#include "mozilla/Try.h"
+#include "nsAttrValueInlines.h"
+#include "nsCRTGlue.h"
+#include "nsIFilePicker.h"
+#include "nsNetUtil.h"
+#include "nsQueryObject.h"
+
+#include "HTMLDataListElement.h"
+#include "HTMLFormSubmissionConstants.h"
+#include "mozilla/Telemetry.h"
+#include "nsBaseCommandController.h"
+#include "nsIStringBundle.h"
+#include "nsFocusManager.h"
+#include "nsColorControlFrame.h"
+#include "nsNumberControlFrame.h"
+#include "nsSearchControlFrame.h"
+#include "nsPIDOMWindow.h"
+#include "nsRepeatService.h"
+#include "nsContentCID.h"
+#include "mozilla/dom/ProgressEvent.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsPresContext.h"
+#include "nsIFormControl.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLDataListElement.h"
+#include "mozilla/dom/HTMLOptionElement.h"
+#include "nsIFormControlFrame.h"
+#include "nsITextControlFrame.h"
+#include "nsIFrame.h"
+#include "nsRangeFrame.h"
+#include "nsError.h"
+#include "nsIEditor.h"
+#include "nsIPromptCollection.h"
+
+#include "mozilla/PresState.h"
+#include "nsLinebreakConverter.h" //to strip out carriage returns
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsVariant.h"
+
+#include "mozilla/ContentEvents.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/InternalMutationEvent.h"
+#include "mozilla/TextControlState.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TouchEvents.h"
+
+#include <algorithm>
+
+// input type=radio
+#include "mozilla/dom/RadioGroupContainer.h"
+#include "nsIRadioVisitor.h"
+#include "nsRadioVisitor.h"
+
+// input type=file
+#include "mozilla/dom/FileSystemEntry.h"
+#include "mozilla/dom/FileSystem.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FileList.h"
+#include "nsIFile.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIContentPrefService2.h"
+#include "nsIMIMEService.h"
+#include "nsIObserverService.h"
+
+// input type=image
+#include "nsImageLoadingContent.h"
+#include "imgRequestProxy.h"
+
+#include "mozAutoDocUpdate.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/DirectionalityUtils.h"
+
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/MathAlgorithms.h"
+
+#include <limits>
+
+#include "nsIColorPicker.h"
+#include "nsIStringEnumerator.h"
+#include "HTMLSplitOnSpacesTokenizer.h"
+#include "nsIMIMEInfo.h"
+#include "nsFrameSelection.h"
+#include "nsXULControllers.h"
+
+// input type=date
+#include "js/Date.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Input)
+
+// XXX align=left, hspace, vspace, border? other nav4 attrs
+
+namespace mozilla::dom {
+
+// First bits are needed for the control type.
+#define NS_OUTER_ACTIVATE_EVENT (1 << 9)
+#define NS_ORIGINAL_CHECKED_VALUE (1 << 10)
+// (1 << 11 is unused)
+#define NS_ORIGINAL_INDETERMINATE_VALUE (1 << 12)
+#define NS_PRE_HANDLE_BLUR_EVENT (1 << 13)
+#define NS_IN_SUBMIT_CLICK (1 << 15)
+#define NS_CONTROL_TYPE(bits) \
+ ((bits) & ~(NS_OUTER_ACTIVATE_EVENT | NS_ORIGINAL_CHECKED_VALUE | \
+ NS_ORIGINAL_INDETERMINATE_VALUE | NS_PRE_HANDLE_BLUR_EVENT | \
+ NS_IN_SUBMIT_CLICK))
+
+// whether textfields should be selected once focused:
+// -1: no, 1: yes, 0: uninitialized
+static int32_t gSelectTextFieldOnFocus;
+UploadLastDir* HTMLInputElement::gUploadLastDir;
+
+static const nsAttrValue::EnumTable kInputTypeTable[] = {
+ {"button", FormControlType::InputButton},
+ {"checkbox", FormControlType::InputCheckbox},
+ {"color", FormControlType::InputColor},
+ {"date", FormControlType::InputDate},
+ {"datetime-local", FormControlType::InputDatetimeLocal},
+ {"email", FormControlType::InputEmail},
+ {"file", FormControlType::InputFile},
+ {"hidden", FormControlType::InputHidden},
+ {"reset", FormControlType::InputReset},
+ {"image", FormControlType::InputImage},
+ {"month", FormControlType::InputMonth},
+ {"number", FormControlType::InputNumber},
+ {"password", FormControlType::InputPassword},
+ {"radio", FormControlType::InputRadio},
+ {"range", FormControlType::InputRange},
+ {"search", FormControlType::InputSearch},
+ {"submit", FormControlType::InputSubmit},
+ {"tel", FormControlType::InputTel},
+ {"time", FormControlType::InputTime},
+ {"url", FormControlType::InputUrl},
+ {"week", FormControlType::InputWeek},
+ // "text" must be last for ParseAttribute to work right. If you add things
+ // before it, please update kInputDefaultType.
+ {"text", FormControlType::InputText},
+ {nullptr, 0}};
+
+// Default type is 'text'.
+static const nsAttrValue::EnumTable* kInputDefaultType =
+ &kInputTypeTable[ArrayLength(kInputTypeTable) - 2];
+
+static const nsAttrValue::EnumTable kCaptureTable[] = {
+ {"user", nsIFilePicker::captureUser},
+ {"environment", nsIFilePicker::captureEnv},
+ {"", nsIFilePicker::captureDefault},
+ {nullptr, nsIFilePicker::captureNone}};
+
+static const nsAttrValue::EnumTable* kCaptureDefault = &kCaptureTable[2];
+
+using namespace blink;
+
+constexpr Decimal HTMLInputElement::kStepScaleFactorDate(86400000_d);
+constexpr Decimal HTMLInputElement::kStepScaleFactorNumberRange(1_d);
+constexpr Decimal HTMLInputElement::kStepScaleFactorTime(1000_d);
+constexpr Decimal HTMLInputElement::kStepScaleFactorMonth(1_d);
+constexpr Decimal HTMLInputElement::kStepScaleFactorWeek(7 * 86400000_d);
+constexpr Decimal HTMLInputElement::kDefaultStepBase(0_d);
+constexpr Decimal HTMLInputElement::kDefaultStepBaseWeek(-259200000_d);
+constexpr Decimal HTMLInputElement::kDefaultStep(1_d);
+constexpr Decimal HTMLInputElement::kDefaultStepTime(60_d);
+constexpr Decimal HTMLInputElement::kStepAny(0_d);
+
+const double HTMLInputElement::kMinimumYear = 1;
+const double HTMLInputElement::kMaximumYear = 275760;
+const double HTMLInputElement::kMaximumWeekInMaximumYear = 37;
+const double HTMLInputElement::kMaximumDayInMaximumYear = 13;
+const double HTMLInputElement::kMaximumMonthInMaximumYear = 9;
+const double HTMLInputElement::kMaximumWeekInYear = 53;
+const double HTMLInputElement::kMsPerDay = 24 * 60 * 60 * 1000;
+
+// An helper class for the dispatching of the 'change' event.
+// This class is used when the FilePicker finished its task (or when files and
+// directories are set by some chrome/test only method).
+// The task of this class is to postpone the dispatching of 'change' and 'input'
+// events at the end of the exploration of the directories.
+class DispatchChangeEventCallback final : public GetFilesCallback {
+ public:
+ explicit DispatchChangeEventCallback(HTMLInputElement* aInputElement)
+ : mInputElement(aInputElement) {
+ MOZ_ASSERT(aInputElement);
+ }
+
+ virtual void Callback(
+ nsresult aStatus,
+ const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) override {
+ if (!mInputElement->GetOwnerGlobal()) {
+ return;
+ }
+
+ nsTArray<OwningFileOrDirectory> array;
+ for (uint32_t i = 0; i < aBlobImpls.Length(); ++i) {
+ OwningFileOrDirectory* element = array.AppendElement();
+ RefPtr<File> file =
+ File::Create(mInputElement->GetOwnerGlobal(), aBlobImpls[i]);
+ if (NS_WARN_IF(!file)) {
+ return;
+ }
+
+ element->SetAsFile() = file;
+ }
+
+ mInputElement->SetFilesOrDirectories(array, true);
+ Unused << NS_WARN_IF(NS_FAILED(DispatchEvents()));
+ }
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult DispatchEvents() {
+ RefPtr<HTMLInputElement> inputElement(mInputElement);
+ nsresult rv = nsContentUtils::DispatchInputEvent(inputElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch input event");
+ mInputElement->SetUserInteracted(true);
+ rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(),
+ mInputElement, u"change"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+
+ return rv;
+ }
+
+ private:
+ RefPtr<HTMLInputElement> mInputElement;
+};
+
+struct HTMLInputElement::FileData {
+ /**
+ * The value of the input if it is a file input. This is the list of files or
+ * directories DOM objects used when uploading a file. It is vital that this
+ * is kept separate from mValue so that it won't be possible to 'leak' the
+ * value from a text-input to a file-input. Additionally, the logic for this
+ * value is kept as simple as possible to avoid accidental errors where the
+ * wrong filename is used. Therefor the list of filenames is always owned by
+ * this member, never by the frame. Whenever the frame wants to change the
+ * filename it has to call SetFilesOrDirectories to update this member.
+ */
+ nsTArray<OwningFileOrDirectory> mFilesOrDirectories;
+
+ RefPtr<GetFilesHelper> mGetFilesRecursiveHelper;
+ RefPtr<GetFilesHelper> mGetFilesNonRecursiveHelper;
+
+ /**
+ * Hack for bug 1086684: Stash the .value when we're a file picker.
+ */
+ nsString mFirstFilePath;
+
+ RefPtr<FileList> mFileList;
+ Sequence<RefPtr<FileSystemEntry>> mEntries;
+
+ nsString mStaticDocFileList;
+
+ void ClearGetFilesHelpers() {
+ if (mGetFilesRecursiveHelper) {
+ mGetFilesRecursiveHelper->Unlink();
+ mGetFilesRecursiveHelper = nullptr;
+ }
+
+ if (mGetFilesNonRecursiveHelper) {
+ mGetFilesNonRecursiveHelper->Unlink();
+ mGetFilesNonRecursiveHelper = nullptr;
+ }
+ }
+
+ // Cycle Collection support.
+ void Traverse(nsCycleCollectionTraversalCallback& cb) {
+ FileData* tmp = this;
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFilesOrDirectories)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFileList)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEntries)
+ if (mGetFilesRecursiveHelper) {
+ mGetFilesRecursiveHelper->Traverse(cb);
+ }
+
+ if (mGetFilesNonRecursiveHelper) {
+ mGetFilesNonRecursiveHelper->Traverse(cb);
+ }
+ }
+
+ void Unlink() {
+ FileData* tmp = this;
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mFilesOrDirectories)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mFileList)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEntries)
+ ClearGetFilesHelpers();
+ }
+};
+
+HTMLInputElement::nsFilePickerShownCallback::nsFilePickerShownCallback(
+ HTMLInputElement* aInput, nsIFilePicker* aFilePicker)
+ : mFilePicker(aFilePicker), mInput(aInput) {}
+
+NS_IMPL_ISUPPORTS(UploadLastDir::ContentPrefCallback, nsIContentPrefCallback2)
+
+NS_IMETHODIMP
+UploadLastDir::ContentPrefCallback::HandleCompletion(uint16_t aReason) {
+ nsCOMPtr<nsIFile> localFile;
+ nsAutoString prefStr;
+
+ if (aReason == nsIContentPrefCallback2::COMPLETE_ERROR || !mResult) {
+ Preferences::GetString("dom.input.fallbackUploadDir", prefStr);
+ }
+
+ if (prefStr.IsEmpty() && mResult) {
+ nsCOMPtr<nsIVariant> pref;
+ mResult->GetValue(getter_AddRefs(pref));
+ pref->GetAsAString(prefStr);
+ }
+
+ if (!prefStr.IsEmpty()) {
+ localFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID);
+ if (localFile && NS_WARN_IF(NS_FAILED(localFile->InitWithPath(prefStr)))) {
+ localFile = nullptr;
+ }
+ }
+
+ if (localFile) {
+ mFilePicker->SetDisplayDirectory(localFile);
+ } else {
+ // If no custom directory was set through the pref, default to
+ // "desktop" directory for each platform.
+ mFilePicker->SetDisplaySpecialDirectory(
+ NS_LITERAL_STRING_FROM_CSTRING(NS_OS_DESKTOP_DIR));
+ }
+
+ mFilePicker->Open(mFpCallback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UploadLastDir::ContentPrefCallback::HandleResult(nsIContentPref* pref) {
+ mResult = pref;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UploadLastDir::ContentPrefCallback::HandleError(nsresult error) {
+ // HandleCompletion is always called (even with HandleError was called),
+ // so we don't need to do anything special here.
+ return NS_OK;
+}
+
+namespace {
+
+/**
+ * This may return nullptr if the DOM File's implementation of
+ * File::mozFullPathInternal does not successfully return a non-empty
+ * string that is a valid path. This can happen on Firefox OS, for example,
+ * where the file picker can create Blobs.
+ */
+static already_AddRefed<nsIFile> LastUsedDirectory(
+ const OwningFileOrDirectory& aData) {
+ if (aData.IsFile()) {
+ nsAutoString path;
+ ErrorResult error;
+ aData.GetAsFile()->GetMozFullPathInternal(path, error);
+ if (error.Failed() || path.IsEmpty()) {
+ error.SuppressException();
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIFile> localFile;
+ nsresult rv = NS_NewLocalFile(path, true, getter_AddRefs(localFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIFile> parentFile;
+ rv = localFile->GetParent(getter_AddRefs(parentFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ return parentFile.forget();
+ }
+
+ MOZ_ASSERT(aData.IsDirectory());
+
+ nsCOMPtr<nsIFile> localFile = aData.GetAsDirectory()->GetInternalNsIFile();
+ MOZ_ASSERT(localFile);
+
+ return localFile.forget();
+}
+
+void GetDOMFileOrDirectoryName(const OwningFileOrDirectory& aData,
+ nsAString& aName) {
+ if (aData.IsFile()) {
+ aData.GetAsFile()->GetName(aName);
+ } else {
+ MOZ_ASSERT(aData.IsDirectory());
+ ErrorResult rv;
+ aData.GetAsDirectory()->GetName(aName, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ }
+ }
+}
+
+void GetDOMFileOrDirectoryPath(const OwningFileOrDirectory& aData,
+ nsAString& aPath, ErrorResult& aRv) {
+ if (aData.IsFile()) {
+ aData.GetAsFile()->GetMozFullPathInternal(aPath, aRv);
+ } else {
+ MOZ_ASSERT(aData.IsDirectory());
+ aData.GetAsDirectory()->GetFullRealPath(aPath);
+ }
+}
+
+} // namespace
+
+NS_IMETHODIMP
+HTMLInputElement::nsFilePickerShownCallback::Done(
+ nsIFilePicker::ResultCode aResult) {
+ mInput->PickerClosed();
+
+ if (aResult == nsIFilePicker::returnCancel) {
+ RefPtr<HTMLInputElement> inputElement(mInput);
+ return nsContentUtils::DispatchTrustedEvent(
+ inputElement->OwnerDoc(), inputElement, u"cancel"_ns, CanBubble::eYes,
+ Cancelable::eNo);
+ }
+
+ mInput->OwnerDoc()->NotifyUserGestureActivation();
+
+ nsIFilePicker::Mode mode;
+ mFilePicker->GetMode(&mode);
+
+ // Collect new selected filenames
+ nsTArray<OwningFileOrDirectory> newFilesOrDirectories;
+ if (mode == nsIFilePicker::modeOpenMultiple) {
+ nsCOMPtr<nsISimpleEnumerator> iter;
+ nsresult rv =
+ mFilePicker->GetDomFileOrDirectoryEnumerator(getter_AddRefs(iter));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!iter) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISupports> tmp;
+ bool hasMore = true;
+
+ while (NS_SUCCEEDED(iter->HasMoreElements(&hasMore)) && hasMore) {
+ iter->GetNext(getter_AddRefs(tmp));
+ RefPtr<Blob> domBlob = do_QueryObject(tmp);
+ MOZ_ASSERT(domBlob,
+ "Null file object from FilePicker's file enumerator?");
+ if (!domBlob) {
+ continue;
+ }
+
+ OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
+ element->SetAsFile() = domBlob->ToFile();
+ }
+ } else {
+ MOZ_ASSERT(mode == nsIFilePicker::modeOpen ||
+ mode == nsIFilePicker::modeGetFolder);
+ nsCOMPtr<nsISupports> tmp;
+ nsresult rv = mFilePicker->GetDomFileOrDirectory(getter_AddRefs(tmp));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Show a prompt to get user confirmation before allowing folder access.
+ // This is to prevent sites from tricking the user into uploading files.
+ // See Bug 1338637.
+ if (mode == nsIFilePicker::modeGetFolder) {
+ nsCOMPtr<nsIPromptCollection> prompter =
+ do_GetService("@mozilla.org/embedcomp/prompt-collection;1");
+ if (!prompter) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ bool confirmed = false;
+ BrowsingContext* bc = mInput->OwnerDoc()->GetBrowsingContext();
+
+ // Get directory name
+ RefPtr<Directory> directory = static_cast<Directory*>(tmp.get());
+ nsAutoString directoryName;
+ ErrorResult error;
+ directory->GetName(directoryName, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+
+ rv = prompter->ConfirmFolderUpload(bc, directoryName, &confirmed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!confirmed) {
+ // User aborted upload
+ return NS_OK;
+ }
+ }
+
+ RefPtr<Blob> blob = do_QueryObject(tmp);
+ if (blob) {
+ RefPtr<File> file = blob->ToFile();
+ MOZ_ASSERT(file);
+
+ OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
+ element->SetAsFile() = file;
+ } else if (tmp) {
+ RefPtr<Directory> directory = static_cast<Directory*>(tmp.get());
+ OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement();
+ element->SetAsDirectory() = directory;
+ }
+ }
+
+ if (newFilesOrDirectories.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // Store the last used directory using the content pref service:
+ nsCOMPtr<nsIFile> lastUsedDir = LastUsedDirectory(newFilesOrDirectories[0]);
+
+ if (lastUsedDir) {
+ HTMLInputElement::gUploadLastDir->StoreLastUsedDirectory(mInput->OwnerDoc(),
+ lastUsedDir);
+ }
+
+ // The text control frame (if there is one) isn't going to send a change
+ // event because it will think this is done by a script.
+ // So, we can safely send one by ourself.
+ mInput->SetFilesOrDirectories(newFilesOrDirectories, true);
+
+ // mInput(HTMLInputElement) has no scriptGlobalObject, don't create
+ // DispatchChangeEventCallback
+ if (!mInput->GetOwnerGlobal()) {
+ return NS_OK;
+ }
+ RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
+ new DispatchChangeEventCallback(mInput);
+
+ if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ mInput->HasAttr(nsGkAtoms::webkitdirectory)) {
+ ErrorResult error;
+ GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(true, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+
+ helper->AddCallback(dispatchChangeEventCallback);
+ return NS_OK;
+ }
+
+ return dispatchChangeEventCallback->DispatchEvents();
+}
+
+NS_IMPL_ISUPPORTS(HTMLInputElement::nsFilePickerShownCallback,
+ nsIFilePickerShownCallback)
+
+class nsColorPickerShownCallback final : public nsIColorPickerShownCallback {
+ ~nsColorPickerShownCallback() = default;
+
+ public:
+ nsColorPickerShownCallback(HTMLInputElement* aInput,
+ nsIColorPicker* aColorPicker)
+ : mInput(aInput), mColorPicker(aColorPicker), mValueChanged(false) {}
+
+ NS_DECL_ISUPPORTS
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD Update(const nsAString& aColor) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD Done(const nsAString& aColor) override;
+
+ private:
+ /**
+ * Updates the internals of the object using aColor as the new value.
+ * If aTrustedUpdate is true, it will consider that aColor is a new value.
+ * Otherwise, it will check that aColor is different from the current value.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult UpdateInternal(const nsAString& aColor, bool aTrustedUpdate);
+
+ RefPtr<HTMLInputElement> mInput;
+ nsCOMPtr<nsIColorPicker> mColorPicker;
+ bool mValueChanged;
+};
+
+nsresult nsColorPickerShownCallback::UpdateInternal(const nsAString& aColor,
+ bool aTrustedUpdate) {
+ bool valueChanged = false;
+ nsAutoString oldValue;
+ if (aTrustedUpdate) {
+ mInput->OwnerDoc()->NotifyUserGestureActivation();
+ valueChanged = true;
+ } else {
+ mInput->GetValue(oldValue, CallerType::System);
+ }
+
+ mInput->SetValue(aColor, CallerType::System, IgnoreErrors());
+
+ if (!aTrustedUpdate) {
+ nsAutoString newValue;
+ mInput->GetValue(newValue, CallerType::System);
+ if (!oldValue.Equals(newValue)) {
+ valueChanged = true;
+ }
+ }
+
+ if (!valueChanged) {
+ return NS_OK;
+ }
+
+ mValueChanged = true;
+ RefPtr<HTMLInputElement> input(mInput);
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(input);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsColorPickerShownCallback::Update(const nsAString& aColor) {
+ return UpdateInternal(aColor, true);
+}
+
+NS_IMETHODIMP
+nsColorPickerShownCallback::Done(const nsAString& aColor) {
+ /**
+ * When Done() is called, we might be at the end of a serie of Update() calls
+ * in which case mValueChanged is set to true and a change event will have to
+ * be fired but we might also be in a one shot Done() call situation in which
+ * case we should fire a change event iif the value actually changed.
+ * UpdateInternal(bool) is taking care of that logic for us.
+ */
+ nsresult rv = NS_OK;
+
+ mInput->PickerClosed();
+
+ if (!aColor.IsEmpty()) {
+ UpdateInternal(aColor, false);
+ }
+
+ if (mValueChanged) {
+ mInput->SetUserInteracted(true);
+ rv = nsContentUtils::DispatchTrustedEvent(
+ mInput->OwnerDoc(), static_cast<Element*>(mInput.get()), u"change"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+ }
+
+ return rv;
+}
+
+NS_IMPL_ISUPPORTS(nsColorPickerShownCallback, nsIColorPickerShownCallback)
+
+static bool IsPopupBlocked(Document* aDoc) {
+ if (aDoc->ConsumeTransientUserGestureActivation()) {
+ return false;
+ }
+
+ WindowContext* wc = aDoc->GetWindowContext();
+ if (wc && wc->CanShowPopup()) {
+ return false;
+ }
+
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, aDoc,
+ nsContentUtils::eDOM_PROPERTIES,
+ "InputPickerBlockedNoUserActivation");
+ return true;
+}
+
+nsTArray<nsString> HTMLInputElement::GetColorsFromList() {
+ RefPtr<HTMLDataListElement> dataList = GetList();
+ if (!dataList) {
+ return {};
+ }
+
+ nsTArray<nsString> colors;
+
+ RefPtr<nsContentList> options = dataList->Options();
+ uint32_t length = options->Length(true);
+ for (uint32_t i = 0; i < length; ++i) {
+ auto* option = HTMLOptionElement::FromNodeOrNull(options->Item(i, false));
+ if (!option) {
+ continue;
+ }
+
+ nsString value;
+ option->GetValue(value);
+ if (IsValidSimpleColor(value)) {
+ ToLowerCase(value);
+ colors.AppendElement(value);
+ }
+ }
+
+ return colors;
+}
+
+nsresult HTMLInputElement::InitColorPicker() {
+ MOZ_ASSERT(IsMutable());
+
+ if (mPickerRunning) {
+ NS_WARNING("Just one nsIColorPicker is allowed");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<Document> doc = OwnerDoc();
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow();
+ if (!win) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (IsPopupBlocked(doc)) {
+ return NS_OK;
+ }
+
+ // Get Loc title
+ nsAutoString title;
+ nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "ColorPicker", title);
+
+ nsCOMPtr<nsIColorPicker> colorPicker =
+ do_CreateInstance("@mozilla.org/colorpicker;1");
+ if (!colorPicker) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoString initialValue;
+ GetNonFileValueInternal(initialValue);
+ nsTArray<nsString> colors = GetColorsFromList();
+ nsresult rv = colorPicker->Init(win, title, initialValue, colors);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIColorPickerShownCallback> callback =
+ new nsColorPickerShownCallback(this, colorPicker);
+
+ rv = colorPicker->Open(callback);
+ if (NS_SUCCEEDED(rv)) {
+ mPickerRunning = true;
+ }
+
+ return rv;
+}
+
+nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) {
+ MOZ_ASSERT(IsMutable());
+
+ if (mPickerRunning) {
+ NS_WARNING("Just one nsIFilePicker is allowed");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get parent nsPIDOMWindow object.
+ nsCOMPtr<Document> doc = OwnerDoc();
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow();
+ if (!win) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (IsPopupBlocked(doc)) {
+ return NS_OK;
+ }
+
+ // Get Loc title
+ nsAutoString title;
+ nsAutoString okButtonLabel;
+ if (aType == FILE_PICKER_DIRECTORY) {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "DirectoryUpload", OwnerDoc(),
+ title);
+
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "DirectoryPickerOkButtonLabel",
+ OwnerDoc(), okButtonLabel);
+ } else {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "FileUpload", OwnerDoc(), title);
+ }
+
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1");
+ if (!filePicker) return NS_ERROR_FAILURE;
+
+ nsIFilePicker::Mode mode;
+
+ if (aType == FILE_PICKER_DIRECTORY) {
+ mode = nsIFilePicker::modeGetFolder;
+ } else if (HasAttr(nsGkAtoms::multiple)) {
+ mode = nsIFilePicker::modeOpenMultiple;
+ } else {
+ mode = nsIFilePicker::modeOpen;
+ }
+
+ nsresult rv =
+ filePicker->Init(win, title, mode, OwnerDoc()->GetBrowsingContext());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!okButtonLabel.IsEmpty()) {
+ filePicker->SetOkButtonLabel(okButtonLabel);
+ }
+
+ // Native directory pickers ignore file type filters, so we don't spend
+ // cycles adding them for FILE_PICKER_DIRECTORY.
+ if (HasAttr(nsGkAtoms::accept) && aType != FILE_PICKER_DIRECTORY) {
+ SetFilePickerFiltersFromAccept(filePicker);
+
+ if (StaticPrefs::dom_capture_enabled()) {
+ if (const nsAttrValue* captureVal = GetParsedAttr(nsGkAtoms::capture)) {
+ filePicker->SetCapture(static_cast<nsIFilePicker::CaptureTarget>(
+ captureVal->GetEnumValue()));
+ }
+ }
+ } else {
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+ }
+
+ // Set default directory and filename
+ nsAutoString defaultName;
+
+ const nsTArray<OwningFileOrDirectory>& oldFiles =
+ GetFilesOrDirectoriesInternal();
+
+ nsCOMPtr<nsIFilePickerShownCallback> callback =
+ new HTMLInputElement::nsFilePickerShownCallback(this, filePicker);
+
+ if (!oldFiles.IsEmpty() && aType != FILE_PICKER_DIRECTORY) {
+ nsAutoString path;
+
+ nsCOMPtr<nsIFile> parentFile = LastUsedDirectory(oldFiles[0]);
+ if (parentFile) {
+ filePicker->SetDisplayDirectory(parentFile);
+ }
+
+ // Unfortunately nsIFilePicker doesn't allow multiple files to be
+ // default-selected, so only select something by default if exactly
+ // one file was selected before.
+ if (oldFiles.Length() == 1) {
+ nsAutoString leafName;
+ GetDOMFileOrDirectoryName(oldFiles[0], leafName);
+
+ if (!leafName.IsEmpty()) {
+ filePicker->SetDefaultString(leafName);
+ }
+ }
+
+ rv = filePicker->Open(callback);
+ if (NS_SUCCEEDED(rv)) {
+ mPickerRunning = true;
+ }
+
+ return rv;
+ }
+
+ HTMLInputElement::gUploadLastDir->FetchDirectoryAndDisplayPicker(
+ doc, filePicker, callback);
+ mPickerRunning = true;
+ return NS_OK;
+}
+
+#define CPS_PREF_NAME u"browser.upload.lastDir"_ns
+
+NS_IMPL_ISUPPORTS(UploadLastDir, nsIObserver, nsISupportsWeakReference)
+
+void HTMLInputElement::InitUploadLastDir() {
+ gUploadLastDir = new UploadLastDir();
+ NS_ADDREF(gUploadLastDir);
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService && gUploadLastDir) {
+ observerService->AddObserver(gUploadLastDir,
+ "browser:purge-session-history", true);
+ }
+}
+
+void HTMLInputElement::DestroyUploadLastDir() { NS_IF_RELEASE(gUploadLastDir); }
+
+nsresult UploadLastDir::FetchDirectoryAndDisplayPicker(
+ Document* aDoc, nsIFilePicker* aFilePicker,
+ nsIFilePickerShownCallback* aFpCallback) {
+ MOZ_ASSERT(aDoc, "aDoc is null");
+ MOZ_ASSERT(aFilePicker, "aFilePicker is null");
+ MOZ_ASSERT(aFpCallback, "aFpCallback is null");
+
+ nsIURI* docURI = aDoc->GetDocumentURI();
+ MOZ_ASSERT(docURI, "docURI is null");
+
+ nsCOMPtr<nsILoadContext> loadContext = aDoc->GetLoadContext();
+ nsCOMPtr<nsIContentPrefCallback2> prefCallback =
+ new UploadLastDir::ContentPrefCallback(aFilePicker, aFpCallback);
+
+ // Attempt to get the CPS, if it's not present we'll fallback to use the
+ // Desktop folder
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ if (!contentPrefService) {
+ prefCallback->HandleCompletion(nsIContentPrefCallback2::COMPLETE_ERROR);
+ return NS_OK;
+ }
+
+ nsAutoCString cstrSpec;
+ docURI->GetSpec(cstrSpec);
+ NS_ConvertUTF8toUTF16 spec(cstrSpec);
+
+ contentPrefService->GetByDomainAndName(spec, CPS_PREF_NAME, loadContext,
+ prefCallback);
+ return NS_OK;
+}
+
+nsresult UploadLastDir::StoreLastUsedDirectory(Document* aDoc, nsIFile* aDir) {
+ MOZ_ASSERT(aDoc, "aDoc is null");
+ if (!aDir) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> docURI = aDoc->GetDocumentURI();
+ MOZ_ASSERT(docURI, "docURI is null");
+
+ // Attempt to get the CPS, if it's not present we'll just return
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ if (!contentPrefService) return NS_ERROR_NOT_AVAILABLE;
+
+ nsAutoCString cstrSpec;
+ docURI->GetSpec(cstrSpec);
+ NS_ConvertUTF8toUTF16 spec(cstrSpec);
+
+ // Find the parent of aFile, and store it
+ nsString unicodePath;
+ aDir->GetPath(unicodePath);
+ if (unicodePath.IsEmpty()) // nothing to do
+ return NS_OK;
+ RefPtr<nsVariantCC> prefValue = new nsVariantCC();
+ prefValue->SetAsAString(unicodePath);
+
+ // Use the document's current load context to ensure that the content pref
+ // service doesn't persistently store this directory for this domain if the
+ // user is using private browsing:
+ nsCOMPtr<nsILoadContext> loadContext = aDoc->GetLoadContext();
+ return contentPrefService->Set(spec, CPS_PREF_NAME, prefValue, loadContext,
+ nullptr);
+}
+
+NS_IMETHODIMP
+UploadLastDir::Observe(nsISupports* aSubject, char const* aTopic,
+ char16_t const* aData) {
+ if (strcmp(aTopic, "browser:purge-session-history") == 0) {
+ nsCOMPtr<nsIContentPrefService2> contentPrefService =
+ do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
+ if (contentPrefService)
+ contentPrefService->RemoveByName(CPS_PREF_NAME, nullptr, nullptr);
+ }
+ return NS_OK;
+}
+
+#ifdef ACCESSIBILITY
+// Helper method
+static nsresult FireEventForAccessibility(HTMLInputElement* aTarget,
+ EventMessage aEventMessage);
+#endif
+
+//
+// construction, destruction
+//
+
+HTMLInputElement::HTMLInputElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser, FromClone aFromClone)
+ : TextControlElement(std::move(aNodeInfo), aFromParser,
+ FormControlType(kInputDefaultType->value)),
+ mAutocompleteAttrState(nsContentUtils::eAutocompleteAttrState_Unknown),
+ mAutocompleteInfoState(nsContentUtils::eAutocompleteAttrState_Unknown),
+ mDisabledChanged(false),
+ mValueChanged(false),
+ mUserInteracted(false),
+ mLastValueChangeWasInteractive(false),
+ mCheckedChanged(false),
+ mChecked(false),
+ mHandlingSelectEvent(false),
+ mShouldInitChecked(false),
+ mDoneCreating(aFromParser == NOT_FROM_PARSER &&
+ aFromClone == FromClone::No),
+ mInInternalActivate(false),
+ mCheckedIsToggled(false),
+ mIndeterminate(false),
+ mInhibitRestoration(aFromParser & FROM_PARSER_FRAGMENT),
+ mHasRange(false),
+ mIsDraggingRange(false),
+ mNumberControlSpinnerIsSpinning(false),
+ mNumberControlSpinnerSpinsUp(false),
+ mPickerRunning(false),
+ mIsPreviewEnabled(false),
+ mHasBeenTypePassword(false),
+ mHasPatternAttribute(false),
+ mRadioGroupContainer(nullptr) {
+ // If size is above 512, mozjemalloc allocates 1kB, see
+ // memory/build/mozjemalloc.cpp
+ static_assert(sizeof(HTMLInputElement) <= 512,
+ "Keep the size of HTMLInputElement under 512 to avoid "
+ "performance regression!");
+
+ // We are in a type=text but we create TextControlState lazily.
+ mInputData.mState = nullptr;
+
+ void* memory = mInputTypeMem;
+ mInputType = InputType::Create(this, mType, memory);
+
+ if (!gUploadLastDir) HTMLInputElement::InitUploadLastDir();
+
+ // Set up our default state. By default we're enabled (since we're a control
+ // type that can be disabled but not actually disabled right now), optional,
+ // read-write, and valid. Also by default we don't have to show validity UI
+ // and so forth.
+ AddStatesSilently(ElementState::ENABLED | ElementState::OPTIONAL_ |
+ ElementState::VALID | ElementState::VALUE_EMPTY |
+ ElementState::READWRITE);
+ RemoveStatesSilently(ElementState::READONLY);
+ UpdateApzAwareFlag();
+}
+
+HTMLInputElement::~HTMLInputElement() {
+ if (mNumberControlSpinnerIsSpinning) {
+ StopNumberControlSpinnerSpin(eDisallowDispatchingEvents);
+ }
+ nsImageLoadingContent::Destroy();
+ FreeData();
+}
+
+void HTMLInputElement::FreeData() {
+ if (!IsSingleLineTextControl(false)) {
+ free(mInputData.mValue);
+ mInputData.mValue = nullptr;
+ } else if (mInputData.mState) {
+ // XXX Passing nullptr to UnbindFromFrame doesn't do anything!
+ UnbindFromFrame(nullptr);
+ mInputData.mState->Destroy();
+ mInputData.mState = nullptr;
+ }
+
+ if (mInputType) {
+ mInputType->DropReference();
+ mInputType = nullptr;
+ }
+}
+
+void HTMLInputElement::EnsureEditorState() {
+ MOZ_ASSERT(IsSingleLineTextControl(false));
+ if (!mInputData.mState) {
+ mInputData.mState = TextControlState::Construct(this);
+ }
+}
+
+TextControlState* HTMLInputElement::GetEditorState() const {
+ if (!IsSingleLineTextControl(false)) {
+ return nullptr;
+ }
+
+ // We've postponed allocating TextControlState, doing that in a const
+ // method is fine.
+ const_cast<HTMLInputElement*>(this)->EnsureEditorState();
+
+ MOZ_ASSERT(mInputData.mState,
+ "Single line text controls need to have a state"
+ " associated with them");
+
+ return mInputData.mState;
+}
+
+// nsISupports
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLInputElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLInputElement,
+ TextControlElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControllers)
+ if (tmp->IsSingleLineTextControl(false) && tmp->mInputData.mState) {
+ tmp->mInputData.mState->Traverse(cb);
+ }
+
+ if (tmp->mFileData) {
+ tmp->mFileData->Traverse(cb);
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLInputElement,
+ TextControlElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mControllers)
+ if (tmp->IsSingleLineTextControl(false) && tmp->mInputData.mState) {
+ tmp->mInputData.mState->Unlink();
+ }
+
+ if (tmp->mFileData) {
+ tmp->mFileData->Unlink();
+ }
+ // XXX should unlink more?
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLInputElement,
+ TextControlElement,
+ imgINotificationObserver,
+ nsIImageLoadingContent,
+ nsIConstraintValidation)
+
+// nsINode
+
+nsresult HTMLInputElement::Clone(dom::NodeInfo* aNodeInfo,
+ nsINode** aResult) const {
+ *aResult = nullptr;
+
+ RefPtr<HTMLInputElement> it = new (aNodeInfo->NodeInfoManager())
+ HTMLInputElement(do_AddRef(aNodeInfo), NOT_FROM_PARSER, FromClone::Yes);
+
+ nsresult rv = const_cast<HTMLInputElement*>(this)->CopyInnerTo(it);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ switch (GetValueMode()) {
+ case VALUE_MODE_VALUE:
+ if (mValueChanged) {
+ // We don't have our default value anymore. Set our value on
+ // the clone.
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ // SetValueInternal handles setting the VALUE_CHANGED bit for us
+ if (NS_WARN_IF(
+ NS_FAILED(rv = it->SetValueInternal(
+ value, {ValueSetterOption::SetValueChanged})))) {
+ return rv;
+ }
+ }
+ break;
+ case VALUE_MODE_FILENAME:
+ if (it->OwnerDoc()->IsStaticDocument()) {
+ // We're going to be used in print preview. Since the doc is static
+ // we can just grab the pretty string and use it as wallpaper
+ GetDisplayFileName(it->mFileData->mStaticDocFileList);
+ } else {
+ it->mFileData->ClearGetFilesHelpers();
+ it->mFileData->mFilesOrDirectories.Clear();
+ it->mFileData->mFilesOrDirectories.AppendElements(
+ mFileData->mFilesOrDirectories);
+ }
+ break;
+ case VALUE_MODE_DEFAULT_ON:
+ case VALUE_MODE_DEFAULT:
+ break;
+ }
+
+ if (mCheckedChanged) {
+ // We no longer have our original checked state. Set our
+ // checked state on the clone.
+ it->DoSetChecked(mChecked, false, true);
+ // Then tell DoneCreatingElement() not to overwrite:
+ it->mShouldInitChecked = false;
+ }
+
+ it->mIndeterminate = mIndeterminate;
+
+ it->DoneCreatingElement();
+
+ it->SetLastValueChangeWasInteractive(mLastValueChangeWasInteractive);
+ it.forget(aResult);
+ return NS_OK;
+}
+
+void HTMLInputElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aNotify && aName == nsGkAtoms::disabled) {
+ mDisabledChanged = true;
+ }
+
+ // When name or type changes, radio should be removed from radio group.
+ // If we are not done creating the radio, we also should not do it.
+ if (mType == FormControlType::InputRadio) {
+ if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) &&
+ (mForm || mDoneCreating)) {
+ RemoveFromRadioGroup();
+ } else if (aName == nsGkAtoms::required) {
+ auto* container = GetCurrentRadioGroupContainer();
+
+ if (container && ((aValue && !HasAttr(aNameSpaceID, aName)) ||
+ (!aValue && HasAttr(aNameSpaceID, aName)))) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ container->RadioRequiredWillChange(name, !!aValue);
+ }
+ }
+ }
+
+ if (aName == nsGkAtoms::webkitdirectory) {
+ Telemetry::Accumulate(Telemetry::WEBKIT_DIRECTORY_USED, true);
+ }
+ }
+
+ return nsGenericHTMLFormControlElementWithState::BeforeSetAttr(
+ aNameSpaceID, aName, aValue, aNotify);
+}
+
+void HTMLInputElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ bool needValidityUpdate = false;
+ if (aName == nsGkAtoms::src) {
+ mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue ? aValue->GetStringValue() : EmptyString(),
+ aSubjectPrincipal);
+ if (aNotify && mType == FormControlType::InputImage) {
+ if (aValue) {
+ // Mark channel as urgent-start before load image if the image load is
+ // initiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ LoadImage(aValue->GetStringValue(), true, aNotify,
+ eImageLoadType_Normal, mSrcTriggeringPrincipal);
+ } else {
+ // Null value means the attr got unset; drop the image
+ CancelImageRequests(aNotify);
+ }
+ }
+ }
+
+ if (aName == nsGkAtoms::value) {
+ // If the element has a value in value mode, the value content attribute
+ // is the default value. So if the elements value didn't change from the
+ // default, we have to re-set it.
+ if (!mValueChanged && GetValueMode() == VALUE_MODE_VALUE) {
+ SetDefaultValueAsValue();
+ } else if (GetValueMode() == VALUE_MODE_DEFAULT && HasDirAuto()) {
+ SetAutoDirectionality(aNotify);
+ }
+ // GetStepBase() depends on the `value` attribute if `min` is not present,
+ // even if the value doesn't change.
+ UpdateStepMismatchValidityState();
+ needValidityUpdate = true;
+ }
+
+ // Checked must be set no matter what type of control it is, since
+ // mChecked must reflect the new value
+ if (aName == nsGkAtoms::checked) {
+ if (IsRadioOrCheckbox()) {
+ SetStates(ElementState::DEFAULT, !!aValue, aNotify);
+ }
+ if (!mCheckedChanged) {
+ // Delay setting checked if we are creating this element (wait
+ // until everything is set)
+ if (!mDoneCreating) {
+ mShouldInitChecked = true;
+ } else {
+ DoSetChecked(!!aValue, aNotify, false);
+ }
+ }
+ needValidityUpdate = true;
+ }
+
+ if (aName == nsGkAtoms::type) {
+ FormControlType newType;
+ if (!aValue) {
+ // We're now a text input.
+ newType = FormControlType(kInputDefaultType->value);
+ } else {
+ newType = FormControlType(aValue->GetEnumValue());
+ }
+ if (newType != mType) {
+ HandleTypeChange(newType, aNotify);
+ needValidityUpdate = true;
+ }
+ }
+
+ // When name or type changes, radio should be added to radio group.
+ // If we are not done creating the radio, we also should not do it.
+ if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) &&
+ mType == FormControlType::InputRadio && (mForm || mDoneCreating)) {
+ AddToRadioGroup();
+ UpdateValueMissingValidityStateForRadio(false);
+ needValidityUpdate = true;
+ }
+
+ if (aName == nsGkAtoms::required || aName == nsGkAtoms::disabled ||
+ aName == nsGkAtoms::readonly) {
+ if (aName == nsGkAtoms::disabled) {
+ // This *has* to be called *before* validity state check because
+ // UpdateBarredFromConstraintValidation and
+ // UpdateValueMissingValidityState depend on our disabled state.
+ UpdateDisabledState(aNotify);
+ }
+
+ if (aName == nsGkAtoms::required && DoesRequiredApply()) {
+ // This *has* to be called *before* UpdateValueMissingValidityState
+ // because UpdateValueMissingValidityState depends on our required
+ // state.
+ UpdateRequiredState(!!aValue, aNotify);
+ }
+
+ if (aName == nsGkAtoms::readonly && !!aValue != !!aOldValue) {
+ UpdateReadOnlyState(aNotify);
+ }
+
+ UpdateValueMissingValidityState();
+
+ // This *has* to be called *after* validity has changed.
+ if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) {
+ UpdateBarredFromConstraintValidation();
+ }
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::maxlength) {
+ UpdateTooLongValidityState();
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::minlength) {
+ UpdateTooShortValidityState();
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::pattern) {
+ // Although pattern attribute only applies to single line text controls,
+ // we set this flag for all input types to save having to check the type
+ // here.
+ mHasPatternAttribute = !!aValue;
+
+ if (mDoneCreating) {
+ UpdatePatternMismatchValidityState();
+ }
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::multiple) {
+ UpdateTypeMismatchValidityState();
+ needValidityUpdate = true;
+ } else if (aName == nsGkAtoms::max) {
+ UpdateHasRange(aNotify);
+ mInputType->MinMaxStepAttrChanged();
+ // Validity state must be updated *after* the UpdateValueDueToAttrChange
+ // call above or else the following assert will not be valid.
+ // We don't assert the state of underflow during creation since
+ // DoneCreatingElement sanitizes.
+ UpdateRangeOverflowValidityState();
+ needValidityUpdate = true;
+ MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange ||
+ !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
+ "HTML5 spec does not allow underflow for type=range");
+ } else if (aName == nsGkAtoms::min) {
+ UpdateHasRange(aNotify);
+ mInputType->MinMaxStepAttrChanged();
+ // See corresponding @max comment
+ UpdateRangeUnderflowValidityState();
+ UpdateStepMismatchValidityState();
+ needValidityUpdate = true;
+ MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange ||
+ !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
+ "HTML5 spec does not allow underflow for type=range");
+ } else if (aName == nsGkAtoms::step) {
+ mInputType->MinMaxStepAttrChanged();
+ // See corresponding @max comment
+ UpdateStepMismatchValidityState();
+ needValidityUpdate = true;
+ MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange ||
+ !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW),
+ "HTML5 spec does not allow underflow for type=range");
+ } else if (aName == nsGkAtoms::dir && aValue &&
+ aValue->Equals(nsGkAtoms::_auto, eIgnoreCase)) {
+ SetAutoDirectionality(aNotify);
+ } else if (aName == nsGkAtoms::lang) {
+ // FIXME(emilio, bug 1651070): This doesn't account for lang changes on
+ // ancestors.
+ if (mType == FormControlType::InputNumber) {
+ // The validity of our value may have changed based on the locale.
+ UpdateValidityState();
+ needValidityUpdate = true;
+ }
+ } else if (aName == nsGkAtoms::autocomplete) {
+ // Clear the cached @autocomplete attribute and autocompleteInfo state.
+ mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown;
+ mAutocompleteInfoState = nsContentUtils::eAutocompleteAttrState_Unknown;
+ } else if (aName == nsGkAtoms::placeholder) {
+ // Full addition / removals of the attribute reconstruct right now.
+ if (nsTextControlFrame* f = do_QueryFrame(GetPrimaryFrame())) {
+ f->PlaceholderChanged(aOldValue, aValue);
+ }
+ UpdatePlaceholderShownState();
+ needValidityUpdate = true;
+ }
+
+ if (CreatesDateTimeWidget()) {
+ if (aName == nsGkAtoms::value || aName == nsGkAtoms::readonly ||
+ aName == nsGkAtoms::tabindex || aName == nsGkAtoms::required ||
+ aName == nsGkAtoms::disabled) {
+ // If original target is this and not the inner text control, we should
+ // pass the focus to the inner text control.
+ if (Element* dateTimeBoxElement = GetDateTimeBoxElement()) {
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *dateTimeBoxElement,
+ aName == nsGkAtoms::value ? u"MozDateTimeValueChanged"_ns
+ : u"MozDateTimeAttributeChanged"_ns,
+ CanBubble::eNo, ChromeOnlyDispatch::eNo);
+ }
+ }
+ }
+ if (needValidityUpdate) {
+ UpdateValidityElementStates(aNotify);
+ }
+ }
+
+ return nsGenericHTMLFormControlElementWithState::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLInputElement::BeforeSetForm(HTMLFormElement* aForm, bool aBindToTree) {
+ // No need to remove from radio group if we are just binding to tree.
+ if (mType == FormControlType::InputRadio && !aBindToTree) {
+ RemoveFromRadioGroup();
+ }
+
+ // Dispatch event when <input> @form is set
+ if (!aBindToTree) {
+ MaybeDispatchLoginManagerEvents(aForm);
+ }
+}
+
+void HTMLInputElement::AfterClearForm(bool aUnbindOrDelete) {
+ MOZ_ASSERT(!mForm);
+
+ // Do not add back to radio group if we are releasing or unbinding from tree.
+ if (mType == FormControlType::InputRadio && !aUnbindOrDelete &&
+ !GetCurrentRadioGroupContainer()) {
+ AddToRadioGroup();
+ UpdateValueMissingValidityStateForRadio(false);
+ }
+}
+
+void HTMLInputElement::ResultForDialogSubmit(nsAString& aResult) {
+ if (mType == FormControlType::InputImage) {
+ // Get a property set by the frame to find out where it was clicked.
+ const auto* lastClickedPoint =
+ static_cast<CSSIntPoint*>(GetProperty(nsGkAtoms::imageClickedPoint));
+ int32_t x, y;
+ if (lastClickedPoint) {
+ x = lastClickedPoint->x;
+ y = lastClickedPoint->y;
+ } else {
+ x = y = 0;
+ }
+ aResult.AppendInt(x);
+ aResult.AppendLiteral(",");
+ aResult.AppendInt(y);
+ } else {
+ GetAttr(nsGkAtoms::value, aResult);
+ }
+}
+
+void HTMLInputElement::GetAutocomplete(nsAString& aValue) {
+ if (!DoesAutocompleteApply()) {
+ return;
+ }
+
+ aValue.Truncate();
+ const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete);
+
+ mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute(
+ attributeVal, aValue, mAutocompleteAttrState);
+}
+
+void HTMLInputElement::GetAutocompleteInfo(Nullable<AutocompleteInfo>& aInfo) {
+ if (!DoesAutocompleteApply()) {
+ aInfo.SetNull();
+ return;
+ }
+
+ const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete);
+ mAutocompleteInfoState = nsContentUtils::SerializeAutocompleteAttribute(
+ attributeVal, aInfo.SetValue(), mAutocompleteInfoState, true);
+}
+
+void HTMLInputElement::GetCapture(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::capture, kCaptureDefault->tag, aValue);
+}
+
+void HTMLInputElement::GetFormEnctype(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::formenctype, "", kFormDefaultEnctype->tag, aValue);
+}
+
+void HTMLInputElement::GetFormMethod(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::formmethod, "", kFormDefaultMethod->tag, aValue);
+}
+
+void HTMLInputElement::GetType(nsAString& aValue) const {
+ GetEnumAttr(nsGkAtoms::type, kInputDefaultType->tag, aValue);
+}
+
+int32_t HTMLInputElement::TabIndexDefault() { return 0; }
+
+uint32_t HTMLInputElement::Height() {
+ if (mType != FormControlType::InputImage) {
+ return 0;
+ }
+ return GetWidthHeightForImage().height;
+}
+
+void HTMLInputElement::SetIndeterminateInternal(bool aValue,
+ bool aShouldInvalidate) {
+ mIndeterminate = aValue;
+ if (mType != FormControlType::InputCheckbox) {
+ return;
+ }
+
+ SetStates(ElementState::INDETERMINATE, aValue);
+
+ if (aShouldInvalidate) {
+ // Repaint the frame
+ if (nsIFrame* frame = GetPrimaryFrame()) {
+ frame->InvalidateFrameSubtree();
+ }
+ }
+}
+
+void HTMLInputElement::SetIndeterminate(bool aValue) {
+ SetIndeterminateInternal(aValue, true);
+}
+
+uint32_t HTMLInputElement::Width() {
+ if (mType != FormControlType::InputImage) {
+ return 0;
+ }
+ return GetWidthHeightForImage().width;
+}
+
+bool HTMLInputElement::SanitizesOnValueGetter() const {
+ // Don't return non-sanitized value for datetime types, email, or number.
+ return mType == FormControlType::InputEmail ||
+ mType == FormControlType::InputNumber || IsDateTimeInputType(mType);
+}
+
+void HTMLInputElement::GetValue(nsAString& aValue, CallerType aCallerType) {
+ GetValueInternal(aValue, aCallerType);
+
+ // In the case where we need to sanitize an input value without affecting
+ // the displayed user's input, we instead sanitize only on .value accesses.
+ // For the more general case of input elements displaying text that isn't
+ // their current value, see bug 805049.
+ if (SanitizesOnValueGetter()) {
+ SanitizeValue(aValue, SanitizationKind::ForValueGetter);
+ }
+}
+
+void HTMLInputElement::GetValueInternal(nsAString& aValue,
+ CallerType aCallerType) const {
+ if (mType != FormControlType::InputFile) {
+ GetNonFileValueInternal(aValue);
+ return;
+ }
+
+ if (aCallerType == CallerType::System) {
+ aValue.Assign(mFileData->mFirstFilePath);
+ return;
+ }
+
+ if (mFileData->mFilesOrDirectories.IsEmpty()) {
+ aValue.Truncate();
+ return;
+ }
+
+ nsAutoString file;
+ GetDOMFileOrDirectoryName(mFileData->mFilesOrDirectories[0], file);
+ if (file.IsEmpty()) {
+ aValue.Truncate();
+ return;
+ }
+
+ aValue.AssignLiteral("C:\\fakepath\\");
+ aValue.Append(file);
+}
+
+void HTMLInputElement::GetNonFileValueInternal(nsAString& aValue) const {
+ switch (GetValueMode()) {
+ case VALUE_MODE_VALUE:
+ if (IsSingleLineTextControl(false)) {
+ if (mInputData.mState) {
+ mInputData.mState->GetValue(aValue, true, /* aForDisplay = */ false);
+ } else {
+ // Value hasn't been set yet.
+ aValue.Truncate();
+ }
+ } else if (!aValue.Assign(mInputData.mValue, fallible)) {
+ aValue.Truncate();
+ }
+ return;
+
+ case VALUE_MODE_FILENAME:
+ MOZ_ASSERT_UNREACHABLE("Someone screwed up here");
+ // We'll just return empty string if someone does screw up.
+ aValue.Truncate();
+ return;
+
+ case VALUE_MODE_DEFAULT:
+ // Treat defaultValue as value.
+ GetAttr(nsGkAtoms::value, aValue);
+ return;
+
+ case VALUE_MODE_DEFAULT_ON:
+ // Treat default value as value and returns "on" if no value.
+ if (!GetAttr(nsGkAtoms::value, aValue)) {
+ aValue.AssignLiteral("on");
+ }
+ return;
+ }
+}
+
+void HTMLInputElement::ClearFiles(bool aSetValueChanged) {
+ nsTArray<OwningFileOrDirectory> data;
+ SetFilesOrDirectories(data, aSetValueChanged);
+}
+
+int32_t HTMLInputElement::MonthsSinceJan1970(uint32_t aYear,
+ uint32_t aMonth) const {
+ return (aYear - 1970) * 12 + aMonth - 1;
+}
+
+/* static */
+Decimal HTMLInputElement::StringToDecimal(const nsAString& aValue) {
+ if (!IsAscii(aValue)) {
+ return Decimal::nan();
+ }
+ NS_LossyConvertUTF16toASCII asciiString(aValue);
+ std::string stdString(asciiString.get(), asciiString.Length());
+ auto decimal = Decimal::fromString(stdString);
+ if (!decimal.isFinite()) {
+ return Decimal::nan();
+ }
+ // Numbers are considered finite IEEE 754 Double-precision floating point
+ // values, but decimal supports a bigger range.
+ static const Decimal maxDouble =
+ Decimal::fromDouble(std::numeric_limits<double>::max());
+ if (decimal < -maxDouble || decimal > maxDouble) {
+ return Decimal::nan();
+ }
+ return decimal;
+}
+
+Decimal HTMLInputElement::GetValueAsDecimal() const {
+ nsAutoString stringValue;
+ GetNonFileValueInternal(stringValue);
+ Decimal result = mInputType->ConvertStringToNumber(stringValue).mResult;
+ return result.isFinite() ? result : Decimal::nan();
+}
+
+void HTMLInputElement::SetValue(const nsAString& aValue, CallerType aCallerType,
+ ErrorResult& aRv) {
+ // check security. Note that setting the value to the empty string is always
+ // OK and gives pages a way to clear a file input if necessary.
+ if (mType == FormControlType::InputFile) {
+ if (!aValue.IsEmpty()) {
+ if (aCallerType != CallerType::System) {
+ // setting the value of a "FILE" input widget requires
+ // chrome privilege
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ Sequence<nsString> list;
+ if (!list.AppendElement(aValue, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ MozSetFileNameArray(list, aRv);
+ return;
+ }
+ ClearFiles(true);
+ } else {
+ if (MayFireChangeOnBlur()) {
+ // If the value has been set by a script, we basically want to keep the
+ // current change event state. If the element is ready to fire a change
+ // event, we should keep it that way. Otherwise, we should make sure the
+ // element will not fire any event because of the script interaction.
+ //
+ // NOTE: this is currently quite expensive work (too much string
+ // manipulation). We should probably optimize that.
+ nsAutoString currentValue;
+ GetNonFileValueInternal(currentValue);
+
+ nsresult rv = SetValueInternal(
+ aValue, &currentValue,
+ {ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged,
+ ValueSetterOption::MoveCursorToEndIfValueChanged});
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return;
+ }
+
+ if (mFocusedValue.Equals(currentValue)) {
+ GetValue(mFocusedValue, aCallerType);
+ }
+ } else {
+ nsresult rv = SetValueInternal(
+ aValue,
+ {ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged,
+ ValueSetterOption::MoveCursorToEndIfValueChanged});
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return;
+ }
+ }
+ }
+}
+
+HTMLDataListElement* HTMLInputElement::GetList() const {
+ nsAutoString dataListId;
+ GetAttr(nsGkAtoms::list_, dataListId);
+ if (dataListId.IsEmpty()) {
+ return nullptr;
+ }
+
+ DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot();
+ if (!docOrShadow) {
+ return nullptr;
+ }
+
+ return HTMLDataListElement::FromNodeOrNull(
+ docOrShadow->GetElementById(dataListId));
+}
+
+void HTMLInputElement::SetValue(Decimal aValue, CallerType aCallerType) {
+ MOZ_ASSERT(!aValue.isInfinity(), "aValue must not be Infinity!");
+
+ if (aValue.isNaN()) {
+ SetValue(u""_ns, aCallerType, IgnoreErrors());
+ return;
+ }
+
+ nsAutoString value;
+ mInputType->ConvertNumberToString(aValue, value);
+ SetValue(value, aCallerType, IgnoreErrors());
+}
+
+void HTMLInputElement::GetValueAsDate(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aObject,
+ ErrorResult& aRv) {
+ aObject.set(nullptr);
+ if (!IsDateTimeInputType(mType)) {
+ return;
+ }
+
+ Maybe<JS::ClippedTime> time;
+
+ switch (mType) {
+ case FormControlType::InputDate: {
+ uint32_t year, month, day;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseDate(value, &year, &month, &day)) {
+ return;
+ }
+
+ time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, day)));
+ break;
+ }
+ case FormControlType::InputTime: {
+ uint32_t millisecond;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseTime(value, &millisecond)) {
+ return;
+ }
+
+ time.emplace(JS::TimeClip(millisecond));
+ MOZ_ASSERT(time->toDouble() == millisecond,
+ "HTML times are restricted to the day after the epoch and "
+ "never clip");
+ break;
+ }
+ case FormControlType::InputMonth: {
+ uint32_t year, month;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseMonth(value, &year, &month)) {
+ return;
+ }
+
+ time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, 1)));
+ break;
+ }
+ case FormControlType::InputWeek: {
+ uint32_t year, week;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseWeek(value, &year, &week)) {
+ return;
+ }
+
+ double days = DaysSinceEpochFromWeek(year, week);
+ time.emplace(JS::TimeClip(days * kMsPerDay));
+
+ break;
+ }
+ case FormControlType::InputDatetimeLocal: {
+ uint32_t year, month, day, timeInMs;
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ if (!ParseDateTimeLocal(value, &year, &month, &day, &timeInMs)) {
+ return;
+ }
+
+ time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs)));
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (time) {
+ aObject.set(JS::NewDateObject(aCx, *time));
+ if (!aObject) {
+ aRv.NoteJSContextException(aCx);
+ }
+ return;
+ }
+
+ MOZ_ASSERT(false, "Unrecognized input type");
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+}
+
+void HTMLInputElement::SetValueAsDate(JSContext* aCx,
+ JS::Handle<JSObject*> aObj,
+ ErrorResult& aRv) {
+ if (!IsDateTimeInputType(mType)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (aObj) {
+ bool isDate;
+ if (!JS::ObjectIsDate(aCx, aObj, &isDate)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+ if (!isDate) {
+ aRv.ThrowTypeError("Value being assigned is not a date.");
+ return;
+ }
+ }
+
+ double milliseconds;
+ if (aObj) {
+ if (!js::DateGetMsecSinceEpoch(aCx, aObj, &milliseconds)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+ } else {
+ milliseconds = UnspecifiedNaN<double>();
+ }
+
+ // At this point we know we're not a file input, so we can just pass "not
+ // system" as the caller type, since the caller type only matters in the file
+ // input case.
+ if (std::isnan(milliseconds)) {
+ SetValue(u""_ns, CallerType::NonSystem, aRv);
+ return;
+ }
+
+ if (mType != FormControlType::InputMonth) {
+ SetValue(Decimal::fromDouble(milliseconds), CallerType::NonSystem);
+ return;
+ }
+
+ // type=month expects the value to be number of months.
+ double year = JS::YearFromTime(milliseconds);
+ double month = JS::MonthFromTime(milliseconds);
+
+ if (std::isnan(year) || std::isnan(month)) {
+ SetValue(u""_ns, CallerType::NonSystem, aRv);
+ return;
+ }
+
+ int32_t months = MonthsSinceJan1970(year, month + 1);
+ SetValue(Decimal(int32_t(months)), CallerType::NonSystem);
+}
+
+void HTMLInputElement::SetValueAsNumber(double aValueAsNumber,
+ ErrorResult& aRv) {
+ // TODO: return TypeError when HTMLInputElement is converted to WebIDL, see
+ // bug 825197.
+ if (std::isinf(aValueAsNumber)) {
+ aRv.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ if (!DoesValueAsNumberApply()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // At this point we know we're not a file input, so we can just pass "not
+ // system" as the caller type, since the caller type only matters in the file
+ // input case.
+ SetValue(Decimal::fromDouble(aValueAsNumber), CallerType::NonSystem);
+}
+
+Decimal HTMLInputElement::GetMinimum() const {
+ MOZ_ASSERT(
+ DoesValueAsNumberApply(),
+ "GetMinimum() should only be used for types that allow .valueAsNumber");
+
+ // Only type=range has a default minimum
+ Decimal defaultMinimum =
+ mType == FormControlType::InputRange ? Decimal(0) : Decimal::nan();
+
+ if (!HasAttr(nsGkAtoms::min)) {
+ return defaultMinimum;
+ }
+
+ nsAutoString minStr;
+ GetAttr(nsGkAtoms::min, minStr);
+
+ Decimal min = mInputType->ConvertStringToNumber(minStr).mResult;
+ return min.isFinite() ? min : defaultMinimum;
+}
+
+Decimal HTMLInputElement::GetMaximum() const {
+ MOZ_ASSERT(
+ DoesValueAsNumberApply(),
+ "GetMaximum() should only be used for types that allow .valueAsNumber");
+
+ // Only type=range has a default maximum
+ Decimal defaultMaximum =
+ mType == FormControlType::InputRange ? Decimal(100) : Decimal::nan();
+
+ if (!HasAttr(nsGkAtoms::max)) {
+ return defaultMaximum;
+ }
+
+ nsAutoString maxStr;
+ GetAttr(nsGkAtoms::max, maxStr);
+
+ Decimal max = mInputType->ConvertStringToNumber(maxStr).mResult;
+ return max.isFinite() ? max : defaultMaximum;
+}
+
+Decimal HTMLInputElement::GetStepBase() const {
+ MOZ_ASSERT(IsDateTimeInputType(mType) ||
+ mType == FormControlType::InputNumber ||
+ mType == FormControlType::InputRange,
+ "Check that kDefaultStepBase is correct for this new type");
+ // Do NOT use GetMinimum here - the spec says to use "the min content
+ // attribute", not "the minimum".
+ nsAutoString minStr;
+ if (GetAttr(nsGkAtoms::min, minStr)) {
+ Decimal min = mInputType->ConvertStringToNumber(minStr).mResult;
+ if (min.isFinite()) {
+ return min;
+ }
+ }
+
+ // If @min is not a double, we should use @value.
+ nsAutoString valueStr;
+ if (GetAttr(nsGkAtoms::value, valueStr)) {
+ Decimal value = mInputType->ConvertStringToNumber(valueStr).mResult;
+ if (value.isFinite()) {
+ return value;
+ }
+ }
+
+ if (mType == FormControlType::InputWeek) {
+ return kDefaultStepBaseWeek;
+ }
+
+ return kDefaultStepBase;
+}
+
+nsresult HTMLInputElement::GetValueIfStepped(int32_t aStep,
+ StepCallerType aCallerType,
+ Decimal* aNextStep) {
+ if (!DoStepDownStepUpApply()) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ Decimal stepBase = GetStepBase();
+ Decimal step = GetStep();
+ if (step == kStepAny) {
+ if (aCallerType != CALLED_FOR_USER_EVENT) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+ // Allow the spin buttons and up/down arrow keys to do something sensible:
+ step = GetDefaultStep();
+ }
+
+ Decimal minimum = GetMinimum();
+ Decimal maximum = GetMaximum();
+
+ if (!maximum.isNaN()) {
+ // "max - (max - stepBase) % step" is the nearest valid value to max.
+ maximum = maximum - NS_floorModulo(maximum - stepBase, step);
+ if (!minimum.isNaN()) {
+ if (minimum > maximum) {
+ // Either the minimum was greater than the maximum prior to our
+ // adjustment to align maximum on a step, or else (if we adjusted
+ // maximum) there is no valid step between minimum and the unadjusted
+ // maximum.
+ return NS_OK;
+ }
+ }
+ }
+
+ Decimal value = GetValueAsDecimal();
+ bool valueWasNaN = false;
+ if (value.isNaN()) {
+ value = Decimal(0);
+ valueWasNaN = true;
+ }
+ Decimal valueBeforeStepping = value;
+
+ Decimal deltaFromStep = NS_floorModulo(value - stepBase, step);
+
+ if (deltaFromStep != Decimal(0)) {
+ if (aStep > 0) {
+ value += step - deltaFromStep; // partial step
+ value += step * Decimal(aStep - 1); // then remaining steps
+ } else if (aStep < 0) {
+ value -= deltaFromStep; // partial step
+ value += step * Decimal(aStep + 1); // then remaining steps
+ }
+ } else {
+ value += step * Decimal(aStep);
+ }
+
+ if (value < minimum) {
+ value = minimum;
+ deltaFromStep = NS_floorModulo(value - stepBase, step);
+ if (deltaFromStep != Decimal(0)) {
+ value += step - deltaFromStep;
+ }
+ }
+ if (value > maximum) {
+ value = maximum;
+ deltaFromStep = NS_floorModulo(value - stepBase, step);
+ if (deltaFromStep != Decimal(0)) {
+ value -= deltaFromStep;
+ }
+ }
+
+ if (!valueWasNaN && // value="", resulting in us using "0"
+ ((aStep > 0 && value < valueBeforeStepping) ||
+ (aStep < 0 && value > valueBeforeStepping))) {
+ // We don't want step-up to effectively step down, or step-down to
+ // effectively step up, so return;
+ return NS_OK;
+ }
+
+ *aNextStep = value;
+ return NS_OK;
+}
+
+nsresult HTMLInputElement::ApplyStep(int32_t aStep) {
+ Decimal nextStep = Decimal::nan(); // unchanged if value will not change
+
+ nsresult rv = GetValueIfStepped(aStep, CALLED_FOR_SCRIPT, &nextStep);
+
+ if (NS_SUCCEEDED(rv) && nextStep.isFinite()) {
+ // We know we're not a file input, so the caller type does not matter; just
+ // pass "not system" to be safe.
+ SetValue(nextStep, CallerType::NonSystem);
+ }
+
+ return rv;
+}
+
+bool HTMLInputElement::IsDateTimeInputType(FormControlType aType) {
+ switch (aType) {
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+ default:
+ return false;
+ }
+}
+
+void HTMLInputElement::MozGetFileNameArray(nsTArray<nsString>& aArray,
+ ErrorResult& aRv) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ const nsTArray<OwningFileOrDirectory>& filesOrDirs =
+ GetFilesOrDirectoriesInternal();
+ for (uint32_t i = 0; i < filesOrDirs.Length(); i++) {
+ nsAutoString str;
+ GetDOMFileOrDirectoryPath(filesOrDirs[i], str, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ aArray.AppendElement(str);
+ }
+}
+
+void HTMLInputElement::MozSetFileArray(
+ const Sequence<OwningNonNull<File>>& aFiles) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+ MOZ_ASSERT(global);
+ if (!global) {
+ return;
+ }
+
+ nsTArray<OwningFileOrDirectory> files;
+ for (uint32_t i = 0; i < aFiles.Length(); ++i) {
+ RefPtr<File> file = File::Create(global, aFiles[i].get()->Impl());
+ if (NS_WARN_IF(!file)) {
+ return;
+ }
+
+ OwningFileOrDirectory* element = files.AppendElement();
+ element->SetAsFile() = file;
+ }
+
+ SetFilesOrDirectories(files, true);
+}
+
+void HTMLInputElement::MozSetFileNameArray(const Sequence<nsString>& aFileNames,
+ ErrorResult& aRv) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ if (XRE_IsContentProcess()) {
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return;
+ }
+
+ nsTArray<OwningFileOrDirectory> files;
+ for (uint32_t i = 0; i < aFileNames.Length(); ++i) {
+ nsCOMPtr<nsIFile> file;
+
+ if (StringBeginsWith(aFileNames[i], u"file:"_ns,
+ nsASCIICaseInsensitiveStringComparator)) {
+ // Converts the URL string into the corresponding nsIFile if possible
+ // A local file will be created if the URL string begins with file://
+ NS_GetFileFromURLSpec(NS_ConvertUTF16toUTF8(aFileNames[i]),
+ getter_AddRefs(file));
+ }
+
+ if (!file) {
+ // this is no "file://", try as local file
+ NS_NewLocalFile(aFileNames[i], false, getter_AddRefs(file));
+ }
+
+ if (!file) {
+ continue; // Not much we can do if the file doesn't exist
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ RefPtr<File> domFile = File::CreateFromFile(global, file);
+ if (NS_WARN_IF(!domFile)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ OwningFileOrDirectory* element = files.AppendElement();
+ element->SetAsFile() = domFile;
+ }
+
+ SetFilesOrDirectories(files, true);
+}
+
+void HTMLInputElement::MozSetDirectory(const nsAString& aDirectoryPath,
+ ErrorResult& aRv) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ nsCOMPtr<nsIFile> file;
+ aRv = NS_NewLocalFile(aDirectoryPath, true, getter_AddRefs(file));
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ if (NS_WARN_IF(!window)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ RefPtr<Directory> directory = Directory::Create(window->AsGlobal(), file);
+ MOZ_ASSERT(directory);
+
+ nsTArray<OwningFileOrDirectory> array;
+ OwningFileOrDirectory* element = array.AppendElement();
+ element->SetAsDirectory() = directory;
+
+ SetFilesOrDirectories(array, true);
+}
+
+void HTMLInputElement::GetDateTimeInputBoxValue(DateTimeValue& aValue) {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType)) || !mDateTimeInputBoxValue) {
+ return;
+ }
+
+ aValue = *mDateTimeInputBoxValue;
+}
+
+Element* HTMLInputElement::GetDateTimeBoxElement() {
+ if (!GetShadowRoot()) {
+ return nullptr;
+ }
+
+ // The datetimebox <div> is the only child of the UA Widget Shadow Root
+ // if it is present.
+ MOZ_ASSERT(GetShadowRoot()->IsUAWidget());
+ MOZ_ASSERT(1 >= GetShadowRoot()->GetChildCount());
+ if (nsIContent* inputAreaContent = GetShadowRoot()->GetFirstChild()) {
+ return inputAreaContent->AsElement();
+ }
+
+ return nullptr;
+}
+
+void HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue) {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+
+ mDateTimeInputBoxValue = MakeUnique<DateTimeValue>(aInitialValue);
+ nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this),
+ u"MozOpenDateTimePicker"_ns,
+ CanBubble::eYes, Cancelable::eYes);
+}
+
+void HTMLInputElement::UpdateDateTimePicker(const DateTimeValue& aValue) {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+
+ mDateTimeInputBoxValue = MakeUnique<DateTimeValue>(aValue);
+ nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this),
+ u"MozUpdateDateTimePicker"_ns,
+ CanBubble::eYes, Cancelable::eYes);
+}
+
+void HTMLInputElement::CloseDateTimePicker() {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+
+ nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this),
+ u"MozCloseDateTimePicker"_ns,
+ CanBubble::eYes, Cancelable::eYes);
+}
+
+void HTMLInputElement::SetFocusState(bool aIsFocused) {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+ SetStates(ElementState::FOCUS | ElementState::FOCUSRING, aIsFocused);
+}
+
+void HTMLInputElement::UpdateValidityState() {
+ if (NS_WARN_IF(!IsDateTimeInputType(mType))) {
+ return;
+ }
+
+ // For now, datetime input box call this function only when the value may
+ // become valid/invalid. For other validity states, they will be updated when
+ // .value is actually changed.
+ UpdateBadInputValidityState();
+ UpdateValidityElementStates(true);
+}
+
+bool HTMLInputElement::MozIsTextField(bool aExcludePassword) {
+ // TODO: temporary until bug 888320 is fixed.
+ //
+ // FIXME: Historically we never returned true for `number`, we should consider
+ // changing that now that it is similar to other inputs.
+ if (IsDateTimeInputType(mType) || mType == FormControlType::InputNumber) {
+ return false;
+ }
+
+ return IsSingleLineTextControl(aExcludePassword);
+}
+
+void HTMLInputElement::SetUserInput(const nsAString& aValue,
+ nsIPrincipal& aSubjectPrincipal) {
+ AutoHandlingUserInputStatePusher inputStatePusher(true);
+
+ if (mType == FormControlType::InputFile &&
+ !aSubjectPrincipal.IsSystemPrincipal()) {
+ return;
+ }
+
+ if (mType == FormControlType::InputFile) {
+ Sequence<nsString> list;
+ if (!list.AppendElement(aValue, fallible)) {
+ return;
+ }
+
+ MozSetFileNameArray(list, IgnoreErrors());
+ return;
+ }
+
+ bool isInputEventDispatchedByTextControlState =
+ GetValueMode() == VALUE_MODE_VALUE && IsSingleLineTextControl(false);
+
+ nsresult rv = SetValueInternal(
+ aValue,
+ {ValueSetterOption::BySetUserInputAPI, ValueSetterOption::SetValueChanged,
+ ValueSetterOption::MoveCursorToEndIfValueChanged});
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ if (!isInputEventDispatchedByTextControlState) {
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ }
+
+ // If this element is not currently focused, it won't receive a change event
+ // for this update through the normal channels. So fire a change event
+ // immediately, instead.
+ if (CreatesDateTimeWidget() || !ShouldBlur(this)) {
+ FireChangeEventIfNeeded();
+ }
+}
+
+nsIEditor* HTMLInputElement::GetEditorForBindings() {
+ if (!GetPrimaryFrame()) {
+ // Ensure we construct frames (and thus an editor) if needed.
+ GetPrimaryFrame(FlushType::Frames);
+ }
+ return GetTextEditorFromState();
+}
+
+bool HTMLInputElement::HasEditor() const {
+ return !!GetTextEditorWithoutCreation();
+}
+
+TextEditor* HTMLInputElement::GetTextEditorFromState() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->GetTextEditor();
+ }
+ return nullptr;
+}
+
+TextEditor* HTMLInputElement::GetTextEditor() {
+ return GetTextEditorFromState();
+}
+
+TextEditor* HTMLInputElement::GetTextEditorWithoutCreation() const {
+ TextControlState* state = GetEditorState();
+ if (!state) {
+ return nullptr;
+ }
+ return state->GetTextEditorWithoutCreation();
+}
+
+nsISelectionController* HTMLInputElement::GetSelectionController() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->GetSelectionController();
+ }
+ return nullptr;
+}
+
+nsFrameSelection* HTMLInputElement::GetConstFrameSelection() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->GetConstFrameSelection();
+ }
+ return nullptr;
+}
+
+nsresult HTMLInputElement::BindToFrame(nsTextControlFrame* aFrame) {
+ MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript());
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->BindToFrame(aFrame);
+ }
+ return NS_ERROR_FAILURE;
+}
+
+void HTMLInputElement::UnbindFromFrame(nsTextControlFrame* aFrame) {
+ TextControlState* state = GetEditorState();
+ if (state && aFrame) {
+ state->UnbindFromFrame(aFrame);
+ }
+}
+
+nsresult HTMLInputElement::CreateEditor() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ return state->PrepareEditor();
+ }
+ return NS_ERROR_FAILURE;
+}
+
+void HTMLInputElement::SetPreviewValue(const nsAString& aValue) {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ state->SetPreviewText(aValue, true);
+ }
+}
+
+void HTMLInputElement::GetPreviewValue(nsAString& aValue) {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ state->GetPreviewText(aValue);
+ }
+}
+
+void HTMLInputElement::EnablePreview() {
+ if (mIsPreviewEnabled) {
+ return;
+ }
+
+ mIsPreviewEnabled = true;
+ // Reconstruct the frame to append an anonymous preview node
+ nsLayoutUtils::PostRestyleEvent(this, RestyleHint{0},
+ nsChangeHint_ReconstructFrame);
+}
+
+bool HTMLInputElement::IsPreviewEnabled() { return mIsPreviewEnabled; }
+
+void HTMLInputElement::GetDisplayFileName(nsAString& aValue) const {
+ MOZ_ASSERT(mFileData);
+
+ if (OwnerDoc()->IsStaticDocument()) {
+ aValue = mFileData->mStaticDocFileList;
+ return;
+ }
+
+ if (mFileData->mFilesOrDirectories.Length() == 1) {
+ GetDOMFileOrDirectoryName(mFileData->mFilesOrDirectories[0], aValue);
+ return;
+ }
+
+ nsAutoString value;
+
+ if (mFileData->mFilesOrDirectories.IsEmpty()) {
+ if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ HasAttr(nsGkAtoms::webkitdirectory)) {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "NoDirSelected", OwnerDoc(),
+ value);
+ } else if (HasAttr(nsGkAtoms::multiple)) {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "NoFilesSelected", OwnerDoc(),
+ value);
+ } else {
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "NoFileSelected", OwnerDoc(),
+ value);
+ }
+ } else {
+ nsString count;
+ count.AppendInt(int(mFileData->mFilesOrDirectories.Length()));
+
+ nsContentUtils::FormatMaybeLocalizedString(
+ value, nsContentUtils::eFORMS_PROPERTIES, "XFilesSelected", OwnerDoc(),
+ count);
+ }
+
+ aValue = value;
+}
+
+const nsTArray<OwningFileOrDirectory>&
+HTMLInputElement::GetFilesOrDirectoriesInternal() const {
+ return mFileData->mFilesOrDirectories;
+}
+
+void HTMLInputElement::SetFilesOrDirectories(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories,
+ bool aSetValueChanged) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ MOZ_ASSERT(mFileData);
+
+ mFileData->ClearGetFilesHelpers();
+
+ if (StaticPrefs::dom_webkitBlink_filesystem_enabled()) {
+ HTMLInputElement_Binding::ClearCachedWebkitEntriesValue(this);
+ mFileData->mEntries.Clear();
+ }
+
+ mFileData->mFilesOrDirectories.Clear();
+ mFileData->mFilesOrDirectories.AppendElements(aFilesOrDirectories);
+
+ AfterSetFilesOrDirectories(aSetValueChanged);
+}
+
+void HTMLInputElement::SetFiles(FileList* aFiles, bool aSetValueChanged) {
+ MOZ_ASSERT(mFileData);
+
+ mFileData->mFilesOrDirectories.Clear();
+ mFileData->ClearGetFilesHelpers();
+
+ if (StaticPrefs::dom_webkitBlink_filesystem_enabled()) {
+ HTMLInputElement_Binding::ClearCachedWebkitEntriesValue(this);
+ mFileData->mEntries.Clear();
+ }
+
+ if (aFiles) {
+ uint32_t listLength = aFiles->Length();
+ for (uint32_t i = 0; i < listLength; i++) {
+ OwningFileOrDirectory* element =
+ mFileData->mFilesOrDirectories.AppendElement();
+ element->SetAsFile() = aFiles->Item(i);
+ }
+ }
+
+ AfterSetFilesOrDirectories(aSetValueChanged);
+}
+
+// This method is used for testing only.
+void HTMLInputElement::MozSetDndFilesAndDirectories(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ SetFilesOrDirectories(aFilesOrDirectories, true);
+
+ if (StaticPrefs::dom_webkitBlink_filesystem_enabled()) {
+ UpdateEntries(aFilesOrDirectories);
+ }
+
+ RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
+ new DispatchChangeEventCallback(this);
+
+ if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ HasAttr(nsGkAtoms::webkitdirectory)) {
+ ErrorResult rv;
+ GetFilesHelper* helper =
+ GetOrCreateGetFilesHelper(true /* recursionFlag */, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return;
+ }
+
+ helper->AddCallback(dispatchChangeEventCallback);
+ } else {
+ dispatchChangeEventCallback->DispatchEvents();
+ }
+}
+
+void HTMLInputElement::AfterSetFilesOrDirectories(bool aSetValueChanged) {
+ // No need to flush here, if there's no frame at this point we
+ // don't need to force creation of one just to tell it about this
+ // new value. We just want the display to update as needed.
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(false);
+ if (formControlFrame) {
+ nsAutoString readableValue;
+ GetDisplayFileName(readableValue);
+ formControlFrame->SetFormProperty(nsGkAtoms::value, readableValue);
+ }
+
+ // Grab the full path here for any chrome callers who access our .value via a
+ // CPOW. This path won't be called from a CPOW meaning the potential sync IPC
+ // call under GetMozFullPath won't be rejected for not being urgent.
+ if (mFileData->mFilesOrDirectories.IsEmpty()) {
+ mFileData->mFirstFilePath.Truncate();
+ } else {
+ ErrorResult rv;
+ GetDOMFileOrDirectoryPath(mFileData->mFilesOrDirectories[0],
+ mFileData->mFirstFilePath, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ }
+ }
+
+ // Null out |mFileData->mFileList| to return a new file list when asked for.
+ // Don't clear it since the file list might come from the user via SetFiles.
+ if (mFileData->mFileList) {
+ mFileData->mFileList = nullptr;
+ }
+
+ if (aSetValueChanged) {
+ SetValueChanged(true);
+ }
+
+ UpdateAllValidityStates(true);
+}
+
+void HTMLInputElement::FireChangeEventIfNeeded() {
+ if (!MayFireChangeOnBlur()) {
+ return;
+ }
+
+ // We're not exposing the GetValue return value anywhere here, so it's safe to
+ // claim to be a system caller.
+ nsAutoString value;
+ GetValue(value, CallerType::System);
+
+ // NOTE(emilio): Per spec we should not set this if we don't fire the change
+ // event, but that seems like a bug. Using mValueChanged seems reasonable to
+ // keep the expected behavior while
+ // https://github.com/whatwg/html/issues/10013 is resolved.
+ if (mValueChanged) {
+ SetUserInteracted(true);
+ }
+ if (mFocusedValue.Equals(value)) {
+ return;
+ }
+ // Dispatch the change event.
+ mFocusedValue = value;
+ nsContentUtils::DispatchTrustedEvent(
+ OwnerDoc(), static_cast<nsIContent*>(this), u"change"_ns, CanBubble::eYes,
+ Cancelable::eNo);
+}
+
+FileList* HTMLInputElement::GetFiles() {
+ if (mType != FormControlType::InputFile) {
+ return nullptr;
+ }
+
+ if (!mFileData->mFileList) {
+ mFileData->mFileList = new FileList(static_cast<nsIContent*>(this));
+ for (const OwningFileOrDirectory& item : GetFilesOrDirectoriesInternal()) {
+ if (item.IsFile()) {
+ mFileData->mFileList->Append(item.GetAsFile());
+ }
+ }
+ }
+
+ return mFileData->mFileList;
+}
+
+void HTMLInputElement::SetFiles(FileList* aFiles) {
+ if (mType != FormControlType::InputFile || !aFiles) {
+ return;
+ }
+
+ // Update |mFileData->mFilesOrDirectories|
+ SetFiles(aFiles, true);
+
+ MOZ_ASSERT(!mFileData->mFileList, "Should've cleared the existing file list");
+
+ // Update |mFileData->mFileList| without copy
+ mFileData->mFileList = aFiles;
+}
+
+/* static */
+void HTMLInputElement::HandleNumberControlSpin(void* aData) {
+ RefPtr<HTMLInputElement> input = static_cast<HTMLInputElement*>(aData);
+
+ NS_ASSERTION(input->mNumberControlSpinnerIsSpinning,
+ "Should have called nsRepeatService::Stop()");
+
+ nsNumberControlFrame* numberControlFrame =
+ do_QueryFrame(input->GetPrimaryFrame());
+ if (input->mType != FormControlType::InputNumber || !numberControlFrame) {
+ // Type has changed (and possibly our frame type hasn't been updated yet)
+ // or else we've lost our frame. Either way, stop the timer and don't do
+ // anything else.
+ input->StopNumberControlSpinnerSpin();
+ } else {
+ input->StepNumberControlForUserEvent(
+ input->mNumberControlSpinnerSpinsUp ? 1 : -1);
+ }
+}
+
+nsresult HTMLInputElement::SetValueInternal(
+ const nsAString& aValue, const nsAString* aOldValue,
+ const ValueSetterOptions& aOptions) {
+ MOZ_ASSERT(GetValueMode() != VALUE_MODE_FILENAME,
+ "Don't call SetValueInternal for file inputs");
+
+ // We want to remember if the SetValueInternal() call is being made for a XUL
+ // element. We do that by looking at the parent node here, and if that node
+ // is a XUL node, we consider our control a XUL control. XUL controls preserve
+ // edit history across value setters.
+ //
+ // TODO(emilio): Rather than doing this maybe add an attribute instead and
+ // read it only on chrome docs or something? That'd allow front-end code to
+ // move away from xul without weird side-effects.
+ const bool forcePreserveUndoHistory = mParent && mParent->IsXULElement();
+
+ switch (GetValueMode()) {
+ case VALUE_MODE_VALUE: {
+ // At the moment, only single line text control have to sanitize their
+ // value Because we have to create a new string for that, we should
+ // prevent doing it if it's useless.
+ nsAutoString value(aValue);
+
+ if (mDoneCreating &&
+ !(mType == FormControlType::InputNumber &&
+ aOptions.contains(ValueSetterOption::BySetUserInputAPI))) {
+ // When the value of a number input is set by a script, we need to make
+ // sure the value is a valid floating-point number.
+ // https://html.spec.whatwg.org/#valid-floating-point-number
+ // When it's set by a user, however, we need to be more permissive, so
+ // we don't sanitize its value here. See bug 1839572.
+ SanitizeValue(value, SanitizationKind::ForValueSetter);
+ }
+ // else DoneCreatingElement calls us again once mDoneCreating is true
+
+ const bool setValueChanged =
+ aOptions.contains(ValueSetterOption::SetValueChanged);
+ if (setValueChanged) {
+ SetValueChanged(true);
+ }
+
+ if (IsSingleLineTextControl(false)) {
+ // Note that if aOptions includes
+ // ValueSetterOption::BySetUserInputAPI, "input" event is automatically
+ // dispatched by TextControlState::SetValue(). If you'd change condition
+ // of calling this method, you need to maintain SetUserInput() too. FYI:
+ // After calling SetValue(), the input type might have been
+ // modified so that mInputData may not store TextControlState.
+ EnsureEditorState();
+ if (!mInputData.mState->SetValue(
+ value, aOldValue,
+ forcePreserveUndoHistory
+ ? aOptions + ValueSetterOption::PreserveUndoHistory
+ : aOptions)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ // If the caller won't dispatch "input" event via
+ // nsContentUtils::DispatchInputEvent(), we need to modify
+ // validationMessage value here.
+ //
+ // FIXME(emilio): ValueSetterOption::ByInternalAPI is not supposed to
+ // change state, but maybe we could run this too?
+ if (aOptions.contains(ValueSetterOption::ByContentAPI)) {
+ MaybeUpdateAllValidityStates(!mDoneCreating);
+ }
+ } else {
+ free(mInputData.mValue);
+ mInputData.mValue = ToNewUnicode(value);
+ if (setValueChanged) {
+ SetValueChanged(true);
+ }
+ if (mType == FormControlType::InputRange) {
+ nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame());
+ if (frame) {
+ frame->UpdateForValueChange();
+ }
+ } else if (CreatesDateTimeWidget() &&
+ !aOptions.contains(ValueSetterOption::BySetUserInputAPI)) {
+ if (Element* dateTimeBoxElement = GetDateTimeBoxElement()) {
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *dateTimeBoxElement, u"MozDateTimeValueChanged"_ns,
+ CanBubble::eNo, ChromeOnlyDispatch::eNo);
+ }
+ }
+ if (mDoneCreating) {
+ OnValueChanged(ValueChangeKind::Internal, value.IsEmpty(), &value);
+ }
+ // else DoneCreatingElement calls us again once mDoneCreating is true
+ }
+
+ if (mType == FormControlType::InputColor) {
+ // Update color frame, to reflect color changes
+ nsColorControlFrame* colorControlFrame =
+ do_QueryFrame(GetPrimaryFrame());
+ if (colorControlFrame) {
+ colorControlFrame->UpdateColor();
+ }
+ }
+ return NS_OK;
+ }
+
+ case VALUE_MODE_DEFAULT:
+ case VALUE_MODE_DEFAULT_ON:
+ // If the value of a hidden input was changed, we mark it changed so that
+ // we will know we need to save / restore the value. Yes, we are
+ // overloading the meaning of ValueChanged just a teensy bit to save a
+ // measly byte of storage space in HTMLInputElement. Yes, you are free to
+ // make a new flag, NEED_TO_SAVE_VALUE, at such time as mBitField becomes
+ // a 16-bit value.
+ if (mType == FormControlType::InputHidden) {
+ SetValueChanged(true);
+ }
+
+ // Make sure to keep track of the last value change not being interactive,
+ // just in case this used to be another kind of editable input before.
+ // Note that a checked change _could_ really be interactive, but we don't
+ // keep track of that elsewhere so seems fine to just do this.
+ SetLastValueChangeWasInteractive(false);
+
+ // Treat value == defaultValue for other input elements.
+ return nsGenericHTMLFormControlElementWithState::SetAttr(
+ kNameSpaceID_None, nsGkAtoms::value, aValue, true);
+
+ case VALUE_MODE_FILENAME:
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // This return statement is required for some compilers.
+ return NS_OK;
+}
+
+void HTMLInputElement::SetValueChanged(bool aValueChanged) {
+ if (mValueChanged == aValueChanged) {
+ return;
+ }
+ mValueChanged = aValueChanged;
+ UpdateTooLongValidityState();
+ UpdateTooShortValidityState();
+ UpdateValidityElementStates(true);
+}
+
+void HTMLInputElement::SetLastValueChangeWasInteractive(bool aWasInteractive) {
+ if (aWasInteractive == mLastValueChangeWasInteractive) {
+ return;
+ }
+ mLastValueChangeWasInteractive = aWasInteractive;
+ const bool wasValid = IsValid();
+ UpdateTooLongValidityState();
+ UpdateTooShortValidityState();
+ if (wasValid != IsValid()) {
+ UpdateValidityElementStates(true);
+ }
+}
+
+void HTMLInputElement::SetCheckedChanged(bool aCheckedChanged) {
+ DoSetCheckedChanged(aCheckedChanged, true);
+}
+
+void HTMLInputElement::DoSetCheckedChanged(bool aCheckedChanged, bool aNotify) {
+ if (mType == FormControlType::InputRadio) {
+ if (mCheckedChanged != aCheckedChanged) {
+ nsCOMPtr<nsIRadioVisitor> visitor =
+ new nsRadioSetCheckedChangedVisitor(aCheckedChanged);
+ VisitGroup(visitor);
+ }
+ } else {
+ SetCheckedChangedInternal(aCheckedChanged);
+ }
+}
+
+void HTMLInputElement::SetCheckedChangedInternal(bool aCheckedChanged) {
+ if (mCheckedChanged == aCheckedChanged) {
+ return;
+ }
+ mCheckedChanged = aCheckedChanged;
+ UpdateValidityElementStates(true);
+}
+
+void HTMLInputElement::SetChecked(bool aChecked) {
+ DoSetChecked(aChecked, true, true);
+}
+
+void HTMLInputElement::DoSetChecked(bool aChecked, bool aNotify,
+ bool aSetValueChanged) {
+ // If the user or JS attempts to set checked, whether it actually changes the
+ // value or not, we say the value was changed so that defaultValue don't
+ // affect it no more.
+ if (aSetValueChanged) {
+ DoSetCheckedChanged(true, aNotify);
+ }
+
+ // Don't do anything if we're not changing whether it's checked (it would
+ // screw up state actually, especially when you are setting radio button to
+ // false)
+ if (mChecked == aChecked) {
+ return;
+ }
+
+ // Set checked
+ if (mType != FormControlType::InputRadio) {
+ SetCheckedInternal(aChecked, aNotify);
+ return;
+ }
+
+ // For radio button, we need to do some extra fun stuff
+ if (aChecked) {
+ RadioSetChecked(aNotify);
+ return;
+ }
+
+ if (auto* container = GetCurrentRadioGroupContainer()) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ container->SetCurrentRadioButton(name, nullptr);
+ }
+ // SetCheckedInternal is going to ask all radios to update their
+ // validity state. We have to be sure the radio group container knows
+ // the currently selected radio.
+ SetCheckedInternal(false, aNotify);
+}
+
+void HTMLInputElement::RadioSetChecked(bool aNotify) {
+ // Find the selected radio button so we can deselect it
+ HTMLInputElement* currentlySelected = GetSelectedRadioButton();
+
+ // Deselect the currently selected radio button
+ if (currentlySelected) {
+ // Pass true for the aNotify parameter since the currently selected
+ // button is already in the document.
+ currentlySelected->SetCheckedInternal(false, true);
+ }
+
+ // Let the group know that we are now the One True Radio Button
+ if (auto* container = GetCurrentRadioGroupContainer()) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ container->SetCurrentRadioButton(name, this);
+ }
+
+ // SetCheckedInternal is going to ask all radios to update their
+ // validity state.
+ SetCheckedInternal(true, aNotify);
+}
+
+RadioGroupContainer* HTMLInputElement::GetCurrentRadioGroupContainer() const {
+ NS_ASSERTION(
+ mType == FormControlType::InputRadio,
+ "GetRadioGroupContainer should only be called when type='radio'");
+ return mRadioGroupContainer;
+}
+
+RadioGroupContainer* HTMLInputElement::FindTreeRadioGroupContainer() const {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ if (name.IsEmpty()) {
+ return nullptr;
+ }
+ if (mForm) {
+ return &mForm->OwnedRadioGroupContainer();
+ }
+ if (IsInNativeAnonymousSubtree()) {
+ return nullptr;
+ }
+ if (Document* doc = GetUncomposedDoc()) {
+ return &doc->OwnedRadioGroupContainer();
+ }
+ return &static_cast<FragmentOrElement*>(SubtreeRoot())
+ ->OwnedRadioGroupContainer();
+}
+
+void HTMLInputElement::DisconnectRadioGroupContainer() {
+ mRadioGroupContainer = nullptr;
+}
+
+HTMLInputElement* HTMLInputElement::GetSelectedRadioButton() const {
+ auto* container = GetCurrentRadioGroupContainer();
+ if (!container) {
+ return nullptr;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ return container->GetCurrentRadioButton(name);
+}
+
+void HTMLInputElement::MaybeSubmitForm(nsPresContext* aPresContext) {
+ if (!mForm) {
+ // Nothing to do here.
+ return;
+ }
+
+ RefPtr<PresShell> presShell = aPresContext->GetPresShell();
+ if (!presShell) {
+ return;
+ }
+
+ // Get the default submit element
+ if (RefPtr<nsGenericHTMLFormElement> submitContent =
+ mForm->GetDefaultSubmitElement()) {
+ WidgetMouseEvent event(true, eMouseClick, nullptr, WidgetMouseEvent::eReal);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ presShell->HandleDOMEventWithTarget(submitContent, &event, &status);
+ } else if (!mForm->ImplicitSubmissionIsDisabled()) {
+ // If there's only one text control, just submit the form
+ // Hold strong ref across the event
+ RefPtr<dom::HTMLFormElement> form(mForm);
+ form->MaybeSubmit(nullptr);
+ }
+}
+
+void HTMLInputElement::UpdateCheckedState(bool aNotify) {
+ SetStates(ElementState::CHECKED, IsRadioOrCheckbox() && mChecked, aNotify);
+}
+
+void HTMLInputElement::UpdateIndeterminateState(bool aNotify) {
+ bool indeterminate = [&] {
+ if (mType == FormControlType::InputCheckbox) {
+ return mIndeterminate;
+ }
+ if (mType == FormControlType::InputRadio) {
+ return !mChecked && !GetSelectedRadioButton();
+ }
+ return false;
+ }();
+ SetStates(ElementState::INDETERMINATE, indeterminate, aNotify);
+}
+
+void HTMLInputElement::SetCheckedInternal(bool aChecked, bool aNotify) {
+ // Set the value
+ mChecked = aChecked;
+
+ if (IsRadioOrCheckbox()) {
+ SetStates(ElementState::CHECKED, aChecked, aNotify);
+ }
+
+ // No need to update element state, since we're about to call
+ // UpdateState anyway.
+ UpdateAllValidityStatesButNotElementState();
+ UpdateIndeterminateState(aNotify);
+ UpdateValidityElementStates(aNotify);
+
+ // Notify all radios in the group that value has changed, this is to let
+ // radios to have the chance to update its states, e.g., :indeterminate.
+ if (mType == FormControlType::InputRadio) {
+ nsCOMPtr<nsIRadioVisitor> visitor = new nsRadioUpdateStateVisitor(this);
+ VisitGroup(visitor);
+ }
+}
+
+#if !defined(ANDROID) && !defined(XP_MACOSX)
+bool HTMLInputElement::IsNodeApzAwareInternal() const {
+ // Tell APZC we may handle mouse wheel event and do preventDefault when input
+ // type is number.
+ return mType == FormControlType::InputNumber ||
+ mType == FormControlType::InputRange ||
+ nsINode::IsNodeApzAwareInternal();
+}
+#endif
+
+bool HTMLInputElement::IsInteractiveHTMLContent() const {
+ return mType != FormControlType::InputHidden ||
+ nsGenericHTMLFormControlElementWithState::IsInteractiveHTMLContent();
+}
+
+void HTMLInputElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) {
+ nsImageLoadingContent::AsyncEventRunning(aEvent);
+}
+
+void HTMLInputElement::Select() {
+ if (!IsSingleLineTextControl(false)) {
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "Single line text controls are expected to have a state");
+
+ if (FocusState() != FocusTristate::eUnfocusable) {
+ RefPtr<nsFrameSelection> fs = state->GetConstFrameSelection();
+ if (fs && fs->MouseDownRecorded()) {
+ // This means that we're being called while the frame selection has a
+ // mouse down event recorded to adjust the caret during the mouse up
+ // event. We are probably called from the focus event handler. We should
+ // override the delayed caret data in this case to ensure that this
+ // select() call takes effect.
+ fs->SetDelayedCaretData(nullptr);
+ }
+
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ fm->SetFocus(this, nsIFocusManager::FLAG_NOSCROLL);
+
+ // A focus event handler may change the type attribute, which will destroy
+ // the previous state object.
+ state = GetEditorState();
+ if (!state) {
+ return;
+ }
+ }
+ }
+
+ // Directly call TextControlState::SetSelectionRange because
+ // HTMLInputElement::SetSelectionRange only applies to fewer types
+ state->SetSelectionRange(0, UINT32_MAX, Optional<nsAString>(), IgnoreErrors(),
+ TextControlState::ScrollAfterSelection::No);
+}
+
+void HTMLInputElement::SelectAll(nsPresContext* aPresContext) {
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(true);
+
+ if (formControlFrame) {
+ formControlFrame->SetFormProperty(nsGkAtoms::select, u""_ns);
+ }
+}
+
+bool HTMLInputElement::NeedToInitializeEditorForEvent(
+ EventChainPreVisitor& aVisitor) const {
+ // We only need to initialize the editor for single line input controls
+ // because they are lazily initialized. We don't need to initialize the
+ // control for certain types of events, because we know that those events are
+ // safe to be handled without the editor being initialized. These events
+ // include: mousein/move/out, overflow/underflow, DOM mutation, and void
+ // events. Void events are dispatched frequently by async keyboard scrolling
+ // to focused elements, so it's important to handle them to prevent excessive
+ // DOM mutations.
+ if (!IsSingleLineTextControl(false) ||
+ aVisitor.mEvent->mClass == eMutationEventClass) {
+ return false;
+ }
+
+ switch (aVisitor.mEvent->mMessage) {
+ case eVoidEvent:
+ case eMouseMove:
+ case eMouseEnterIntoWidget:
+ case eMouseExitFromWidget:
+ case eMouseOver:
+ case eMouseOut:
+ case eScrollPortUnderflow:
+ case eScrollPortOverflow:
+ return false;
+ default:
+ return true;
+ }
+}
+
+bool HTMLInputElement::IsDisabledForEvents(WidgetEvent* aEvent) {
+ return IsElementDisabledForEvents(aEvent, GetPrimaryFrame());
+}
+
+bool HTMLInputElement::CheckActivationBehaviorPreconditions(
+ EventChainVisitor& aVisitor) const {
+ switch (mType) {
+ case FormControlType::InputColor:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputRadio:
+ case FormControlType::InputFile:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ case FormControlType::InputReset:
+ case FormControlType::InputButton: {
+ // Track whether we're in the outermost Dispatch invocation that will
+ // cause activation of the input. That is, if we're a click event, or a
+ // DOMActivate that was dispatched directly, this will be set, but if
+ // we're a DOMActivate dispatched from click handling, it will not be set.
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ bool outerActivateEvent =
+ (mouseEvent && mouseEvent->IsLeftClickEvent()) ||
+ (aVisitor.mEvent->mMessage == eLegacyDOMActivate &&
+ !mInInternalActivate);
+ if (outerActivateEvent) {
+ aVisitor.mItemFlags |= NS_OUTER_ACTIVATE_EVENT;
+ }
+ return outerActivateEvent;
+ }
+ default:
+ return false;
+ }
+}
+
+void HTMLInputElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ // Do not process any DOM events if the element is disabled
+ aVisitor.mCanHandle = false;
+ if (IsDisabledForEvents(aVisitor.mEvent)) {
+ return;
+ }
+
+ // Initialize the editor if needed.
+ if (NeedToInitializeEditorForEvent(aVisitor)) {
+ nsITextControlFrame* textControlFrame = do_QueryFrame(GetPrimaryFrame());
+ if (textControlFrame) textControlFrame->EnsureEditorInitialized();
+ }
+
+ if (CheckActivationBehaviorPreconditions(aVisitor)) {
+ aVisitor.mWantsActivationBehavior = true;
+ }
+
+ // We must cache type because mType may change during JS event (bug 2369)
+ aVisitor.mItemFlags |= uint8_t(mType);
+
+ if (aVisitor.mEvent->mMessage == eFocus && aVisitor.mEvent->IsTrusted() &&
+ MayFireChangeOnBlur() &&
+ // StartRangeThumbDrag already set mFocusedValue on 'mousedown' before
+ // we get the 'focus' event.
+ !mIsDraggingRange) {
+ GetValue(mFocusedValue, CallerType::System);
+ }
+
+ // Fire onchange (if necessary), before we do the blur, bug 357684.
+ if (aVisitor.mEvent->mMessage == eBlur) {
+ // We set NS_PRE_HANDLE_BLUR_EVENT here and handle it in PreHandleEvent to
+ // prevent breaking event target chain creation.
+ aVisitor.mWantsPreHandleEvent = true;
+ aVisitor.mItemFlags |= NS_PRE_HANDLE_BLUR_EVENT;
+ }
+
+ if (mType == FormControlType::InputRange &&
+ (aVisitor.mEvent->mMessage == eFocus ||
+ aVisitor.mEvent->mMessage == eBlur)) {
+ // Just as nsGenericHTMLFormControlElementWithState::GetEventTargetParent
+ // calls nsIFormControlFrame::SetFocus, we handle focus here.
+ nsIFrame* frame = GetPrimaryFrame();
+ if (frame) {
+ frame->InvalidateFrameSubtree();
+ }
+ }
+
+ if (mType == FormControlType::InputNumber && aVisitor.mEvent->IsTrusted()) {
+ if (mNumberControlSpinnerIsSpinning) {
+ // If the timer is running the user has depressed the mouse on one of the
+ // spin buttons. If the mouse exits the button we either want to reverse
+ // the direction of spin if it has moved over the other button, or else
+ // we want to end the spin. We do this here (rather than in
+ // PostHandleEvent) because we don't want to let content preventDefault()
+ // the end of the spin.
+ if (aVisitor.mEvent->mMessage == eMouseMove) {
+ // Be aggressive about stopping the spin:
+ bool stopSpin = true;
+ nsNumberControlFrame* numberControlFrame =
+ do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ bool oldNumberControlSpinTimerSpinsUpValue =
+ mNumberControlSpinnerSpinsUp;
+ switch (numberControlFrame->GetSpinButtonForPointerEvent(
+ aVisitor.mEvent->AsMouseEvent())) {
+ case nsNumberControlFrame::eSpinButtonUp:
+ mNumberControlSpinnerSpinsUp = true;
+ stopSpin = false;
+ break;
+ case nsNumberControlFrame::eSpinButtonDown:
+ mNumberControlSpinnerSpinsUp = false;
+ stopSpin = false;
+ break;
+ }
+ if (mNumberControlSpinnerSpinsUp !=
+ oldNumberControlSpinTimerSpinsUpValue) {
+ nsNumberControlFrame* numberControlFrame =
+ do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ numberControlFrame->SpinnerStateChanged();
+ }
+ }
+ }
+ if (stopSpin) {
+ StopNumberControlSpinnerSpin();
+ }
+ } else if (aVisitor.mEvent->mMessage == eMouseUp) {
+ StopNumberControlSpinnerSpin();
+ }
+ }
+ }
+
+ nsGenericHTMLFormControlElementWithState::GetEventTargetParent(aVisitor);
+
+ // Stop the event if the related target's first non-native ancestor is the
+ // same as the original target's first non-native ancestor (we are moving
+ // inside of the same element).
+ //
+ // FIXME(emilio): Is this still needed now that we use Shadow DOM for this?
+ if (CreatesDateTimeWidget() && aVisitor.mEvent->IsTrusted() &&
+ (aVisitor.mEvent->mMessage == eFocus ||
+ aVisitor.mEvent->mMessage == eFocusIn ||
+ aVisitor.mEvent->mMessage == eFocusOut ||
+ aVisitor.mEvent->mMessage == eBlur)) {
+ nsIContent* originalTarget = nsIContent::FromEventTargetOrNull(
+ aVisitor.mEvent->AsFocusEvent()->mOriginalTarget);
+ nsIContent* relatedTarget = nsIContent::FromEventTargetOrNull(
+ aVisitor.mEvent->AsFocusEvent()->mRelatedTarget);
+
+ if (originalTarget && relatedTarget &&
+ originalTarget->FindFirstNonChromeOnlyAccessContent() ==
+ relatedTarget->FindFirstNonChromeOnlyAccessContent()) {
+ aVisitor.mCanHandle = false;
+ }
+ }
+}
+
+void HTMLInputElement::LegacyPreActivationBehavior(
+ EventChainVisitor& aVisitor) {
+ //
+ // Web pages expect the value of a radio button or checkbox to be set
+ // *before* onclick and DOMActivate fire, and they expect that if they set
+ // the value explicitly during onclick or DOMActivate it will not be toggled
+ // or any such nonsense.
+ // In order to support that (bug 57137 and 58460 are examples) we toggle
+ // the checked attribute *first*, and then fire onclick. If the user
+ // returns false, we reset the control to the old checked value. Otherwise,
+ // we dispatch DOMActivate. If DOMActivate is cancelled, we also reset
+ // the control to the old checked value. We need to keep track of whether
+ // we've already toggled the state from onclick since the user could
+ // explicitly dispatch DOMActivate on the element.
+ //
+ // These are compatibility hacks and are defined as legacy-pre-activation
+ // and legacy-canceled-activation behavior in HTML.
+ //
+
+ // Assert mType didn't change after GetEventTargetParent
+ MOZ_ASSERT(NS_CONTROL_TYPE(aVisitor.mItemFlags) == uint8_t(mType));
+
+ bool originalCheckedValue = false;
+ mCheckedIsToggled = false;
+
+ if (mType == FormControlType::InputCheckbox) {
+ if (mIndeterminate) {
+ // indeterminate is always set to FALSE when the checkbox is toggled
+ SetIndeterminateInternal(false, false);
+ aVisitor.mItemFlags |= NS_ORIGINAL_INDETERMINATE_VALUE;
+ }
+
+ originalCheckedValue = Checked();
+ DoSetChecked(!originalCheckedValue, true, true);
+ mCheckedIsToggled = true;
+
+ if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) {
+ aVisitor.mEventStatus = nsEventStatus_eConsumeDoDefault;
+ }
+ } else if (mType == FormControlType::InputRadio) {
+ HTMLInputElement* selectedRadioButton = GetSelectedRadioButton();
+ aVisitor.mItemData = static_cast<Element*>(selectedRadioButton);
+
+ originalCheckedValue = Checked();
+ if (!originalCheckedValue) {
+ DoSetChecked(true, true, true);
+ mCheckedIsToggled = true;
+ }
+
+ if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) {
+ aVisitor.mEventStatus = nsEventStatus_eConsumeDoDefault;
+ }
+ }
+
+ if (originalCheckedValue) {
+ aVisitor.mItemFlags |= NS_ORIGINAL_CHECKED_VALUE;
+ }
+
+ // out-of-spec legacy pre-activation behavior needed because of bug 1803805
+ if ((mType == FormControlType::InputSubmit ||
+ mType == FormControlType::InputImage) &&
+ mForm) {
+ aVisitor.mItemFlags |= NS_IN_SUBMIT_CLICK;
+ aVisitor.mItemData = static_cast<Element*>(mForm);
+ // tell the form that we are about to enter a click handler.
+ // that means that if there are scripted submissions, the
+ // latest one will be deferred until after the exit point of the
+ // handler.
+ mForm->OnSubmitClickBegin(this);
+ }
+}
+
+nsresult HTMLInputElement::PreHandleEvent(EventChainVisitor& aVisitor) {
+ if (aVisitor.mItemFlags & NS_PRE_HANDLE_BLUR_EVENT) {
+ MOZ_ASSERT(aVisitor.mEvent->mMessage == eBlur);
+ FireChangeEventIfNeeded();
+ }
+ return nsGenericHTMLFormControlElementWithState::PreHandleEvent(aVisitor);
+}
+
+void HTMLInputElement::StartRangeThumbDrag(WidgetGUIEvent* aEvent) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame());
+ if (!rangeFrame) {
+ return;
+ }
+
+ mIsDraggingRange = true;
+ mRangeThumbDragStartValue = GetValueAsDecimal();
+ // Don't use CaptureFlags::RetargetToElement, as that breaks pseudo-class
+ // styling of the thumb.
+ PresShell::SetCapturingContent(this, CaptureFlags::IgnoreAllowedState);
+
+ // Before we change the value, record the current value so that we'll
+ // correctly send a 'change' event if appropriate. We need to do this here
+ // because the 'focus' event is handled after the 'mousedown' event that
+ // we're being called for (i.e. too late to update mFocusedValue, since we'll
+ // have changed it by then).
+ GetValue(mFocusedValue, CallerType::System);
+
+ SetValueOfRangeForUserEvent(rangeFrame->GetValueAtEventPoint(aEvent),
+ SnapToTickMarks::Yes);
+}
+
+void HTMLInputElement::FinishRangeThumbDrag(WidgetGUIEvent* aEvent) {
+ MOZ_ASSERT(mIsDraggingRange);
+
+ if (PresShell::GetCapturingContent() == this) {
+ PresShell::ReleaseCapturingContent();
+ }
+ if (aEvent) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame());
+ SetValueOfRangeForUserEvent(rangeFrame->GetValueAtEventPoint(aEvent),
+ SnapToTickMarks::Yes);
+ }
+ mIsDraggingRange = false;
+ FireChangeEventIfNeeded();
+}
+
+void HTMLInputElement::CancelRangeThumbDrag(bool aIsForUserEvent) {
+ MOZ_ASSERT(mIsDraggingRange);
+
+ mIsDraggingRange = false;
+ if (PresShell::GetCapturingContent() == this) {
+ PresShell::ReleaseCapturingContent();
+ }
+ if (aIsForUserEvent) {
+ SetValueOfRangeForUserEvent(mRangeThumbDragStartValue,
+ SnapToTickMarks::Yes);
+ } else {
+ // Don't dispatch an 'input' event - at least not using
+ // DispatchTrustedEvent.
+ // TODO: decide what we should do here - bug 851782.
+ nsAutoString val;
+ mInputType->ConvertNumberToString(mRangeThumbDragStartValue, val);
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // is small, so we should be fine here.)
+ SetValueInternal(val, {ValueSetterOption::BySetUserInputAPI,
+ ValueSetterOption::SetValueChanged});
+ if (nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame())) {
+ frame->UpdateForValueChange();
+ }
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ }
+}
+
+void HTMLInputElement::SetValueOfRangeForUserEvent(
+ Decimal aValue, SnapToTickMarks aSnapToTickMarks) {
+ MOZ_ASSERT(aValue.isFinite());
+ if (aSnapToTickMarks == SnapToTickMarks::Yes) {
+ MaybeSnapToTickMark(aValue);
+ }
+
+ Decimal oldValue = GetValueAsDecimal();
+
+ nsAutoString val;
+ mInputType->ConvertNumberToString(aValue, val);
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // is small, so we should be fine here.)
+ SetValueInternal(val, {ValueSetterOption::BySetUserInputAPI,
+ ValueSetterOption::SetValueChanged});
+ if (nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame())) {
+ frame->UpdateForValueChange();
+ }
+
+ if (GetValueAsDecimal() != oldValue) {
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ }
+}
+
+void HTMLInputElement::StartNumberControlSpinnerSpin() {
+ MOZ_ASSERT(!mNumberControlSpinnerIsSpinning);
+
+ mNumberControlSpinnerIsSpinning = true;
+
+ nsRepeatService::GetInstance()->Start(
+ HandleNumberControlSpin, this, OwnerDoc(), "HandleNumberControlSpin"_ns);
+
+ // Capture the mouse so that we can tell if the pointer moves from one
+ // spin button to the other, or to some other element:
+ PresShell::SetCapturingContent(this, CaptureFlags::IgnoreAllowedState);
+
+ nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ numberControlFrame->SpinnerStateChanged();
+ }
+}
+
+void HTMLInputElement::StopNumberControlSpinnerSpin(SpinnerStopState aState) {
+ if (mNumberControlSpinnerIsSpinning) {
+ if (PresShell::GetCapturingContent() == this) {
+ PresShell::ReleaseCapturingContent();
+ }
+
+ nsRepeatService::GetInstance()->Stop(HandleNumberControlSpin, this);
+
+ mNumberControlSpinnerIsSpinning = false;
+
+ if (aState == eAllowDispatchingEvents) {
+ FireChangeEventIfNeeded();
+ }
+
+ nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ MOZ_ASSERT(aState == eAllowDispatchingEvents,
+ "Shouldn't have primary frame for the element when we're not "
+ "allowed to dispatch events to it anymore.");
+ numberControlFrame->SpinnerStateChanged();
+ }
+ }
+}
+
+void HTMLInputElement::StepNumberControlForUserEvent(int32_t aDirection) {
+ // We can't use GetValidityState here because the validity state is not set
+ // if the user hasn't previously taken an action to set or change the value,
+ // according to the specs.
+ if (HasBadInput()) {
+ // If the user has typed a value into the control and inadvertently made a
+ // mistake (e.g. put a thousand separator at the wrong point) we do not
+ // want to wipe out what they typed if they try to increment/decrement the
+ // value. Better is to highlight the value as being invalid so that they
+ // can correct what they typed.
+ // We only do this if there actually is a value typed in by/displayed to
+ // the user. (IsValid() can return false if the 'required' attribute is
+ // set and the value is the empty string.)
+ if (!IsValueEmpty()) {
+ // We pass 'true' for SetUserInteracted because we need the UI to update
+ // _now_ or the user will wonder why the step behavior isn't functioning.
+ SetUserInteracted(true);
+ return;
+ }
+ }
+
+ Decimal newValue = Decimal::nan(); // unchanged if value will not change
+
+ nsresult rv = GetValueIfStepped(aDirection, CALLED_FOR_USER_EVENT, &newValue);
+
+ if (NS_FAILED(rv) || !newValue.isFinite()) {
+ return; // value should not or will not change
+ }
+
+ nsAutoString newVal;
+ mInputType->ConvertNumberToString(newValue, newVal);
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // is small, so we should be fine here.)
+ SetValueInternal(newVal, {ValueSetterOption::BySetUserInputAPI,
+ ValueSetterOption::SetValueChanged});
+}
+
+static bool SelectTextFieldOnFocus() {
+ if (!gSelectTextFieldOnFocus) {
+ int32_t selectTextfieldsOnKeyFocus = -1;
+ nsresult rv =
+ LookAndFeel::GetInt(LookAndFeel::IntID::SelectTextfieldsOnKeyFocus,
+ &selectTextfieldsOnKeyFocus);
+ if (NS_FAILED(rv)) {
+ gSelectTextFieldOnFocus = -1;
+ } else {
+ gSelectTextFieldOnFocus = selectTextfieldsOnKeyFocus != 0 ? 1 : -1;
+ }
+ }
+
+ return gSelectTextFieldOnFocus == 1;
+}
+
+bool HTMLInputElement::ShouldPreventDOMActivateDispatch(
+ EventTarget* aOriginalTarget) {
+ /*
+ * For the moment, there is only one situation where we actually want to
+ * prevent firing a DOMActivate event:
+ * - we are a <input type='file'> that just got a click event,
+ * - the event was targeted to our button which should have sent a
+ * DOMActivate event.
+ */
+
+ if (mType != FormControlType::InputFile) {
+ return false;
+ }
+
+ Element* target = Element::FromEventTargetOrNull(aOriginalTarget);
+ if (!target) {
+ return false;
+ }
+
+ return target->GetParent() == this &&
+ target->IsRootOfNativeAnonymousSubtree() &&
+ target->IsHTMLElement(nsGkAtoms::button);
+}
+
+nsresult HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor) {
+ // Open a file picker when we receive a click on a <input type='file'>, or
+ // open a color picker when we receive a click on a <input type='color'>.
+ // A click is handled if it's the left mouse button.
+ // We do not prevent non-trusted click because authors can already use
+ // .click(). However, the pickers will follow the rules of popup-blocking.
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ if (!(mouseEvent && mouseEvent->IsLeftClickEvent())) {
+ return NS_OK;
+ }
+ if (mType == FormControlType::InputFile) {
+ // If the user clicked on the "Choose folder..." button we open the
+ // directory picker, else we open the file picker.
+ FilePickerType type = FILE_PICKER_FILE;
+ nsIContent* target =
+ nsIContent::FromEventTargetOrNull(aVisitor.mEvent->mOriginalTarget);
+ if (target && target->FindFirstNonChromeOnlyAccessContent() == this &&
+ StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ HasAttr(nsGkAtoms::webkitdirectory)) {
+ type = FILE_PICKER_DIRECTORY;
+ }
+ return InitFilePicker(type);
+ }
+ if (mType == FormControlType::InputColor) {
+ return InitColorPicker();
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Return true if the input event should be ignored because of its modifiers.
+ * Control is treated specially, since sometimes we ignore it, and sometimes
+ * we don't (for webcompat reasons).
+ */
+static bool IgnoreInputEventWithModifier(const WidgetInputEvent& aEvent,
+ bool ignoreControl) {
+ return (ignoreControl && aEvent.IsControl()) ||
+ aEvent.IsAltGraph()
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
+ // Meta key is the Windows Logo key on Windows and Linux which may
+ // assign some special meaning for the events while it's pressed.
+ // On the other hand, it's a normal modifier in macOS and Android.
+ // Therefore, We should ignore it only in Win/Linux.
+ || aEvent.IsMeta()
+#endif
+ || aEvent.IsFn();
+}
+
+bool HTMLInputElement::StepsInputValue(
+ const WidgetKeyboardEvent& aEvent) const {
+ if (mType != FormControlType::InputNumber) {
+ return false;
+ }
+ if (aEvent.mMessage != eKeyPress) {
+ return false;
+ }
+ if (!aEvent.IsTrusted()) {
+ return false;
+ }
+ if (aEvent.mKeyCode != NS_VK_UP && aEvent.mKeyCode != NS_VK_DOWN) {
+ return false;
+ }
+ if (IgnoreInputEventWithModifier(aEvent, false)) {
+ return false;
+ }
+ if (aEvent.DefaultPrevented()) {
+ return false;
+ }
+ if (!IsMutable()) {
+ return false;
+ }
+ return true;
+}
+
+static bool ActivatesWithKeyboard(FormControlType aType, uint32_t aKeyCode) {
+ switch (aType) {
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputRadio:
+ // Checkbox and Radio try to submit on Enter press
+ return aKeyCode != NS_VK_RETURN;
+ case FormControlType::InputButton:
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputFile:
+ case FormControlType::InputImage: // Bug 34418
+ case FormControlType::InputColor:
+ return true;
+ default:
+ return false;
+ }
+}
+
+nsresult HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
+ if (aVisitor.mEvent->mMessage == eBlur) {
+ if (mIsDraggingRange) {
+ FinishRangeThumbDrag();
+ } else if (mNumberControlSpinnerIsSpinning) {
+ StopNumberControlSpinnerSpin();
+ }
+ }
+
+ nsresult rv = NS_OK;
+ auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags));
+
+ // Ideally we would make the default action for click and space just dispatch
+ // DOMActivate, and the default action for DOMActivate flip the checkbox/
+ // radio state and fire onchange. However, for backwards compatibility, we
+ // need to flip the state before firing click, and we need to fire click
+ // when space is pressed. So, we just nest the firing of DOMActivate inside
+ // the click event handling, and allow cancellation of DOMActivate to cancel
+ // the click.
+ if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault &&
+ !IsSingleLineTextControl(true) && mType != FormControlType::InputNumber) {
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ if (mouseEvent && mouseEvent->IsLeftClickEvent() &&
+ OwnerDoc()->MayHaveDOMActivateListeners() &&
+ !ShouldPreventDOMActivateDispatch(aVisitor.mEvent->mOriginalTarget)) {
+ // DOMActive event should be trusted since the activation is actually
+ // occurred even if the cause is an untrusted click event.
+ InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent);
+ actEvent.mDetail = 1;
+
+ if (RefPtr<PresShell> presShell =
+ aVisitor.mPresContext ? aVisitor.mPresContext->GetPresShell()
+ : nullptr) {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ mInInternalActivate = true;
+ rv = presShell->HandleDOMEventWithTarget(this, &actEvent, &status);
+ mInInternalActivate = false;
+
+ // If activate is cancelled, we must do the same as when click is
+ // cancelled (revert the checkbox to its original value).
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ aVisitor.mEventStatus = status;
+ }
+ }
+ }
+ }
+
+ bool preventDefault =
+ aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault;
+ if (IsDisabled() && oldType != FormControlType::InputCheckbox &&
+ oldType != FormControlType::InputRadio) {
+ // Behave as if defaultPrevented when the element becomes disabled by event
+ // listeners. Checkboxes and radio buttons should still process clicks for
+ // web compat. See:
+ // https://html.spec.whatwg.org/multipage/input.html#the-input-element:activation-behaviour
+ preventDefault = true;
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent();
+ if (keyEvent && StepsInputValue(*keyEvent)) {
+ StepNumberControlForUserEvent(keyEvent->mKeyCode == NS_VK_UP ? 1 : -1);
+ FireChangeEventIfNeeded();
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ } else if (!preventDefault) {
+ if (keyEvent && ActivatesWithKeyboard(mType, keyEvent->mKeyCode) &&
+ keyEvent->IsTrusted()) {
+ // We maybe dispatch a synthesized click for keyboard activation.
+ HandleKeyboardActivation(aVisitor);
+ }
+
+ switch (aVisitor.mEvent->mMessage) {
+ case eFocus: {
+ // see if we should select the contents of the textbox. This happens
+ // for text and password fields when the field was focused by the
+ // keyboard or a navigation, the platform allows it, and it wasn't
+ // just because we raised a window.
+ //
+ // While it'd usually make sense, we don't do this for JS callers
+ // because it causes some compat issues, see bug 1712724 for example.
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm && IsSingleLineTextControl(false) &&
+ !aVisitor.mEvent->AsFocusEvent()->mFromRaise &&
+ SelectTextFieldOnFocus()) {
+ if (Document* document = GetComposedDoc()) {
+ uint32_t lastFocusMethod =
+ fm->GetLastFocusMethod(document->GetWindow());
+ const bool shouldSelectAllOnFocus = [&] {
+ if (lastFocusMethod & nsIFocusManager::FLAG_BYMOVEFOCUS) {
+ return true;
+ }
+ if (lastFocusMethod & nsIFocusManager::FLAG_BYJS) {
+ return false;
+ }
+ return bool(lastFocusMethod & nsIFocusManager::FLAG_BYKEY);
+ }();
+ if (shouldSelectAllOnFocus) {
+ RefPtr<nsPresContext> presContext =
+ GetPresContext(eForComposedDoc);
+ SelectAll(presContext);
+ }
+ }
+ }
+ break;
+ }
+
+ case eKeyDown: {
+ // For compatibility with the other browsers, we should active this
+ // element at least when a checkbox or a radio button.
+ // TODO: Investigate which elements are activated by space key in the
+ // other browsers.
+ if (aVisitor.mPresContext && keyEvent->IsTrusted() && !IsDisabled() &&
+ keyEvent->ShouldWorkAsSpaceKey() &&
+ (mType == FormControlType::InputCheckbox ||
+ mType == FormControlType::InputRadio)) {
+ EventStateManager::SetActiveManager(
+ aVisitor.mPresContext->EventStateManager(), this);
+ }
+ break;
+ }
+
+ case eKeyPress: {
+ if (mType == FormControlType::InputRadio && keyEvent->IsTrusted() &&
+ !keyEvent->IsAlt() && !keyEvent->IsControl() &&
+ !keyEvent->IsMeta()) {
+ rv = MaybeHandleRadioButtonNavigation(aVisitor, keyEvent->mKeyCode);
+ }
+
+ /*
+ * For some input types, if the user hits enter, the form is
+ * submitted.
+ *
+ * Bug 99920, bug 109463 and bug 147850:
+ * (a) if there is a submit control in the form, click the first
+ * submit control in the form.
+ * (b) if there is just one text control in the form, submit by
+ * sending a submit event directly to the form
+ * (c) if there is more than one text input and no submit buttons, do
+ * not submit, period.
+ */
+
+ if (keyEvent->mKeyCode == NS_VK_RETURN && keyEvent->IsTrusted() &&
+ (IsSingleLineTextControl(false, mType) ||
+ IsDateTimeInputType(mType) ||
+ mType == FormControlType::InputCheckbox ||
+ mType == FormControlType::InputRadio)) {
+ if (IsSingleLineTextControl(false, mType) ||
+ IsDateTimeInputType(mType)) {
+ FireChangeEventIfNeeded();
+ }
+
+ if (aVisitor.mPresContext) {
+ MaybeSubmitForm(aVisitor.mPresContext);
+ }
+ }
+
+ if (mType == FormControlType::InputRange && keyEvent->IsTrusted() &&
+ !keyEvent->IsAlt() && !keyEvent->IsControl() &&
+ !keyEvent->IsMeta() &&
+ (keyEvent->mKeyCode == NS_VK_LEFT ||
+ keyEvent->mKeyCode == NS_VK_RIGHT ||
+ keyEvent->mKeyCode == NS_VK_UP ||
+ keyEvent->mKeyCode == NS_VK_DOWN ||
+ keyEvent->mKeyCode == NS_VK_PAGE_UP ||
+ keyEvent->mKeyCode == NS_VK_PAGE_DOWN ||
+ keyEvent->mKeyCode == NS_VK_HOME ||
+ keyEvent->mKeyCode == NS_VK_END)) {
+ Decimal minimum = GetMinimum();
+ Decimal maximum = GetMaximum();
+ MOZ_ASSERT(minimum.isFinite() && maximum.isFinite());
+ if (minimum < maximum) { // else the value is locked to the minimum
+ Decimal value = GetValueAsDecimal();
+ Decimal step = GetStep();
+ if (step == kStepAny) {
+ step = GetDefaultStep();
+ }
+ MOZ_ASSERT(value.isFinite() && step.isFinite());
+ Decimal newValue;
+ switch (keyEvent->mKeyCode) {
+ case NS_VK_LEFT:
+ newValue = value +
+ (GetComputedDirectionality() == Directionality::Rtl
+ ? step
+ : -step);
+ break;
+ case NS_VK_RIGHT:
+ newValue = value +
+ (GetComputedDirectionality() == Directionality::Rtl
+ ? -step
+ : step);
+ break;
+ case NS_VK_UP:
+ // Even for horizontal range, "up" means "increase"
+ newValue = value + step;
+ break;
+ case NS_VK_DOWN:
+ // Even for horizontal range, "down" means "decrease"
+ newValue = value - step;
+ break;
+ case NS_VK_HOME:
+ newValue = minimum;
+ break;
+ case NS_VK_END:
+ newValue = maximum;
+ break;
+ case NS_VK_PAGE_UP:
+ // For PgUp/PgDn we jump 10% of the total range, unless step
+ // requires us to jump more.
+ newValue =
+ value + std::max(step, (maximum - minimum) / Decimal(10));
+ break;
+ case NS_VK_PAGE_DOWN:
+ newValue =
+ value - std::max(step, (maximum - minimum) / Decimal(10));
+ break;
+ }
+ SetValueOfRangeForUserEvent(newValue);
+ FireChangeEventIfNeeded();
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ }
+
+ } break; // eKeyPress
+
+ case eMouseDown:
+ case eMouseUp:
+ case eMouseDoubleClick: {
+ // cancel all of these events for buttons
+ // XXXsmaug Why?
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ if (mouseEvent->mButton == MouseButton::eMiddle ||
+ mouseEvent->mButton == MouseButton::eSecondary) {
+ if (mType == FormControlType::InputButton ||
+ mType == FormControlType::InputReset ||
+ mType == FormControlType::InputSubmit) {
+ if (aVisitor.mDOMEvent) {
+ aVisitor.mDOMEvent->StopPropagation();
+ } else {
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+ }
+ if (mType == FormControlType::InputNumber &&
+ aVisitor.mEvent->IsTrusted()) {
+ if (mouseEvent->mButton == MouseButton::ePrimary &&
+ !IgnoreInputEventWithModifier(*mouseEvent, false)) {
+ nsNumberControlFrame* numberControlFrame =
+ do_QueryFrame(GetPrimaryFrame());
+ if (numberControlFrame) {
+ if (aVisitor.mEvent->mMessage == eMouseDown && IsMutable()) {
+ switch (numberControlFrame->GetSpinButtonForPointerEvent(
+ aVisitor.mEvent->AsMouseEvent())) {
+ case nsNumberControlFrame::eSpinButtonUp:
+ StepNumberControlForUserEvent(1);
+ mNumberControlSpinnerSpinsUp = true;
+ StartNumberControlSpinnerSpin();
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ break;
+ case nsNumberControlFrame::eSpinButtonDown:
+ StepNumberControlForUserEvent(-1);
+ mNumberControlSpinnerSpinsUp = false;
+ StartNumberControlSpinnerSpin();
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ break;
+ }
+ }
+ }
+ }
+ if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) {
+ // We didn't handle this to step up/down. Whatever this was, be
+ // aggressive about stopping the spin. (And don't set
+ // nsEventStatus_eConsumeNoDefault after doing so, since that
+ // might prevent, say, the context menu from opening.)
+ StopNumberControlSpinnerSpin();
+ }
+ }
+ break;
+ }
+#if !defined(ANDROID) && !defined(XP_MACOSX)
+ case eWheel: {
+ // Handle wheel events as increasing / decreasing the input element's
+ // value when it's focused and it's type is number or range.
+ WidgetWheelEvent* wheelEvent = aVisitor.mEvent->AsWheelEvent();
+ if (!aVisitor.mEvent->DefaultPrevented() &&
+ aVisitor.mEvent->IsTrusted() && IsMutable() && wheelEvent &&
+ wheelEvent->mDeltaY != 0 &&
+ wheelEvent->mDeltaMode != WheelEvent_Binding::DOM_DELTA_PIXEL) {
+ if (mType == FormControlType::InputNumber) {
+ if (nsContentUtils::IsFocusedContent(this)) {
+ StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1);
+ FireChangeEventIfNeeded();
+ aVisitor.mEvent->PreventDefault();
+ }
+ } else if (mType == FormControlType::InputRange &&
+ nsContentUtils::IsFocusedContent(this) &&
+ GetMinimum() < GetMaximum()) {
+ Decimal value = GetValueAsDecimal();
+ Decimal step = GetStep();
+ if (step == kStepAny) {
+ step = GetDefaultStep();
+ }
+ MOZ_ASSERT(value.isFinite() && step.isFinite());
+ SetValueOfRangeForUserEvent(
+ wheelEvent->mDeltaY < 0 ? value + step : value - step);
+ FireChangeEventIfNeeded();
+ aVisitor.mEvent->PreventDefault();
+ }
+ }
+ break;
+ }
+#endif
+ case eMouseClick: {
+ if (!aVisitor.mEvent->DefaultPrevented() &&
+ aVisitor.mEvent->IsTrusted() &&
+ aVisitor.mEvent->AsMouseEvent()->mButton ==
+ MouseButton::ePrimary) {
+ // TODO(emilio): Handling this should ideally not move focus.
+ if (mType == FormControlType::InputSearch) {
+ if (nsSearchControlFrame* searchControlFrame =
+ do_QueryFrame(GetPrimaryFrame())) {
+ Element* clearButton = searchControlFrame->GetAnonClearButton();
+ if (clearButton &&
+ aVisitor.mEvent->mOriginalTarget == clearButton) {
+ SetUserInput(EmptyString(),
+ *nsContentUtils::GetSystemPrincipal());
+ // TODO(emilio): This should focus the input, but calling
+ // SetFocus(this, FLAG_NOSCROLL) for some reason gets us into
+ // an inconsistent state where we're focused but don't match
+ // :focus-visible / :focus.
+ }
+ }
+ } else if (mType == FormControlType::InputPassword) {
+ if (nsTextControlFrame* textControlFrame =
+ do_QueryFrame(GetPrimaryFrame())) {
+ auto* reveal = textControlFrame->GetRevealButton();
+ if (reveal && aVisitor.mEvent->mOriginalTarget == reveal) {
+ SetRevealPassword(!RevealPassword());
+ // TODO(emilio): This should focus the input, but calling
+ // SetFocus(this, FLAG_NOSCROLL) for some reason gets us into
+ // an inconsistent state where we're focused but don't match
+ // :focus-visible / :focus.
+ }
+ }
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ // Bug 1459231: Temporarily needed till links respect activation target,
+ // then also remove NS_OUTER_ACTIVATE_EVENT. The appropriate
+ // behavior/model for links is still under discussion (see
+ // https://github.com/whatwg/html/issues/1576). For now, we aim for
+ // consistency with other browsers.
+ if (aVisitor.mItemFlags & NS_OUTER_ACTIVATE_EVENT) {
+ switch (mType) {
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ if (mForm) {
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ }
+ break;
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputRadio:
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ } // if
+
+ if (NS_SUCCEEDED(rv) && mType == FormControlType::InputRange) {
+ PostHandleEventForRangeThumb(aVisitor);
+ }
+
+ if (!preventDefault) {
+ MOZ_TRY(MaybeInitPickers(aVisitor));
+ }
+ return NS_OK;
+}
+
+void EndSubmitClick(EventChainPostVisitor& aVisitor) {
+ auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags));
+ if ((aVisitor.mItemFlags & NS_IN_SUBMIT_CLICK) &&
+ (oldType == FormControlType::InputSubmit ||
+ oldType == FormControlType::InputImage)) {
+ nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mItemData));
+ RefPtr<HTMLFormElement> form = HTMLFormElement::FromNodeOrNull(content);
+ // Tell the form that we are about to exit a click handler,
+ // so the form knows not to defer subsequent submissions.
+ // The pending ones that were created during the handler
+ // will be flushed or forgotten.
+ form->OnSubmitClickEnd();
+ // tell the form to flush a possible pending submission.
+ // the reason is that the script returned false (the event was
+ // not ignored) so if there is a stored submission, it needs to
+ // be submitted immediately.
+ form->FlushPendingSubmission();
+ }
+}
+
+void HTMLInputElement::ActivationBehavior(EventChainPostVisitor& aVisitor) {
+ auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags));
+
+ if (IsDisabled() && oldType != FormControlType::InputCheckbox &&
+ oldType != FormControlType::InputRadio) {
+ // Behave as if defaultPrevented when the element becomes disabled by event
+ // listeners. Checkboxes and radio buttons should still process clicks for
+ // web compat. See:
+ // https://html.spec.whatwg.org/multipage/input.html#the-input-element:activation-behaviour
+ EndSubmitClick(aVisitor);
+ return;
+ }
+
+ if (mCheckedIsToggled) {
+ SetUserInteracted(true);
+
+ // Fire input event and then change event.
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+
+ // FIXME: Why is this different than every other change event?
+ nsContentUtils::DispatchTrustedEvent<WidgetEvent>(
+ OwnerDoc(), static_cast<Element*>(this), eFormChange, CanBubble::eYes,
+ Cancelable::eNo);
+#ifdef ACCESSIBILITY
+ // Fire an event to notify accessibility
+ if (mType == FormControlType::InputCheckbox) {
+ if (nsContentUtils::MayHaveFormCheckboxStateChangeListeners()) {
+ FireEventForAccessibility(this, eFormCheckboxStateChange);
+ }
+ } else if (nsContentUtils::MayHaveFormRadioStateChangeListeners()) {
+ FireEventForAccessibility(this, eFormRadioStateChange);
+ // Fire event for the previous selected radio.
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aVisitor.mItemData);
+ if (auto* previous = HTMLInputElement::FromNodeOrNull(content)) {
+ FireEventForAccessibility(previous, eFormRadioStateChange);
+ }
+ }
+#endif
+ }
+
+ switch (mType) {
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ if (mForm) {
+ // Hold a strong ref while dispatching
+ RefPtr<HTMLFormElement> form(mForm);
+ if (mType == FormControlType::InputReset) {
+ form->MaybeReset(this);
+ } else {
+ form->MaybeSubmit(this);
+ }
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ break;
+
+ default:
+ break;
+ } // switch
+ if (IsButtonControl()) {
+ if (!GetInvokeTargetElement()) {
+ HandlePopoverTargetAction();
+ } else {
+ HandleInvokeTargetAction();
+ }
+ }
+
+ EndSubmitClick(aVisitor);
+}
+
+void HTMLInputElement::LegacyCanceledActivationBehavior(
+ EventChainPostVisitor& aVisitor) {
+ bool originalCheckedValue =
+ !!(aVisitor.mItemFlags & NS_ORIGINAL_CHECKED_VALUE);
+ auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags));
+
+ if (mCheckedIsToggled) {
+ // if it was canceled and a radio button, then set the old
+ // selected btn to TRUE. if it is a checkbox then set it to its
+ // original value (legacy-canceled-activation)
+ if (oldType == FormControlType::InputRadio) {
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aVisitor.mItemData);
+ HTMLInputElement* selectedRadioButton =
+ HTMLInputElement::FromNodeOrNull(content);
+ if (selectedRadioButton) {
+ selectedRadioButton->SetChecked(true);
+ }
+ // If there was no checked radio button or this one is no longer a
+ // radio button we must reset it back to false to cancel the action.
+ // See how the web of hack grows?
+ if (!selectedRadioButton || mType != FormControlType::InputRadio) {
+ DoSetChecked(false, true, true);
+ }
+ } else if (oldType == FormControlType::InputCheckbox) {
+ bool originalIndeterminateValue =
+ !!(aVisitor.mItemFlags & NS_ORIGINAL_INDETERMINATE_VALUE);
+ SetIndeterminateInternal(originalIndeterminateValue, false);
+ DoSetChecked(originalCheckedValue, true, true);
+ }
+ }
+
+ // Relevant for bug 242494: submit button with "submit(); return false;"
+ EndSubmitClick(aVisitor);
+}
+
+enum class RadioButtonMove { Back, Forward, None };
+nsresult HTMLInputElement::MaybeHandleRadioButtonNavigation(
+ EventChainPostVisitor& aVisitor, uint32_t aKeyCode) {
+ auto move = [&] {
+ switch (aKeyCode) {
+ case NS_VK_UP:
+ return RadioButtonMove::Back;
+ case NS_VK_DOWN:
+ return RadioButtonMove::Forward;
+ case NS_VK_LEFT:
+ case NS_VK_RIGHT: {
+ const bool isRtl = GetComputedDirectionality() == Directionality::Rtl;
+ return isRtl == (aKeyCode == NS_VK_LEFT) ? RadioButtonMove::Forward
+ : RadioButtonMove::Back;
+ }
+ }
+ return RadioButtonMove::None;
+ }();
+ if (move == RadioButtonMove::None) {
+ return NS_OK;
+ }
+ // Arrow key pressed, focus+select prev/next radio button
+ RefPtr<HTMLInputElement> selectedRadioButton;
+ if (auto* container = GetCurrentRadioGroupContainer()) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ container->GetNextRadioButton(name, move == RadioButtonMove::Back, this,
+ getter_AddRefs(selectedRadioButton));
+ }
+ if (!selectedRadioButton) {
+ return NS_OK;
+ }
+ FocusOptions options;
+ ErrorResult error;
+ selectedRadioButton->Focus(options, CallerType::System, error);
+ if (error.Failed()) {
+ return error.StealNSResult();
+ }
+ nsresult rv = DispatchSimulatedClick(
+ selectedRadioButton, aVisitor.mEvent->IsTrusted(), aVisitor.mPresContext);
+ if (NS_SUCCEEDED(rv)) {
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ return rv;
+}
+
+void HTMLInputElement::PostHandleEventForRangeThumb(
+ EventChainPostVisitor& aVisitor) {
+ MOZ_ASSERT(mType == FormControlType::InputRange);
+
+ if (nsEventStatus_eConsumeNoDefault == aVisitor.mEventStatus ||
+ !(aVisitor.mEvent->mClass == eMouseEventClass ||
+ aVisitor.mEvent->mClass == eTouchEventClass ||
+ aVisitor.mEvent->mClass == eKeyboardEventClass)) {
+ return;
+ }
+
+ nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame());
+ if (!rangeFrame && mIsDraggingRange) {
+ CancelRangeThumbDrag();
+ return;
+ }
+
+ switch (aVisitor.mEvent->mMessage) {
+ case eMouseDown:
+ case eTouchStart: {
+ if (mIsDraggingRange) {
+ break;
+ }
+ if (PresShell::GetCapturingContent()) {
+ break; // don't start drag if someone else is already capturing
+ }
+ WidgetInputEvent* inputEvent = aVisitor.mEvent->AsInputEvent();
+ if (IgnoreInputEventWithModifier(*inputEvent, true)) {
+ break; // ignore
+ }
+ if (aVisitor.mEvent->mMessage == eMouseDown) {
+ if (aVisitor.mEvent->AsMouseEvent()->mButtons ==
+ MouseButtonsFlag::ePrimaryFlag) {
+ StartRangeThumbDrag(inputEvent);
+ } else if (mIsDraggingRange) {
+ CancelRangeThumbDrag();
+ }
+ } else {
+ if (aVisitor.mEvent->AsTouchEvent()->mTouches.Length() == 1) {
+ StartRangeThumbDrag(inputEvent);
+ } else if (mIsDraggingRange) {
+ CancelRangeThumbDrag();
+ }
+ }
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ } break;
+
+ case eMouseMove:
+ case eTouchMove:
+ if (!mIsDraggingRange) {
+ break;
+ }
+ if (PresShell::GetCapturingContent() != this) {
+ // Someone else grabbed capture.
+ CancelRangeThumbDrag();
+ break;
+ }
+ SetValueOfRangeForUserEvent(
+ rangeFrame->GetValueAtEventPoint(aVisitor.mEvent->AsInputEvent()),
+ SnapToTickMarks::Yes);
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ break;
+
+ case eMouseUp:
+ case eTouchEnd:
+ if (!mIsDraggingRange) {
+ break;
+ }
+ // We don't check to see whether we are the capturing content here and
+ // call CancelRangeThumbDrag() if that is the case. We just finish off
+ // the drag and set our final value (unless someone has called
+ // preventDefault() and prevents us getting here).
+ FinishRangeThumbDrag(aVisitor.mEvent->AsInputEvent());
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true;
+ break;
+
+ case eKeyPress:
+ if (mIsDraggingRange &&
+ aVisitor.mEvent->AsKeyboardEvent()->mKeyCode == NS_VK_ESCAPE) {
+ CancelRangeThumbDrag();
+ }
+ break;
+
+ case eTouchCancel:
+ if (mIsDraggingRange) {
+ CancelRangeThumbDrag();
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+void HTMLInputElement::MaybeLoadImage() {
+ // Our base URI may have changed; claim that our URI changed, and the
+ // nsImageLoadingContent will decide whether a new image load is warranted.
+ nsAutoString uri;
+ if (mType == FormControlType::InputImage && GetAttr(nsGkAtoms::src, uri) &&
+ (NS_FAILED(LoadImage(uri, false, true, eImageLoadType_Normal,
+ mSrcTriggeringPrincipal)) ||
+ !LoadingEnabled())) {
+ CancelImageRequests(true);
+ }
+}
+
+nsresult HTMLInputElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ // If we are currently bound to a disconnected subtree root, remove
+ // ourselves from it first.
+ if (!mForm && mType == FormControlType::InputRadio) {
+ RemoveFromRadioGroup();
+ }
+
+ nsresult rv =
+ nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsImageLoadingContent::BindToTree(aContext, aParent);
+
+ if (mType == FormControlType::InputImage) {
+ // Our base URI may have changed; claim that our URI changed, and the
+ // nsImageLoadingContent will decide whether a new image load is warranted.
+ if (HasAttr(nsGkAtoms::src)) {
+ // Mark channel as urgent-start before load image if the image load is
+ // initaiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod("dom::HTMLInputElement::MaybeLoadImage", this,
+ &HTMLInputElement::MaybeLoadImage));
+ }
+ }
+
+ // Add radio to document if we don't have a form already (if we do it's
+ // already been added into that group)
+ if (!mForm && mType == FormControlType::InputRadio) {
+ AddToRadioGroup();
+ }
+
+ // Set direction based on value if dir=auto
+ if (HasDirAuto()) {
+ SetAutoDirectionality(false);
+ }
+
+ // An element can't suffer from value missing if it is not in a document.
+ // We have to check if we suffer from that as we are now in a document.
+ UpdateValueMissingValidityState();
+
+ // If there is a disabled fieldset in the parent chain, the element is now
+ // barred from constraint validation and can't suffer from value missing
+ // (call done before).
+ UpdateBarredFromConstraintValidation();
+
+ // And now make sure our state is up to date
+ UpdateValidityElementStates(true);
+
+ if (CreatesDateTimeWidget() && IsInComposedDoc()) {
+ // Construct Shadow Root so web content can be hidden in the DOM.
+ AttachAndSetUAShadowRoot(NotifyUAWidgetSetup::Yes, DelegatesFocus::Yes);
+ }
+
+ MaybeDispatchLoginManagerEvents(mForm);
+
+ return rv;
+}
+
+void HTMLInputElement::MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm) {
+ // Don't disptach the event if the <input> is disconnected
+ // or belongs to a disconnected form
+ if (!IsInComposedDoc()) {
+ return;
+ }
+
+ nsString eventType;
+ Element* target = nullptr;
+
+ if (mType == FormControlType::InputPassword) {
+ // Don't fire another event if we have a pending event.
+ if (aForm && aForm->mHasPendingPasswordEvent) {
+ return;
+ }
+
+ // TODO(Bug 1864404): Use one event for formless and form inputs.
+ eventType = aForm ? u"DOMFormHasPassword"_ns : u"DOMInputPasswordAdded"_ns;
+
+ target = aForm ? static_cast<Element*>(aForm) : this;
+
+ if (aForm) {
+ aForm->mHasPendingPasswordEvent = true;
+ }
+
+ } else if (mType == FormControlType::InputEmail ||
+ mType == FormControlType::InputText) {
+ // Don't fire a username event if:
+ // - <input> is not part of a form
+ // - we have a pending event
+ // - username only forms are not supported
+ if (!aForm || aForm->mHasPendingPossibleUsernameEvent ||
+ !StaticPrefs::signon_usernameOnlyForm_enabled()) {
+ return;
+ }
+
+ eventType = u"DOMFormHasPossibleUsername"_ns;
+ target = aForm;
+
+ aForm->mHasPendingPossibleUsernameEvent = true;
+
+ } else {
+ return;
+ }
+
+ RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher(
+ target, eventType, CanBubble::eYes, ChromeOnlyDispatch::eYes);
+ dispatcher->PostDOMEvent();
+}
+
+void HTMLInputElement::UnbindFromTree(bool aNullParent) {
+ if (mType == FormControlType::InputPassword) {
+ MaybeFireInputPasswordRemoved();
+ }
+
+ // If we have a form and are unbound from it,
+ // nsGenericHTMLFormControlElementWithState::UnbindFromTree() will unset the
+ // form and that takes care of form's WillRemove so we just have to take care
+ // of the case where we're removing from the document and we don't
+ // have a form
+ if (!mForm && mType == FormControlType::InputRadio) {
+ RemoveFromRadioGroup();
+ }
+
+ if (CreatesDateTimeWidget() && IsInComposedDoc()) {
+ NotifyUAWidgetTeardown();
+ }
+
+ nsImageLoadingContent::UnbindFromTree(aNullParent);
+ nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent);
+
+ // If we are contained within a disconnected subtree, attempt to add
+ // ourselves to the subtree root's radio group.
+ if (!mForm && mType == FormControlType::InputRadio) {
+ AddToRadioGroup();
+ }
+
+ // GetCurrentDoc is returning nullptr so we can update the value
+ // missing validity state to reflect we are no longer into a doc.
+ UpdateValueMissingValidityState();
+ // We might be no longer disabled because of parent chain changed.
+ UpdateBarredFromConstraintValidation();
+ // And now make sure our state is up to date
+ UpdateValidityElementStates(false);
+}
+
+/**
+ * @param aType InputElementTypes
+ * @return true, iff SetRangeText applies to aType as specified at
+ * https://html.spec.whatwg.org/#concept-input-apply.
+ */
+static bool SetRangeTextApplies(FormControlType aType) {
+ return aType == FormControlType::InputText ||
+ aType == FormControlType::InputSearch ||
+ aType == FormControlType::InputUrl ||
+ aType == FormControlType::InputTel ||
+ aType == FormControlType::InputPassword;
+}
+
+void HTMLInputElement::HandleTypeChange(FormControlType aNewType,
+ bool aNotify) {
+ FormControlType oldType = mType;
+ MOZ_ASSERT(oldType != aNewType);
+
+ mHasBeenTypePassword =
+ mHasBeenTypePassword || aNewType == FormControlType::InputPassword;
+
+ if (nsFocusManager* fm = nsFocusManager::GetFocusManager()) {
+ // Input element can represent very different kinds of UIs, and we may
+ // need to flush styling even when focusing the already focused input
+ // element.
+ fm->NeedsFlushBeforeEventHandling(this);
+ }
+
+ if (oldType == FormControlType::InputPassword &&
+ State().HasState(ElementState::REVEALED)) {
+ // Modify the state directly to avoid dispatching events.
+ RemoveStates(ElementState::REVEALED, aNotify);
+ }
+
+ if (aNewType == FormControlType::InputFile ||
+ oldType == FormControlType::InputFile) {
+ if (aNewType == FormControlType::InputFile) {
+ mFileData.reset(new FileData());
+ } else {
+ mFileData->Unlink();
+ mFileData = nullptr;
+ }
+ }
+
+ if (oldType == FormControlType::InputRange && mIsDraggingRange) {
+ CancelRangeThumbDrag(false);
+ }
+
+ const ValueModeType oldValueMode = GetValueMode();
+ nsAutoString oldValue;
+ if (oldValueMode == VALUE_MODE_VALUE) {
+ // Doesn't matter what caller type we pass here, since we know we're not a
+ // file input anyway.
+ GetValue(oldValue, CallerType::NonSystem);
+ }
+
+ TextControlState::SelectionProperties sp;
+
+ if (IsSingleLineTextControl(false) && mInputData.mState) {
+ mInputData.mState->SyncUpSelectionPropertiesBeforeDestruction();
+ sp = mInputData.mState->GetSelectionProperties();
+ }
+
+ // We already have a copy of the value, lets free it and changes the type.
+ FreeData();
+ mType = aNewType;
+ void* memory = mInputTypeMem;
+ mInputType = InputType::Create(this, mType, memory);
+
+ if (IsSingleLineTextControl()) {
+ mInputData.mState = TextControlState::Construct(this);
+ if (!sp.IsDefault()) {
+ mInputData.mState->SetSelectionProperties(sp);
+ }
+ }
+
+ // Whether placeholder applies might have changed.
+ UpdatePlaceholderShownState();
+ // Whether readonly applies might have changed.
+ UpdateReadOnlyState(aNotify);
+ UpdateCheckedState(aNotify);
+ UpdateIndeterminateState(aNotify);
+ const bool isDefault = IsRadioOrCheckbox()
+ ? DefaultChecked()
+ : (mForm && mForm->IsDefaultSubmitElement(this));
+ SetStates(ElementState::DEFAULT, isDefault, aNotify);
+
+ // https://html.spec.whatwg.org/#input-type-change
+ switch (GetValueMode()) {
+ case VALUE_MODE_DEFAULT:
+ case VALUE_MODE_DEFAULT_ON:
+ // 1. If the previous state of the element's type attribute put the value
+ // IDL attribute in the value mode, and the element's value is not the
+ // empty string, and the new state of the element's type attribute puts
+ // the value IDL attribute in either the default mode or the default/on
+ // mode, then set the element's value content attribute to the
+ // element's value.
+ if (oldValueMode == VALUE_MODE_VALUE && !oldValue.IsEmpty()) {
+ SetAttr(kNameSpaceID_None, nsGkAtoms::value, oldValue, true);
+ }
+ break;
+ case VALUE_MODE_VALUE: {
+ ValueSetterOptions options{ValueSetterOption::ByInternalAPI};
+ if (!SetRangeTextApplies(oldType) && SetRangeTextApplies(mType)) {
+ options +=
+ ValueSetterOption::MoveCursorToBeginSetSelectionDirectionForward;
+ }
+ if (oldValueMode != VALUE_MODE_VALUE) {
+ // 2. Otherwise, if the previous state of the element's type attribute
+ // put the value IDL attribute in any mode other than the value
+ // mode, and the new state of the element's type attribute puts the
+ // value IDL attribute in the value mode, then set the value of the
+ // element to the value of the value content attribute, if there is
+ // one, or the empty string otherwise, and then set the control's
+ // dirty value flag to false.
+ nsAutoString value;
+ GetAttr(nsGkAtoms::value, value);
+ SetValueInternal(value, options);
+ SetValueChanged(false);
+ } else if (mValueChanged) {
+ // We're both in the "value" mode state, we need to make no change per
+ // spec, but due to how we store the value internally we need to call
+ // SetValueInternal, if our value had changed at all.
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // may potentially be big, but most likely we've failed to allocate
+ // before the type change.)
+ SetValueInternal(oldValue, options);
+ } else {
+ // The value dirty flag is not set, so our value is based on our default
+ // value. But our default value might be dependent on the type. Make
+ // sure to set it so that state is consistent.
+ SetDefaultValueAsValue();
+ }
+ break;
+ }
+ case VALUE_MODE_FILENAME:
+ default:
+ // 3. Otherwise, if the previous state of the element's type attribute
+ // put the value IDL attribute in any mode other than the filename
+ // mode, and the new state of the element's type attribute puts the
+ // value IDL attribute in the filename mode, then set the value of the
+ // element to the empty string.
+ //
+ // Setting the attribute to the empty string is basically calling
+ // ClearFiles, but there can't be any files.
+ break;
+ }
+
+ // Updating mFocusedValue in consequence:
+ // If the new type fires a change event on blur, but the previous type
+ // doesn't, we should set mFocusedValue to the current value.
+ // Otherwise, if the new type doesn't fire a change event on blur, but the
+ // previous type does, we should clear out mFocusedValue.
+ if (MayFireChangeOnBlur(mType) && !MayFireChangeOnBlur(oldType)) {
+ GetValue(mFocusedValue, CallerType::System);
+ } else if (!IsSingleLineTextControl(false, mType) &&
+ IsSingleLineTextControl(false, oldType)) {
+ mFocusedValue.Truncate();
+ }
+
+ // Update or clear our required states since we may have changed from a
+ // required input type to a non-required input type or viceversa.
+ if (DoesRequiredApply()) {
+ const bool isRequired = HasAttr(nsGkAtoms::required);
+ UpdateRequiredState(isRequired, aNotify);
+ } else {
+ RemoveStates(ElementState::REQUIRED_STATES, aNotify);
+ }
+
+ UpdateHasRange(aNotify);
+
+ // Update validity states, but not element state. We'll update
+ // element state later, as part of this attribute change.
+ UpdateAllValidityStatesButNotElementState();
+
+ UpdateApzAwareFlag();
+
+ UpdateBarredFromConstraintValidation();
+
+ // Changing type may affect auto directionality, or non-auto directionality
+ // because of the special-case for <input type=tel>, as specified in
+ // https://html.spec.whatwg.org/multipage/dom.html#the-directionality
+ if (HasDirAuto()) {
+ const bool autoDirAssociated = IsAutoDirectionalityAssociated(mType);
+ if (IsAutoDirectionalityAssociated(oldType) != autoDirAssociated) {
+ SetAutoDirectionality(aNotify);
+ }
+ } else if (oldType == FormControlType::InputTel ||
+ mType == FormControlType::InputTel) {
+ RecomputeDirectionality(this, aNotify);
+ }
+
+ if (oldType == FormControlType::InputImage ||
+ mType == FormControlType::InputImage) {
+ if (oldType == FormControlType::InputImage) {
+ // We're no longer an image input. Cancel our image requests, if we have
+ // any.
+ CancelImageRequests(aNotify);
+ RemoveStates(ElementState::BROKEN, aNotify);
+ } else {
+ // We just got switched to be an image input; we should see whether we
+ // have an image to load;
+ bool hasSrc = false;
+ if (aNotify) {
+ nsAutoString src;
+ if ((hasSrc = GetAttr(nsGkAtoms::src, src))) {
+ // Mark channel as urgent-start before load image if the image load is
+ // initiated by a user interaction.
+ mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput();
+
+ LoadImage(src, false, aNotify, eImageLoadType_Normal,
+ mSrcTriggeringPrincipal);
+ }
+ } else {
+ hasSrc = HasAttr(nsGkAtoms::src);
+ }
+ if (!hasSrc) {
+ AddStates(ElementState::BROKEN, aNotify);
+ }
+ }
+ // We should update our mapped attribute mapping function.
+ if (mAttrs.HasAttrs() && !mAttrs.IsPendingMappedAttributeEvaluation()) {
+ mAttrs.InfallibleMarkAsPendingPresAttributeEvaluation();
+ if (auto* doc = GetComposedDoc()) {
+ doc->ScheduleForPresAttrEvaluation(this);
+ }
+ }
+ }
+
+ MaybeDispatchLoginManagerEvents(mForm);
+
+ if (IsInComposedDoc()) {
+ if (CreatesDateTimeWidget(oldType)) {
+ if (!CreatesDateTimeWidget()) {
+ // Switch away from date/time type.
+ NotifyUAWidgetTeardown();
+ } else {
+ // Switch between date and time.
+ NotifyUAWidgetSetupOrChange();
+ }
+ } else if (CreatesDateTimeWidget()) {
+ // Switch to date/time type.
+ AttachAndSetUAShadowRoot(NotifyUAWidgetSetup::Yes, DelegatesFocus::Yes);
+ }
+ // If we're becoming a text control and have focus, make sure to show focus
+ // rings.
+ if (State().HasState(ElementState::FOCUS) && IsSingleLineTextControl() &&
+ !IsSingleLineTextControl(/* aExcludePassword = */ false, oldType)) {
+ AddStates(ElementState::FOCUSRING);
+ }
+ }
+}
+
+void HTMLInputElement::MaybeSnapToTickMark(Decimal& aValue) {
+ nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame());
+ if (!rangeFrame) {
+ return;
+ }
+ auto tickMark = rangeFrame->NearestTickMark(aValue);
+ if (tickMark.isNaN()) {
+ return;
+ }
+ auto rangeFrameSize = CSSPixel::FromAppUnits(rangeFrame->GetSize());
+ CSSCoord rangeTrackLength;
+ if (rangeFrame->IsHorizontal()) {
+ rangeTrackLength = rangeFrameSize.width;
+ } else {
+ rangeTrackLength = rangeFrameSize.height;
+ }
+ auto stepBase = GetStepBase();
+ auto distanceToTickMark =
+ rangeTrackLength * float(rangeFrame->GetDoubleAsFractionOfRange(
+ stepBase + (tickMark - aValue).abs()));
+ const CSSCoord magnetEffectRange(
+ StaticPrefs::dom_range_element_magnet_effect_threshold());
+ if (distanceToTickMark <= magnetEffectRange) {
+ aValue = tickMark;
+ }
+}
+
+void HTMLInputElement::SanitizeValue(nsAString& aValue,
+ SanitizationKind aKind) const {
+ NS_ASSERTION(mDoneCreating, "The element creation should be finished!");
+
+ switch (mType) {
+ case FormControlType::InputText:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputPassword: {
+ aValue.StripCRLF();
+ } break;
+ case FormControlType::InputEmail: {
+ aValue.StripCRLF();
+ aValue = nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(
+ aValue);
+
+ if (Multiple() && !aValue.IsEmpty()) {
+ nsAutoString oldValue(aValue);
+ HTMLSplitOnSpacesTokenizer tokenizer(oldValue, ',');
+ aValue.Truncate(0);
+ aValue.Append(tokenizer.nextToken());
+ while (tokenizer.hasMoreTokens() ||
+ tokenizer.separatorAfterCurrentToken()) {
+ aValue.Append(',');
+ aValue.Append(tokenizer.nextToken());
+ }
+ }
+ } break;
+ case FormControlType::InputUrl: {
+ aValue.StripCRLF();
+
+ aValue = nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(
+ aValue);
+ } break;
+ case FormControlType::InputNumber: {
+ if (aKind == SanitizationKind::ForValueSetter && !aValue.IsEmpty() &&
+ (aValue.First() == '+' || aValue.Last() == '.')) {
+ // A value with a leading plus or trailing dot should fail to parse.
+ // However, the localized parser accepts this, and when we convert it
+ // back to a Decimal, it disappears. So, we need to check first.
+ //
+ // FIXME(emilio): Should we just use the unlocalized parser
+ // (StringToDecimal) for the value setter? Other browsers don't seem to
+ // allow setting localized strings there, and that way we don't need
+ // this special-case.
+ aValue.Truncate();
+ return;
+ }
+
+ InputType::StringToNumberResult result =
+ mInputType->ConvertStringToNumber(aValue);
+ if (!result.mResult.isFinite()) {
+ aValue.Truncate();
+ return;
+ }
+ switch (aKind) {
+ case SanitizationKind::ForValueGetter: {
+ // If the default non-localized algorithm parses the value, then we're
+ // done, don't un-localize it, to avoid precision loss, and to
+ // preserve scientific notation as well for example.
+ if (!result.mLocalized) {
+ return;
+ }
+ // For the <input type=number> value getter, we return the unlocalized
+ // value if it doesn't parse as StringToDecimal, for compat with other
+ // browsers.
+ char buf[32];
+ DebugOnly<bool> ok = result.mResult.toString(buf, ArrayLength(buf));
+ aValue.AssignASCII(buf);
+ MOZ_ASSERT(ok, "buf not big enough");
+ break;
+ }
+ case SanitizationKind::ForDisplay:
+ case SanitizationKind::ForValueSetter: {
+ // We localize as needed, but if both the localized and unlocalized
+ // version parse with the generic parser, we just use the unlocalized
+ // one, to preserve the input as much as possible.
+ //
+ // FIXME(emilio, bug 1622808): Localization should ideally be more
+ // input-preserving.
+ nsString localizedValue;
+ mInputType->ConvertNumberToString(result.mResult, localizedValue);
+ if (!StringToDecimal(localizedValue).isFinite()) {
+ aValue = std::move(localizedValue);
+ }
+ break;
+ }
+ }
+ break;
+ }
+ case FormControlType::InputRange: {
+ Decimal minimum = GetMinimum();
+ Decimal maximum = GetMaximum();
+ MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(),
+ "type=range should have a default maximum/minimum");
+
+ // We use this to avoid modifying the string unnecessarily, since that
+ // may introduce rounding. This is set to true only if the value we
+ // parse out from aValue needs to be sanitized.
+ bool needSanitization = false;
+
+ Decimal value = mInputType->ConvertStringToNumber(aValue).mResult;
+ if (!value.isFinite()) {
+ needSanitization = true;
+ // Set value to midway between minimum and maximum.
+ value = maximum <= minimum ? minimum
+ : minimum + (maximum - minimum) / Decimal(2);
+ } else if (value < minimum || maximum < minimum) {
+ needSanitization = true;
+ value = minimum;
+ } else if (value > maximum) {
+ needSanitization = true;
+ value = maximum;
+ }
+
+ Decimal step = GetStep();
+ if (step != kStepAny) {
+ Decimal stepBase = GetStepBase();
+ // There could be rounding issues below when dealing with fractional
+ // numbers, but let's ignore that until ECMAScript supplies us with a
+ // decimal number type.
+ Decimal deltaToStep = NS_floorModulo(value - stepBase, step);
+ if (deltaToStep != Decimal(0)) {
+ // "suffering from a step mismatch"
+ // Round the element's value to the nearest number for which the
+ // element would not suffer from a step mismatch, and which is
+ // greater than or equal to the minimum, and, if the maximum is not
+ // less than the minimum, which is less than or equal to the
+ // maximum, if there is a number that matches these constraints:
+ MOZ_ASSERT(deltaToStep > Decimal(0),
+ "stepBelow/stepAbove will be wrong");
+ Decimal stepBelow = value - deltaToStep;
+ Decimal stepAbove = value - deltaToStep + step;
+ Decimal halfStep = step / Decimal(2);
+ bool stepAboveIsClosest = (stepAbove - value) <= halfStep;
+ bool stepAboveInRange = stepAbove >= minimum && stepAbove <= maximum;
+ bool stepBelowInRange = stepBelow >= minimum && stepBelow <= maximum;
+
+ if ((stepAboveIsClosest || !stepBelowInRange) && stepAboveInRange) {
+ needSanitization = true;
+ value = stepAbove;
+ } else if ((!stepAboveIsClosest || !stepAboveInRange) &&
+ stepBelowInRange) {
+ needSanitization = true;
+ value = stepBelow;
+ }
+ }
+ }
+
+ if (needSanitization) {
+ char buf[32];
+ DebugOnly<bool> ok = value.toString(buf, ArrayLength(buf));
+ aValue.AssignASCII(buf);
+ MOZ_ASSERT(ok, "buf not big enough");
+ }
+ } break;
+ case FormControlType::InputDate: {
+ if (!aValue.IsEmpty() && !IsValidDate(aValue)) {
+ aValue.Truncate();
+ }
+ } break;
+ case FormControlType::InputTime: {
+ if (!aValue.IsEmpty() && !IsValidTime(aValue)) {
+ aValue.Truncate();
+ }
+ } break;
+ case FormControlType::InputMonth: {
+ if (!aValue.IsEmpty() && !IsValidMonth(aValue)) {
+ aValue.Truncate();
+ }
+ } break;
+ case FormControlType::InputWeek: {
+ if (!aValue.IsEmpty() && !IsValidWeek(aValue)) {
+ aValue.Truncate();
+ }
+ } break;
+ case FormControlType::InputDatetimeLocal: {
+ if (!aValue.IsEmpty() && !IsValidDateTimeLocal(aValue)) {
+ aValue.Truncate();
+ } else {
+ NormalizeDateTimeLocal(aValue);
+ }
+ } break;
+ case FormControlType::InputColor: {
+ if (IsValidSimpleColor(aValue)) {
+ ToLowerCase(aValue);
+ } else {
+ // Set default (black) color, if aValue wasn't parsed correctly.
+ aValue.AssignLiteral("#000000");
+ }
+ } break;
+ default:
+ break;
+ }
+}
+
+Maybe<nscolor> HTMLInputElement::ParseSimpleColor(const nsAString& aColor) {
+ // Input color string should be 7 length (i.e. a string representing a valid
+ // simple color)
+ if (aColor.Length() != 7 || aColor.First() != '#') {
+ return {};
+ }
+
+ const nsAString& withoutHash = StringTail(aColor, 6);
+ nscolor color;
+ if (!NS_HexToRGBA(withoutHash, nsHexColorType::NoAlpha, &color)) {
+ return {};
+ }
+
+ return Some(color);
+}
+
+bool HTMLInputElement::IsValidSimpleColor(const nsAString& aValue) const {
+ if (aValue.Length() != 7 || aValue.First() != '#') {
+ return false;
+ }
+
+ for (int i = 1; i < 7; ++i) {
+ if (!IsAsciiDigit(aValue[i]) && !(aValue[i] >= 'a' && aValue[i] <= 'f') &&
+ !(aValue[i] >= 'A' && aValue[i] <= 'F')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool HTMLInputElement::IsLeapYear(uint32_t aYear) const {
+ if ((aYear % 4 == 0 && aYear % 100 != 0) || (aYear % 400 == 0)) {
+ return true;
+ }
+ return false;
+}
+
+uint32_t HTMLInputElement::DayOfWeek(uint32_t aYear, uint32_t aMonth,
+ uint32_t aDay, bool isoWeek) const {
+ MOZ_ASSERT(1 <= aMonth && aMonth <= 12, "month is in 1..12");
+ MOZ_ASSERT(1 <= aDay && aDay <= 31, "day is in 1..31");
+
+ // Tomohiko Sakamoto algorithm.
+ int monthTable[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
+ aYear -= aMonth < 3;
+
+ uint32_t day = (aYear + aYear / 4 - aYear / 100 + aYear / 400 +
+ monthTable[aMonth - 1] + aDay) %
+ 7;
+
+ if (isoWeek) {
+ return ((day + 6) % 7) + 1;
+ }
+
+ return day;
+}
+
+uint32_t HTMLInputElement::MaximumWeekInYear(uint32_t aYear) const {
+ int day = DayOfWeek(aYear, 1, 1, true); // January 1.
+ // A year starting on Thursday or a leap year starting on Wednesday has 53
+ // weeks. All other years have 52 weeks.
+ return day == 4 || (day == 3 && IsLeapYear(aYear)) ? kMaximumWeekInYear
+ : kMaximumWeekInYear - 1;
+}
+
+bool HTMLInputElement::IsValidWeek(const nsAString& aValue) const {
+ uint32_t year, week;
+ return ParseWeek(aValue, &year, &week);
+}
+
+bool HTMLInputElement::IsValidMonth(const nsAString& aValue) const {
+ uint32_t year, month;
+ return ParseMonth(aValue, &year, &month);
+}
+
+bool HTMLInputElement::IsValidDate(const nsAString& aValue) const {
+ uint32_t year, month, day;
+ return ParseDate(aValue, &year, &month, &day);
+}
+
+bool HTMLInputElement::IsValidDateTimeLocal(const nsAString& aValue) const {
+ uint32_t year, month, day, time;
+ return ParseDateTimeLocal(aValue, &year, &month, &day, &time);
+}
+
+bool HTMLInputElement::ParseYear(const nsAString& aValue,
+ uint32_t* aYear) const {
+ if (aValue.Length() < 4) {
+ return false;
+ }
+
+ return DigitSubStringToNumber(aValue, 0, aValue.Length(), aYear) &&
+ *aYear > 0;
+}
+
+bool HTMLInputElement::ParseMonth(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth) const {
+ // Parse the year, month values out a string formatted as 'yyyy-mm'.
+ if (aValue.Length() < 7) {
+ return false;
+ }
+
+ uint32_t endOfYearOffset = aValue.Length() - 3;
+ if (aValue[endOfYearOffset] != '-') {
+ return false;
+ }
+
+ const nsAString& yearStr = Substring(aValue, 0, endOfYearOffset);
+ if (!ParseYear(yearStr, aYear)) {
+ return false;
+ }
+
+ return DigitSubStringToNumber(aValue, endOfYearOffset + 1, 2, aMonth) &&
+ *aMonth > 0 && *aMonth <= 12;
+}
+
+bool HTMLInputElement::ParseWeek(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aWeek) const {
+ // Parse the year, month values out a string formatted as 'yyyy-Www'.
+ if (aValue.Length() < 8) {
+ return false;
+ }
+
+ uint32_t endOfYearOffset = aValue.Length() - 4;
+ if (aValue[endOfYearOffset] != '-') {
+ return false;
+ }
+
+ if (aValue[endOfYearOffset + 1] != 'W') {
+ return false;
+ }
+
+ const nsAString& yearStr = Substring(aValue, 0, endOfYearOffset);
+ if (!ParseYear(yearStr, aYear)) {
+ return false;
+ }
+
+ return DigitSubStringToNumber(aValue, endOfYearOffset + 2, 2, aWeek) &&
+ *aWeek > 0 && *aWeek <= MaximumWeekInYear(*aYear);
+}
+
+bool HTMLInputElement::ParseDate(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth, uint32_t* aDay) const {
+ /*
+ * Parse the year, month, day values out a date string formatted as
+ * yyyy-mm-dd. -The year must be 4 or more digits long, and year > 0 -The
+ * month must be exactly 2 digits long, and 01 <= month <= 12 -The day must be
+ * exactly 2 digit long, and 01 <= day <= maxday Where maxday is the number of
+ * days in the month 'month' and year 'year'
+ */
+ if (aValue.Length() < 10) {
+ return false;
+ }
+
+ uint32_t endOfMonthOffset = aValue.Length() - 3;
+ if (aValue[endOfMonthOffset] != '-') {
+ return false;
+ }
+
+ const nsAString& yearMonthStr = Substring(aValue, 0, endOfMonthOffset);
+ if (!ParseMonth(yearMonthStr, aYear, aMonth)) {
+ return false;
+ }
+
+ return DigitSubStringToNumber(aValue, endOfMonthOffset + 1, 2, aDay) &&
+ *aDay > 0 && *aDay <= NumberOfDaysInMonth(*aMonth, *aYear);
+}
+
+bool HTMLInputElement::ParseDateTimeLocal(const nsAString& aValue,
+ uint32_t* aYear, uint32_t* aMonth,
+ uint32_t* aDay,
+ uint32_t* aTime) const {
+ // Parse the year, month, day and time values out a string formatted as
+ // 'yyyy-mm-ddThh:mm[:ss.s] or 'yyyy-mm-dd hh:mm[:ss.s]', where fractions of
+ // seconds can be 1 to 3 digits.
+ // The minimum length allowed is 16, which is of the form 'yyyy-mm-ddThh:mm'
+ // or 'yyyy-mm-dd hh:mm'.
+ if (aValue.Length() < 16) {
+ return false;
+ }
+
+ int32_t sepIndex = aValue.FindChar('T');
+ if (sepIndex == -1) {
+ sepIndex = aValue.FindChar(' ');
+
+ if (sepIndex == -1) {
+ return false;
+ }
+ }
+
+ const nsAString& dateStr = Substring(aValue, 0, sepIndex);
+ if (!ParseDate(dateStr, aYear, aMonth, aDay)) {
+ return false;
+ }
+
+ const nsAString& timeStr =
+ Substring(aValue, sepIndex + 1, aValue.Length() - sepIndex + 1);
+ if (!ParseTime(timeStr, aTime)) {
+ return false;
+ }
+
+ return true;
+}
+
+void HTMLInputElement::NormalizeDateTimeLocal(nsAString& aValue) const {
+ if (aValue.IsEmpty()) {
+ return;
+ }
+
+ // Use 'T' as the separator between date string and time string.
+ int32_t sepIndex = aValue.FindChar(' ');
+ if (sepIndex != -1) {
+ aValue.ReplaceLiteral(sepIndex, 1, u"T");
+ } else {
+ sepIndex = aValue.FindChar('T');
+ }
+
+ // Time expressed as the shortest possible string, which is hh:mm.
+ if ((aValue.Length() - sepIndex) == 6) {
+ return;
+ }
+
+ // Fractions of seconds part is optional, ommit it if it's 0.
+ if ((aValue.Length() - sepIndex) > 9) {
+ const uint32_t millisecSepIndex = sepIndex + 9;
+ uint32_t milliseconds;
+ if (!DigitSubStringToNumber(aValue, millisecSepIndex + 1,
+ aValue.Length() - (millisecSepIndex + 1),
+ &milliseconds)) {
+ return;
+ }
+
+ if (milliseconds != 0) {
+ return;
+ }
+
+ aValue.Cut(millisecSepIndex, aValue.Length() - millisecSepIndex);
+ }
+
+ // Seconds part is optional, ommit it if it's 0.
+ const uint32_t secondSepIndex = sepIndex + 6;
+ uint32_t seconds;
+ if (!DigitSubStringToNumber(aValue, secondSepIndex + 1,
+ aValue.Length() - (secondSepIndex + 1),
+ &seconds)) {
+ return;
+ }
+
+ if (seconds != 0) {
+ return;
+ }
+
+ aValue.Cut(secondSepIndex, aValue.Length() - secondSepIndex);
+}
+
+double HTMLInputElement::DaysSinceEpochFromWeek(uint32_t aYear,
+ uint32_t aWeek) const {
+ double days = JS::DayFromYear(aYear) + (aWeek - 1) * 7;
+ uint32_t dayOneIsoWeekday = DayOfWeek(aYear, 1, 1, true);
+
+ // If day one of that year is on/before Thursday, we should subtract the
+ // days that belong to last year in our first week, otherwise, our first
+ // days belong to last year's last week, and we should add those days
+ // back.
+ if (dayOneIsoWeekday <= 4) {
+ days -= (dayOneIsoWeekday - 1);
+ } else {
+ days += (7 - dayOneIsoWeekday + 1);
+ }
+
+ return days;
+}
+
+uint32_t HTMLInputElement::NumberOfDaysInMonth(uint32_t aMonth,
+ uint32_t aYear) const {
+ /*
+ * Returns the number of days in a month.
+ * Months that are |longMonths| always have 31 days.
+ * Months that are not |longMonths| have 30 days except February (month 2).
+ * February has 29 days during leap years which are years that are divisible
+ * by 400. or divisible by 100 and 4. February has 28 days otherwise.
+ */
+
+ static const bool longMonths[] = {true, false, true, false, true, false,
+ true, true, false, true, false, true};
+ MOZ_ASSERT(aMonth <= 12 && aMonth > 0);
+
+ if (longMonths[aMonth - 1]) {
+ return 31;
+ }
+
+ if (aMonth != 2) {
+ return 30;
+ }
+
+ return IsLeapYear(aYear) ? 29 : 28;
+}
+
+/* static */
+bool HTMLInputElement::DigitSubStringToNumber(const nsAString& aStr,
+ uint32_t aStart, uint32_t aLen,
+ uint32_t* aRetVal) {
+ MOZ_ASSERT(aStr.Length() > (aStart + aLen - 1));
+
+ for (uint32_t offset = 0; offset < aLen; ++offset) {
+ if (!IsAsciiDigit(aStr[aStart + offset])) {
+ return false;
+ }
+ }
+
+ nsresult ec;
+ *aRetVal = static_cast<uint32_t>(
+ PromiseFlatString(Substring(aStr, aStart, aLen)).ToInteger(&ec));
+
+ return NS_SUCCEEDED(ec);
+}
+
+bool HTMLInputElement::IsValidTime(const nsAString& aValue) const {
+ return ParseTime(aValue, nullptr);
+}
+
+/* static */
+bool HTMLInputElement::ParseTime(const nsAString& aValue, uint32_t* aResult) {
+ /* The string must have the following parts:
+ * - HOURS: two digits, value being in [0, 23];
+ * - Colon (:);
+ * - MINUTES: two digits, value being in [0, 59];
+ * - Optional:
+ * - Colon (:);
+ * - SECONDS: two digits, value being in [0, 59];
+ * - Optional:
+ * - DOT (.);
+ * - FRACTIONAL SECONDS: one to three digits, no value range.
+ */
+
+ // The following format is the shorter one allowed: "HH:MM".
+ if (aValue.Length() < 5) {
+ return false;
+ }
+
+ uint32_t hours;
+ if (!DigitSubStringToNumber(aValue, 0, 2, &hours) || hours > 23) {
+ return false;
+ }
+
+ // Hours/minutes separator.
+ if (aValue[2] != ':') {
+ return false;
+ }
+
+ uint32_t minutes;
+ if (!DigitSubStringToNumber(aValue, 3, 2, &minutes) || minutes > 59) {
+ return false;
+ }
+
+ if (aValue.Length() == 5) {
+ if (aResult) {
+ *aResult = ((hours * 60) + minutes) * 60000;
+ }
+ return true;
+ }
+
+ // The following format is the next shorter one: "HH:MM:SS".
+ if (aValue.Length() < 8 || aValue[5] != ':') {
+ return false;
+ }
+
+ uint32_t seconds;
+ if (!DigitSubStringToNumber(aValue, 6, 2, &seconds) || seconds > 59) {
+ return false;
+ }
+
+ if (aValue.Length() == 8) {
+ if (aResult) {
+ *aResult = (((hours * 60) + minutes) * 60 + seconds) * 1000;
+ }
+ return true;
+ }
+
+ // The string must follow this format now: "HH:MM:SS.{s,ss,sss}".
+ // There can be 1 to 3 digits for the fractions of seconds.
+ if (aValue.Length() == 9 || aValue.Length() > 12 || aValue[8] != '.') {
+ return false;
+ }
+
+ uint32_t fractionsSeconds;
+ if (!DigitSubStringToNumber(aValue, 9, aValue.Length() - 9,
+ &fractionsSeconds)) {
+ return false;
+ }
+
+ if (aResult) {
+ *aResult = (((hours * 60) + minutes) * 60 + seconds) * 1000 +
+ // NOTE: there is 10.0 instead of 10 and static_cast<int> because
+ // some old [and stupid] compilers can't just do the right thing.
+ fractionsSeconds *
+ pow(10.0, static_cast<int>(3 - (aValue.Length() - 9)));
+ }
+
+ return true;
+}
+
+/* static */
+bool HTMLInputElement::IsDateTimeTypeSupported(
+ FormControlType aDateTimeInputType) {
+ switch (aDateTimeInputType) {
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ return StaticPrefs::dom_forms_datetime_others();
+ default:
+ return false;
+ }
+}
+
+void HTMLInputElement::GetLastInteractiveValue(nsAString& aValue) {
+ if (mLastValueChangeWasInteractive) {
+ return GetValue(aValue, CallerType::System);
+ }
+ if (TextControlState* state = GetEditorState()) {
+ return aValue.Assign(
+ state->LastInteractiveValueIfLastChangeWasNonInteractive());
+ }
+ aValue.Truncate();
+}
+
+bool HTMLInputElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ // We can't make these static_asserts because kInputDefaultType and
+ // kInputTypeTable aren't constexpr.
+ MOZ_ASSERT(
+ FormControlType(kInputDefaultType->value) == FormControlType::InputText,
+ "Someone forgot to update kInputDefaultType when adding a new "
+ "input type.");
+ MOZ_ASSERT(kInputTypeTable[ArrayLength(kInputTypeTable) - 1].tag == nullptr,
+ "Last entry in the table must be the nullptr guard");
+ MOZ_ASSERT(FormControlType(
+ kInputTypeTable[ArrayLength(kInputTypeTable) - 2].value) ==
+ FormControlType::InputText,
+ "Next to last entry in the table must be the \"text\" entry");
+
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::type) {
+ aResult.ParseEnumValue(aValue, kInputTypeTable, false, kInputDefaultType);
+ auto newType = FormControlType(aResult.GetEnumValue());
+ if (IsDateTimeInputType(newType) && !IsDateTimeTypeSupported(newType)) {
+ // There's no public way to set an nsAttrValue to an enum value, but we
+ // can just re-parse with a table that doesn't have any types other than
+ // "text" in it.
+ aResult.ParseEnumValue(aValue, kInputDefaultType, false,
+ kInputDefaultType);
+ }
+
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::width) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::height) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::maxlength) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::minlength) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::size) {
+ return aResult.ParsePositiveIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::formmethod) {
+ return aResult.ParseEnumValue(aValue, kFormMethodTable, false);
+ }
+ if (aAttribute == nsGkAtoms::formenctype) {
+ return aResult.ParseEnumValue(aValue, kFormEnctypeTable, false);
+ }
+ if (aAttribute == nsGkAtoms::autocomplete) {
+ aResult.ParseAtomArray(aValue);
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::capture) {
+ return aResult.ParseEnumValue(aValue, kCaptureTable, false,
+ kCaptureDefault);
+ }
+ if (ParseImageAttribute(aAttribute, aValue, aResult)) {
+ // We have to call |ParseImageAttribute| unconditionally since we
+ // don't know if we're going to have a type="image" attribute yet,
+ // (or could have it set dynamically in the future). See bug
+ // 214077.
+ return true;
+ }
+ }
+
+ return TextControlElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLInputElement::ImageInputMapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ nsGenericHTMLFormControlElementWithState::MapImageBorderAttributeInto(
+ aBuilder);
+ nsGenericHTMLFormControlElementWithState::MapImageMarginAttributeInto(
+ aBuilder);
+ nsGenericHTMLFormControlElementWithState::MapImageSizeAttributesInto(
+ aBuilder, MapAspectRatio::Yes);
+ // Images treat align as "float"
+ nsGenericHTMLFormControlElementWithState::MapImageAlignAttributeInto(
+ aBuilder);
+ nsGenericHTMLFormControlElementWithState::MapCommonAttributesInto(aBuilder);
+}
+
+nsChangeHint HTMLInputElement::GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const {
+ nsChangeHint retval =
+ nsGenericHTMLFormControlElementWithState::GetAttributeChangeHint(
+ aAttribute, aModType);
+
+ const bool isAdditionOrRemoval =
+ aModType == MutationEvent_Binding::ADDITION ||
+ aModType == MutationEvent_Binding::REMOVAL;
+
+ const bool reconstruct = [&] {
+ if (aAttribute == nsGkAtoms::type) {
+ return true;
+ }
+
+ if (PlaceholderApplies() && aAttribute == nsGkAtoms::placeholder &&
+ isAdditionOrRemoval) {
+ // We need to re-create our placeholder text.
+ return true;
+ }
+
+ if (mType == FormControlType::InputFile &&
+ aAttribute == nsGkAtoms::webkitdirectory) {
+ // The presence or absence of the 'directory' attribute determines what
+ // value we show in the file label when empty, via GetDisplayFileName.
+ return true;
+ }
+
+ if (mType == FormControlType::InputImage && isAdditionOrRemoval &&
+ (aAttribute == nsGkAtoms::alt || aAttribute == nsGkAtoms::value)) {
+ // We might need to rebuild our alt text. Just go ahead and
+ // reconstruct our frame. This should be quite rare..
+ return true;
+ }
+ return false;
+ }();
+
+ if (reconstruct) {
+ retval |= nsChangeHint_ReconstructFrame;
+ } else if (aAttribute == nsGkAtoms::value) {
+ retval |= NS_STYLE_HINT_REFLOW;
+ } else if (aAttribute == nsGkAtoms::size && IsSingleLineTextControl(false)) {
+ retval |= NS_STYLE_HINT_REFLOW;
+ }
+
+ return retval;
+}
+
+NS_IMETHODIMP_(bool)
+HTMLInputElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::align},
+ {nullptr},
+ };
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ sImageMarginSizeAttributeMap,
+ sImageBorderAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLInputElement::GetAttributeMappingFunction()
+ const {
+ // GetAttributeChangeHint guarantees that changes to mType will trigger a
+ // reframe, and we update the mapping function in our mapped attrs when our
+ // type changes, so it's safe to condition our attribute mapping function on
+ // mType.
+ if (mType == FormControlType::InputImage) {
+ return &ImageInputMapAttributesIntoRule;
+ }
+
+ return &MapCommonAttributesInto;
+}
+
+// Directory picking methods:
+
+already_AddRefed<Promise> HTMLInputElement::GetFilesAndDirectories(
+ ErrorResult& aRv) {
+ if (mType != FormControlType::InputFile) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+ MOZ_ASSERT(global);
+ if (!global) {
+ return nullptr;
+ }
+
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ const nsTArray<OwningFileOrDirectory>& filesAndDirs =
+ GetFilesOrDirectoriesInternal();
+
+ Sequence<OwningFileOrDirectory> filesAndDirsSeq;
+
+ if (!filesAndDirsSeq.SetLength(filesAndDirs.Length(), fallible)) {
+ p->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+ return p.forget();
+ }
+
+ for (uint32_t i = 0; i < filesAndDirs.Length(); ++i) {
+ if (filesAndDirs[i].IsDirectory()) {
+ RefPtr<Directory> directory = filesAndDirs[i].GetAsDirectory();
+
+ // In future we could refactor SetFilePickerFiltersFromAccept to return a
+ // semicolon separated list of file extensions and include that in the
+ // filter string passed here.
+ directory->SetContentFilters(u"filter-out-sensitive"_ns);
+ filesAndDirsSeq[i].SetAsDirectory() = directory;
+ } else {
+ MOZ_ASSERT(filesAndDirs[i].IsFile());
+
+ // This file was directly selected by the user, so don't filter it.
+ filesAndDirsSeq[i].SetAsFile() = filesAndDirs[i].GetAsFile();
+ }
+ }
+
+ p->MaybeResolve(filesAndDirsSeq);
+ return p.forget();
+}
+
+// Controllers Methods
+
+nsIControllers* HTMLInputElement::GetControllers(ErrorResult& aRv) {
+ // XXX: what about type "file"?
+ if (IsSingleLineTextControl(false)) {
+ if (!mControllers) {
+ mControllers = new nsXULControllers();
+ if (!mControllers) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<nsBaseCommandController> commandController =
+ nsBaseCommandController::CreateEditorController();
+ if (!commandController) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ mControllers->AppendController(commandController);
+
+ commandController = nsBaseCommandController::CreateEditingController();
+ if (!commandController) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ mControllers->AppendController(commandController);
+ }
+ }
+
+ return mControllers;
+}
+
+nsresult HTMLInputElement::GetControllers(nsIControllers** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ ErrorResult rv;
+ RefPtr<nsIControllers> controller = GetControllers(rv);
+ controller.forget(aResult);
+ return rv.StealNSResult();
+}
+
+int32_t HTMLInputElement::InputTextLength(CallerType aCallerType) {
+ nsAutoString val;
+ GetValue(val, aCallerType);
+ return val.Length();
+}
+
+void HTMLInputElement::SetSelectionRange(uint32_t aSelectionStart,
+ uint32_t aSelectionEnd,
+ const Optional<nsAString>& aDirection,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetSelectionRange(aSelectionStart, aSelectionEnd, aDirection, aRv);
+}
+
+void HTMLInputElement::SetRangeText(const nsAString& aReplacement,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetRangeText(aReplacement, aRv);
+}
+
+void HTMLInputElement::SetRangeText(const nsAString& aReplacement,
+ uint32_t aStart, uint32_t aEnd,
+ SelectionMode aSelectMode,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetRangeText(aReplacement, aStart, aEnd, aSelectMode, aRv);
+}
+
+void HTMLInputElement::GetValueFromSetRangeText(nsAString& aValue) {
+ GetNonFileValueInternal(aValue);
+}
+
+nsresult HTMLInputElement::SetValueFromSetRangeText(const nsAString& aValue) {
+ return SetValueInternal(aValue, {ValueSetterOption::ByContentAPI,
+ ValueSetterOption::BySetRangeTextAPI,
+ ValueSetterOption::SetValueChanged});
+}
+
+Nullable<uint32_t> HTMLInputElement::GetSelectionStart(ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ return Nullable<uint32_t>();
+ }
+
+ uint32_t selStart = GetSelectionStartIgnoringType(aRv);
+ if (aRv.Failed()) {
+ return Nullable<uint32_t>();
+ }
+
+ return Nullable<uint32_t>(selStart);
+}
+
+uint32_t HTMLInputElement::GetSelectionStartIgnoringType(ErrorResult& aRv) {
+ uint32_t selEnd, selStart;
+ GetSelectionRange(&selStart, &selEnd, aRv);
+ return selStart;
+}
+
+void HTMLInputElement::SetSelectionStart(
+ const Nullable<uint32_t>& aSelectionStart, ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetSelectionStart(aSelectionStart, aRv);
+}
+
+Nullable<uint32_t> HTMLInputElement::GetSelectionEnd(ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ return Nullable<uint32_t>();
+ }
+
+ uint32_t selEnd = GetSelectionEndIgnoringType(aRv);
+ if (aRv.Failed()) {
+ return Nullable<uint32_t>();
+ }
+
+ return Nullable<uint32_t>(selEnd);
+}
+
+uint32_t HTMLInputElement::GetSelectionEndIgnoringType(ErrorResult& aRv) {
+ uint32_t selEnd, selStart;
+ GetSelectionRange(&selStart, &selEnd, aRv);
+ return selEnd;
+}
+
+void HTMLInputElement::SetSelectionEnd(const Nullable<uint32_t>& aSelectionEnd,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection() returned true!");
+ state->SetSelectionEnd(aSelectionEnd, aRv);
+}
+
+void HTMLInputElement::GetSelectionRange(uint32_t* aSelectionStart,
+ uint32_t* aSelectionEnd,
+ ErrorResult& aRv) {
+ TextControlState* state = GetEditorState();
+ if (!state) {
+ // Not a text control.
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ state->GetSelectionRange(aSelectionStart, aSelectionEnd, aRv);
+}
+
+void HTMLInputElement::GetSelectionDirection(nsAString& aDirection,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aDirection.SetIsVoid(true);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection came back true!");
+ state->GetSelectionDirectionString(aDirection, aRv);
+}
+
+void HTMLInputElement::SetSelectionDirection(const nsAString& aDirection,
+ ErrorResult& aRv) {
+ if (!SupportsTextSelection()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ TextControlState* state = GetEditorState();
+ MOZ_ASSERT(state, "SupportsTextSelection came back true!");
+ state->SetSelectionDirection(aDirection, aRv);
+}
+
+// https://html.spec.whatwg.org/multipage/input.html#dom-input-showpicker
+void HTMLInputElement::ShowPicker(ErrorResult& aRv) {
+ // Step 1. If this is not mutable, then throw an "InvalidStateError"
+ // DOMException.
+ if (!IsMutable()) {
+ return aRv.ThrowInvalidStateError(
+ "This input is either disabled or readonly.");
+ }
+
+ // Step 2. If this's relevant settings object's origin is not same origin with
+ // this's relevant settings object's top-level origin, and this's type
+ // attribute is not in the File Upload state or Color state, then throw a
+ // "SecurityError" DOMException.
+ if (mType != FormControlType::InputFile &&
+ mType != FormControlType::InputColor) {
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ WindowGlobalChild* windowGlobalChild =
+ window ? window->GetWindowGlobalChild() : nullptr;
+ if (!windowGlobalChild || !windowGlobalChild->SameOriginWithTop()) {
+ return aRv.ThrowSecurityError(
+ "Call was blocked because the current origin isn't same-origin with "
+ "top.");
+ }
+ }
+
+ // Step 3. If this's relevant global object does not have transient
+ // activation, then throw a "NotAllowedError" DOMException.
+ if (!OwnerDoc()->HasValidTransientUserGestureActivation()) {
+ return aRv.ThrowNotAllowedError(
+ "Call was blocked due to lack of user activation.");
+ }
+
+ // Step 4. Show the picker, if applicable, for this.
+ //
+ // https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable
+ // To show the picker, if applicable for an input element element:
+
+ // Step 1. Assert: element's relevant global object has transient activation.
+ // Step 2. If element is not mutable, then return.
+ // (See above.)
+
+ // Step 3. If element's type attribute is in the File Upload state, then run
+ // these steps in parallel:
+ if (mType == FormControlType::InputFile) {
+ FilePickerType type = FILE_PICKER_FILE;
+ if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
+ HasAttr(nsGkAtoms::webkitdirectory)) {
+ type = FILE_PICKER_DIRECTORY;
+ }
+ InitFilePicker(type);
+ return;
+ }
+
+ // Step 4. Otherwise, the user agent should show any relevant user interface
+ // for selecting a value for element, in the way it normally would when the
+ // user interacts with the control
+ if (mType == FormControlType::InputColor) {
+ InitColorPicker();
+ return;
+ }
+
+ if (!IsInComposedDoc()) {
+ return;
+ }
+
+ if (IsDateTimeTypeSupported(mType)) {
+ if (CreatesDateTimeWidget()) {
+ if (RefPtr<Element> dateTimeBoxElement = GetDateTimeBoxElement()) {
+ // Event is dispatched to closed-shadow tree and doesn't bubble.
+ RefPtr<Document> doc = dateTimeBoxElement->OwnerDoc();
+ nsContentUtils::DispatchTrustedEvent(doc, dateTimeBoxElement,
+ u"MozDateTimeShowPickerForJS"_ns,
+ CanBubble::eNo, Cancelable::eNo);
+ }
+ } else {
+ DateTimeValue value;
+ GetDateTimeInputBoxValue(value);
+ OpenDateTimePicker(value);
+ }
+ }
+}
+
+#ifdef ACCESSIBILITY
+/*static*/ nsresult FireEventForAccessibility(HTMLInputElement* aTarget,
+ EventMessage aEventMessage) {
+ Element* element = static_cast<Element*>(aTarget);
+ return nsContentUtils::DispatchTrustedEvent<WidgetEvent>(
+ element->OwnerDoc(), element, aEventMessage, CanBubble::eYes,
+ Cancelable::eYes);
+}
+#endif
+
+void HTMLInputElement::UpdateApzAwareFlag() {
+#if !defined(ANDROID) && !defined(XP_MACOSX)
+ if (mType == FormControlType::InputNumber ||
+ mType == FormControlType::InputRange) {
+ SetMayBeApzAware();
+ }
+#endif
+}
+
+nsresult HTMLInputElement::SetDefaultValueAsValue() {
+ NS_ASSERTION(GetValueMode() == VALUE_MODE_VALUE,
+ "GetValueMode() should return VALUE_MODE_VALUE!");
+
+ // The element has a content attribute value different from it's value when
+ // it's in the value mode value.
+ nsAutoString resetVal;
+ GetDefaultValue(resetVal);
+
+ // SetValueInternal is going to sanitize the value.
+ // TODO(mbrodesser): sanitizing will only happen if `mDoneCreating` is true.
+ return SetValueInternal(resetVal, ValueSetterOption::ByInternalAPI);
+}
+
+// https://html.spec.whatwg.org/#auto-directionality
+void HTMLInputElement::SetAutoDirectionality(bool aNotify,
+ const nsAString* aKnownValue) {
+ if (!IsAutoDirectionalityAssociated()) {
+ return SetDirectionality(GetParentDirectionality(this), aNotify);
+ }
+ nsAutoString value;
+ if (!aKnownValue) {
+ // It's unclear if per spec we should use the sanitized or unsanitized
+ // value to set the directionality, but aKnownValue is unsanitized, so be
+ // consistent. Using what the user is seeing to determine directionality
+ // instead of the sanitized (empty if invalid) value probably makes more
+ // sense.
+ GetValueInternal(value, CallerType::System);
+ aKnownValue = &value;
+ }
+ SetDirectionalityFromValue(this, *aKnownValue, aNotify);
+}
+
+NS_IMETHODIMP
+HTMLInputElement::Reset() {
+ // We should be able to reset all dirty flags regardless of the type.
+ SetCheckedChanged(false);
+ SetValueChanged(false);
+ SetLastValueChangeWasInteractive(false);
+ SetUserInteracted(false);
+
+ switch (GetValueMode()) {
+ case VALUE_MODE_VALUE: {
+ nsresult result = SetDefaultValueAsValue();
+ if (CreatesDateTimeWidget()) {
+ // mFocusedValue has to be set here, so that `FireChangeEventIfNeeded`
+ // can fire a change event if necessary.
+ GetValue(mFocusedValue, CallerType::System);
+ }
+ return result;
+ }
+ case VALUE_MODE_DEFAULT_ON:
+ DoSetChecked(DefaultChecked(), true, false);
+ return NS_OK;
+ case VALUE_MODE_FILENAME:
+ ClearFiles(false);
+ return NS_OK;
+ case VALUE_MODE_DEFAULT:
+ default:
+ return NS_OK;
+ }
+}
+
+NS_IMETHODIMP
+HTMLInputElement::SubmitNamesValues(FormData* aFormData) {
+ // For type=reset, and type=button, we just never submit, period.
+ // For type=image and type=button, we only submit if we were the button
+ // pressed
+ // For type=radio and type=checkbox, we only submit if checked=true
+ if (mType == FormControlType::InputReset ||
+ mType == FormControlType::InputButton ||
+ ((mType == FormControlType::InputSubmit ||
+ mType == FormControlType::InputImage) &&
+ aFormData->GetSubmitterElement() != this) ||
+ ((mType == FormControlType::InputRadio ||
+ mType == FormControlType::InputCheckbox) &&
+ !mChecked)) {
+ return NS_OK;
+ }
+
+ // Get the name
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ // Submit .x, .y for input type=image
+ if (mType == FormControlType::InputImage) {
+ // Get a property set by the frame to find out where it was clicked.
+ const auto* lastClickedPoint =
+ static_cast<CSSIntPoint*>(GetProperty(nsGkAtoms::imageClickedPoint));
+ int32_t x, y;
+ if (lastClickedPoint) {
+ // Convert the values to strings for submission
+ x = lastClickedPoint->x;
+ y = lastClickedPoint->y;
+ } else {
+ x = y = 0;
+ }
+
+ nsAutoString xVal, yVal;
+ xVal.AppendInt(x);
+ yVal.AppendInt(y);
+
+ if (!name.IsEmpty()) {
+ aFormData->AddNameValuePair(name + u".x"_ns, xVal);
+ aFormData->AddNameValuePair(name + u".y"_ns, yVal);
+ } else {
+ // If the Image Element has no name, simply return x and y
+ // to Nav and IE compatibility.
+ aFormData->AddNameValuePair(u"x"_ns, xVal);
+ aFormData->AddNameValuePair(u"y"_ns, yVal);
+ }
+
+ return NS_OK;
+ }
+
+ // If name not there, don't submit
+ if (name.IsEmpty()) {
+ return NS_OK;
+ }
+
+ //
+ // Submit file if its input type=file and this encoding method accepts files
+ //
+ if (mType == FormControlType::InputFile) {
+ // Submit files
+
+ const nsTArray<OwningFileOrDirectory>& files =
+ GetFilesOrDirectoriesInternal();
+
+ if (files.IsEmpty()) {
+ NS_ENSURE_STATE(GetOwnerGlobal());
+ ErrorResult rv;
+ RefPtr<Blob> blob = Blob::CreateStringBlob(
+ GetOwnerGlobal(), ""_ns, u"application/octet-stream"_ns);
+ RefPtr<File> file = blob->ToFile(u""_ns, rv);
+
+ if (!rv.Failed()) {
+ aFormData->AddNameBlobPair(name, file);
+ }
+
+ return rv.StealNSResult();
+ }
+
+ for (uint32_t i = 0; i < files.Length(); ++i) {
+ if (files[i].IsFile()) {
+ aFormData->AddNameBlobPair(name, files[i].GetAsFile());
+ } else {
+ MOZ_ASSERT(files[i].IsDirectory());
+ aFormData->AddNameDirectoryPair(name, files[i].GetAsDirectory());
+ }
+ }
+
+ return NS_OK;
+ }
+
+ if (mType == FormControlType::InputHidden &&
+ name.LowerCaseEqualsLiteral("_charset_")) {
+ nsCString charset;
+ aFormData->GetCharset(charset);
+ return aFormData->AddNameValuePair(name, NS_ConvertASCIItoUTF16(charset));
+ }
+
+ //
+ // Submit name=value
+ //
+
+ // Get the value
+ nsAutoString value;
+ GetValue(value, CallerType::System);
+
+ if (mType == FormControlType::InputSubmit && value.IsEmpty() &&
+ !HasAttr(nsGkAtoms::value)) {
+ // Get our default value, which is the same as our default label
+ nsAutoString defaultValue;
+ nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "Submit", OwnerDoc(), defaultValue);
+ value = defaultValue;
+ }
+
+ const nsresult rv = aFormData->AddNameValuePair(name, value);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Submit dirname=dir
+ if (IsAutoDirectionalityAssociated()) {
+ return SubmitDirnameDir(aFormData);
+ }
+
+ return NS_OK;
+}
+
+static nsTArray<FileContentData> SaveFileContentData(
+ const nsTArray<OwningFileOrDirectory>& aArray) {
+ nsTArray<FileContentData> res(aArray.Length());
+ for (const auto& it : aArray) {
+ if (it.IsFile()) {
+ RefPtr<BlobImpl> impl = it.GetAsFile()->Impl();
+ res.AppendElement(std::move(impl));
+ } else {
+ MOZ_ASSERT(it.IsDirectory());
+ nsString fullPath;
+ nsresult rv = it.GetAsDirectory()->GetFullRealPath(fullPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ res.AppendElement(std::move(fullPath));
+ }
+ }
+ return res;
+}
+
+void HTMLInputElement::SaveState() {
+ PresState* state = nullptr;
+ switch (GetValueMode()) {
+ case VALUE_MODE_DEFAULT_ON:
+ if (mCheckedChanged) {
+ state = GetPrimaryPresState();
+ if (!state) {
+ return;
+ }
+
+ state->contentData() = CheckedContentData(mChecked);
+ }
+ break;
+ case VALUE_MODE_FILENAME:
+ if (!mFileData->mFilesOrDirectories.IsEmpty()) {
+ state = GetPrimaryPresState();
+ if (!state) {
+ return;
+ }
+
+ state->contentData() =
+ SaveFileContentData(mFileData->mFilesOrDirectories);
+ }
+ break;
+ case VALUE_MODE_VALUE:
+ case VALUE_MODE_DEFAULT:
+ // VALUE_MODE_DEFAULT shouldn't have their value saved except 'hidden',
+ // mType should have never been FormControlType::InputPassword and value
+ // should have changed.
+ if ((GetValueMode() == VALUE_MODE_DEFAULT &&
+ mType != FormControlType::InputHidden) ||
+ mHasBeenTypePassword || !mValueChanged) {
+ break;
+ }
+
+ state = GetPrimaryPresState();
+ if (!state) {
+ return;
+ }
+
+ nsAutoString value;
+ GetValue(value, CallerType::System);
+
+ if (!IsSingleLineTextControl(false) &&
+ NS_FAILED(nsLinebreakConverter::ConvertStringLineBreaks(
+ value, nsLinebreakConverter::eLinebreakPlatform,
+ nsLinebreakConverter::eLinebreakContent))) {
+ NS_ERROR("Converting linebreaks failed!");
+ return;
+ }
+
+ state->contentData() =
+ TextContentData(value, mLastValueChangeWasInteractive);
+ break;
+ }
+
+ if (mDisabledChanged) {
+ if (!state) {
+ state = GetPrimaryPresState();
+ }
+ if (state) {
+ // We do not want to save the real disabled state but the disabled
+ // attribute.
+ state->disabled() = HasAttr(nsGkAtoms::disabled);
+ state->disabledSet() = true;
+ }
+ }
+}
+
+void HTMLInputElement::DoneCreatingElement() {
+ mDoneCreating = true;
+
+ //
+ // Restore state as needed. Note that disabled state applies to all control
+ // types.
+ //
+ bool restoredCheckedState = false;
+ if (!mInhibitRestoration) {
+ GenerateStateKey();
+ restoredCheckedState = RestoreFormControlState();
+ }
+
+ //
+ // If restore does not occur, we initialize .checked using the CHECKED
+ // property.
+ //
+ if (!restoredCheckedState && mShouldInitChecked) {
+ DoSetChecked(DefaultChecked(), false, false);
+ }
+
+ // Sanitize the value and potentially set mFocusedValue.
+ if (GetValueMode() == VALUE_MODE_VALUE) {
+ nsAutoString value;
+ GetValue(value, CallerType::System);
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // may potentially be big, but most likely we've failed to allocate
+ // before the type change.)
+ SetValueInternal(value, ValueSetterOption::ByInternalAPI);
+
+ if (CreatesDateTimeWidget()) {
+ // mFocusedValue has to be set here, so that `FireChangeEventIfNeeded` can
+ // fire a change event if necessary.
+ mFocusedValue = value;
+ }
+ }
+
+ mShouldInitChecked = false;
+}
+
+void HTMLInputElement::DestroyContent() {
+ nsImageLoadingContent::Destroy();
+ TextControlElement::DestroyContent();
+}
+
+void HTMLInputElement::UpdateValidityElementStates(bool aNotify) {
+ AutoStateChangeNotifier notifier(*this, aNotify);
+ RemoveStatesSilently(ElementState::VALIDITY_STATES);
+ if (!IsCandidateForConstraintValidation()) {
+ return;
+ }
+ ElementState state;
+ if (IsValid()) {
+ state |= ElementState::VALID;
+ if (mUserInteracted) {
+ state |= ElementState::USER_VALID;
+ }
+ } else {
+ state |= ElementState::INVALID;
+ if (mUserInteracted) {
+ state |= ElementState::USER_INVALID;
+ }
+ }
+ AddStatesSilently(state);
+}
+
+static nsTArray<OwningFileOrDirectory> RestoreFileContentData(
+ nsPIDOMWindowInner* aWindow, const nsTArray<FileContentData>& aData) {
+ nsTArray<OwningFileOrDirectory> res(aData.Length());
+ for (const auto& it : aData) {
+ if (it.type() == FileContentData::TBlobImpl) {
+ if (!it.get_BlobImpl()) {
+ // Serialization failed, skip this file.
+ continue;
+ }
+
+ RefPtr<File> file = File::Create(aWindow->AsGlobal(), it.get_BlobImpl());
+ if (NS_WARN_IF(!file)) {
+ continue;
+ }
+
+ OwningFileOrDirectory* element = res.AppendElement();
+ element->SetAsFile() = file;
+ } else {
+ MOZ_ASSERT(it.type() == FileContentData::TnsString);
+ nsCOMPtr<nsIFile> file;
+ nsresult rv =
+ NS_NewLocalFile(it.get_nsString(), true, getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ RefPtr<Directory> directory =
+ Directory::Create(aWindow->AsGlobal(), file);
+ MOZ_ASSERT(directory);
+
+ OwningFileOrDirectory* element = res.AppendElement();
+ element->SetAsDirectory() = directory;
+ }
+ }
+ return res;
+}
+
+bool HTMLInputElement::RestoreState(PresState* aState) {
+ bool restoredCheckedState = false;
+
+ const PresContentData& inputState = aState->contentData();
+
+ switch (GetValueMode()) {
+ case VALUE_MODE_DEFAULT_ON:
+ if (inputState.type() == PresContentData::TCheckedContentData) {
+ restoredCheckedState = true;
+ bool checked = inputState.get_CheckedContentData().checked();
+ DoSetChecked(checked, true, true);
+ }
+ break;
+ case VALUE_MODE_FILENAME:
+ if (inputState.type() == PresContentData::TArrayOfFileContentData) {
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ if (window) {
+ nsTArray<OwningFileOrDirectory> array =
+ RestoreFileContentData(window, inputState);
+ SetFilesOrDirectories(array, true);
+ }
+ }
+ break;
+ case VALUE_MODE_VALUE:
+ case VALUE_MODE_DEFAULT:
+ if (GetValueMode() == VALUE_MODE_DEFAULT &&
+ mType != FormControlType::InputHidden) {
+ break;
+ }
+
+ if (inputState.type() == PresContentData::TTextContentData) {
+ // TODO: What should we do if SetValueInternal fails? (The allocation
+ // may potentially be big, but most likely we've failed to allocate
+ // before the type change.)
+ SetValueInternal(inputState.get_TextContentData().value(),
+ ValueSetterOption::SetValueChanged);
+ if (inputState.get_TextContentData().lastValueChangeWasInteractive()) {
+ SetLastValueChangeWasInteractive(true);
+ }
+ }
+ break;
+ }
+
+ if (aState->disabledSet() && !aState->disabled()) {
+ SetDisabled(false, IgnoreErrors());
+ }
+
+ return restoredCheckedState;
+}
+
+/*
+ * Radio group stuff
+ */
+
+void HTMLInputElement::AddToRadioGroup() {
+ MOZ_ASSERT(!mRadioGroupContainer,
+ "Radio button must be removed from previous radio group container "
+ "before being added to another!");
+
+ // If the element has no radio group container we can stop here.
+ auto* container = FindTreeRadioGroupContainer();
+ if (!container) {
+ return;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ // If we are part of a radio group, the element must have a name.
+ MOZ_ASSERT(!name.IsEmpty());
+
+ //
+ // Add the radio to the radio group container.
+ //
+ container->AddToRadioGroup(name, this, mForm);
+ mRadioGroupContainer = container;
+
+ //
+ // If the input element is checked, and we add it to the group, it will
+ // deselect whatever is currently selected in that group
+ //
+ if (mChecked) {
+ //
+ // If it is checked, call "RadioSetChecked" to perform the selection/
+ // deselection ritual. This has the side effect of repainting the
+ // radio button, but as adding a checked radio button into the group
+ // should not be that common an occurrence, I think we can live with
+ // that.
+ // Make sure not to notify if we're still being created.
+ //
+ RadioSetChecked(mDoneCreating);
+ } else {
+ bool indeterminate = !container->GetCurrentRadioButton(name);
+ SetStates(ElementState::INDETERMINATE, indeterminate, mDoneCreating);
+ }
+
+ //
+ // For integrity purposes, we have to ensure that "checkedChanged" is
+ // the same for this new element as for all the others in the group
+ //
+ bool checkedChanged = mCheckedChanged;
+
+ nsCOMPtr<nsIRadioVisitor> visitor =
+ new nsRadioGetCheckedChangedVisitor(&checkedChanged, this);
+ VisitGroup(visitor);
+
+ SetCheckedChangedInternal(checkedChanged);
+
+ // We initialize the validity of the element to the validity of the group
+ // because we assume UpdateValueMissingState() will be called after.
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING,
+ container->GetValueMissingState(name));
+}
+
+void HTMLInputElement::RemoveFromRadioGroup() {
+ auto* container = GetCurrentRadioGroupContainer();
+ if (!container) {
+ return;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ // If this button was checked, we need to notify the group that there is no
+ // longer a selected radio button
+ if (mChecked) {
+ container->SetCurrentRadioButton(name, nullptr);
+ nsCOMPtr<nsIRadioVisitor> visitor = new nsRadioUpdateStateVisitor(this);
+ VisitGroup(visitor);
+ } else {
+ AddStates(ElementState::INDETERMINATE);
+ }
+
+ // Remove this radio from its group in the container.
+ // We need to call UpdateValueMissingValidityStateForRadio before to make sure
+ // the group validity is updated (with this element being ignored).
+ UpdateValueMissingValidityStateForRadio(true);
+ container->RemoveFromRadioGroup(name, this);
+ mRadioGroupContainer = nullptr;
+}
+
+bool HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable(
+ aWithMouse, aIsFocusable, aTabIndex)) {
+ return true;
+ }
+
+ if (IsDisabled()) {
+ *aIsFocusable = false;
+ return true;
+ }
+
+ if (IsSingleLineTextControl(false) || mType == FormControlType::InputRange) {
+ *aIsFocusable = true;
+ return false;
+ }
+
+ const bool defaultFocusable = IsFormControlDefaultFocusable(aWithMouse);
+ if (CreatesDateTimeWidget()) {
+ if (aTabIndex) {
+ // We only want our native anonymous child to be tabable to, not ourself.
+ *aTabIndex = -1;
+ }
+ *aIsFocusable = true;
+ return true;
+ }
+
+ if (mType == FormControlType::InputHidden) {
+ if (aTabIndex) {
+ *aTabIndex = -1;
+ }
+ *aIsFocusable = false;
+ return false;
+ }
+
+ if (!aTabIndex) {
+ // The other controls are all focusable
+ *aIsFocusable = defaultFocusable;
+ return false;
+ }
+
+ if (mType != FormControlType::InputRadio) {
+ *aIsFocusable = defaultFocusable;
+ return false;
+ }
+
+ if (mChecked) {
+ // Selected radio buttons are tabbable
+ *aIsFocusable = defaultFocusable;
+ return false;
+ }
+
+ // Current radio button is not selected.
+ // But make it tabbable if nothing in group is selected.
+ auto* container = GetCurrentRadioGroupContainer();
+ if (!container) {
+ *aIsFocusable = defaultFocusable;
+ return false;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ if (container->GetCurrentRadioButton(name)) {
+ *aTabIndex = -1;
+ }
+ *aIsFocusable = defaultFocusable;
+ return false;
+}
+
+nsresult HTMLInputElement::VisitGroup(nsIRadioVisitor* aVisitor) {
+ if (auto* container = GetCurrentRadioGroupContainer()) {
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ return container->WalkRadioGroup(name, aVisitor);
+ }
+
+ aVisitor->Visit(this);
+ return NS_OK;
+}
+
+HTMLInputElement::ValueModeType HTMLInputElement::GetValueMode() const {
+ switch (mType) {
+ case FormControlType::InputHidden:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputButton:
+ case FormControlType::InputReset:
+ case FormControlType::InputImage:
+ return VALUE_MODE_DEFAULT;
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputRadio:
+ return VALUE_MODE_DEFAULT_ON;
+ case FormControlType::InputFile:
+ return VALUE_MODE_FILENAME;
+#ifdef DEBUG
+ case FormControlType::InputText:
+ case FormControlType::InputPassword:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputEmail:
+ case FormControlType::InputUrl:
+ case FormControlType::InputNumber:
+ case FormControlType::InputRange:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputColor:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return VALUE_MODE_VALUE;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected input type in GetValueMode()");
+ return VALUE_MODE_VALUE;
+#else // DEBUG
+ default:
+ return VALUE_MODE_VALUE;
+#endif // DEBUG
+ }
+}
+
+bool HTMLInputElement::IsMutable() const {
+ return !IsDisabled() &&
+ !(DoesReadOnlyApply() && State().HasState(ElementState::READONLY));
+}
+
+bool HTMLInputElement::DoesRequiredApply() const {
+ switch (mType) {
+ case FormControlType::InputHidden:
+ case FormControlType::InputButton:
+ case FormControlType::InputImage:
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputRange:
+ case FormControlType::InputColor:
+ return false;
+#ifdef DEBUG
+ case FormControlType::InputRadio:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputFile:
+ case FormControlType::InputText:
+ case FormControlType::InputPassword:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputEmail:
+ case FormControlType::InputUrl:
+ case FormControlType::InputNumber:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected input type in DoesRequiredApply()");
+ return true;
+#else // DEBUG
+ default:
+ return true;
+#endif // DEBUG
+ }
+}
+
+bool HTMLInputElement::PlaceholderApplies() const {
+ if (IsDateTimeInputType(mType)) {
+ return false;
+ }
+ return IsSingleLineTextControl(false);
+}
+
+bool HTMLInputElement::DoesMinMaxApply() const {
+ switch (mType) {
+ case FormControlType::InputNumber:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputRange:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+#ifdef DEBUG
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ case FormControlType::InputButton:
+ case FormControlType::InputHidden:
+ case FormControlType::InputRadio:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputFile:
+ case FormControlType::InputText:
+ case FormControlType::InputPassword:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputEmail:
+ case FormControlType::InputUrl:
+ case FormControlType::InputColor:
+ return false;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected input type in DoesRequiredApply()");
+ return false;
+#else // DEBUG
+ default:
+ return false;
+#endif // DEBUG
+ }
+}
+
+bool HTMLInputElement::DoesAutocompleteApply() const {
+ switch (mType) {
+ case FormControlType::InputHidden:
+ case FormControlType::InputText:
+ case FormControlType::InputSearch:
+ case FormControlType::InputUrl:
+ case FormControlType::InputTel:
+ case FormControlType::InputEmail:
+ case FormControlType::InputPassword:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputNumber:
+ case FormControlType::InputRange:
+ case FormControlType::InputColor:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+#ifdef DEBUG
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputImage:
+ case FormControlType::InputButton:
+ case FormControlType::InputRadio:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputFile:
+ return false;
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "Unexpected input type in DoesAutocompleteApply()");
+ return false;
+#else // DEBUG
+ default:
+ return false;
+#endif // DEBUG
+ }
+}
+
+Decimal HTMLInputElement::GetStep() const {
+ MOZ_ASSERT(DoesStepApply(), "GetStep() can only be called if @step applies");
+
+ if (!HasAttr(nsGkAtoms::step)) {
+ return GetDefaultStep() * GetStepScaleFactor();
+ }
+
+ nsAutoString stepStr;
+ GetAttr(nsGkAtoms::step, stepStr);
+
+ if (stepStr.LowerCaseEqualsLiteral("any")) {
+ // The element can't suffer from step mismatch if there is no step.
+ return kStepAny;
+ }
+
+ Decimal step = StringToDecimal(stepStr);
+ if (!step.isFinite() || step <= Decimal(0)) {
+ step = GetDefaultStep();
+ }
+
+ // For input type=date, we round the step value to have a rounded day.
+ if (mType == FormControlType::InputDate ||
+ mType == FormControlType::InputMonth ||
+ mType == FormControlType::InputWeek) {
+ step = std::max(step.round(), Decimal(1));
+ }
+
+ return step * GetStepScaleFactor();
+}
+
+// ConstraintValidation
+
+void HTMLInputElement::SetCustomValidity(const nsAString& aError) {
+ ConstraintValidation::SetCustomValidity(aError);
+ UpdateValidityElementStates(true);
+}
+
+bool HTMLInputElement::IsTooLong() {
+ if (!mValueChanged || !mLastValueChangeWasInteractive) {
+ return false;
+ }
+
+ return mInputType->IsTooLong();
+}
+
+bool HTMLInputElement::IsTooShort() {
+ if (!mValueChanged || !mLastValueChangeWasInteractive) {
+ return false;
+ }
+
+ return mInputType->IsTooShort();
+}
+
+bool HTMLInputElement::IsValueMissing() const {
+ // Should use UpdateValueMissingValidityStateForRadio() for type radio.
+ MOZ_ASSERT(mType != FormControlType::InputRadio);
+
+ return mInputType->IsValueMissing();
+}
+
+bool HTMLInputElement::HasTypeMismatch() const {
+ return mInputType->HasTypeMismatch();
+}
+
+Maybe<bool> HTMLInputElement::HasPatternMismatch() const {
+ return mInputType->HasPatternMismatch();
+}
+
+bool HTMLInputElement::IsRangeOverflow() const {
+ return mInputType->IsRangeOverflow();
+}
+
+bool HTMLInputElement::IsRangeUnderflow() const {
+ return mInputType->IsRangeUnderflow();
+}
+
+bool HTMLInputElement::ValueIsStepMismatch(const Decimal& aValue) const {
+ if (aValue.isNaN()) {
+ // The element can't suffer from step mismatch if its value isn't a
+ // number.
+ return false;
+ }
+
+ Decimal step = GetStep();
+ if (step == kStepAny) {
+ return false;
+ }
+
+ // Value has to be an integral multiple of step.
+ return NS_floorModulo(aValue - GetStepBase(), step) != Decimal(0);
+}
+
+bool HTMLInputElement::HasStepMismatch() const {
+ return mInputType->HasStepMismatch();
+}
+
+bool HTMLInputElement::HasBadInput() const { return mInputType->HasBadInput(); }
+
+void HTMLInputElement::UpdateTooLongValidityState() {
+ SetValidityState(VALIDITY_STATE_TOO_LONG, IsTooLong());
+}
+
+void HTMLInputElement::UpdateTooShortValidityState() {
+ SetValidityState(VALIDITY_STATE_TOO_SHORT, IsTooShort());
+}
+
+void HTMLInputElement::UpdateValueMissingValidityStateForRadio(
+ bool aIgnoreSelf) {
+ MOZ_ASSERT(mType == FormControlType::InputRadio,
+ "This should be called only for radio input types");
+
+ HTMLInputElement* selection = GetSelectedRadioButton();
+
+ // If there is no selection, that might mean the radio is not in a group.
+ // In that case, we can look for the checked state of the radio.
+ bool selected = selection || (!aIgnoreSelf && mChecked);
+ bool required = !aIgnoreSelf && IsRequired();
+
+ auto* container = GetCurrentRadioGroupContainer();
+ if (!container) {
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING, false);
+ return;
+ }
+
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+
+ // If the current radio is required and not ignored, we can assume the entire
+ // group is required.
+ if (!required) {
+ required = (aIgnoreSelf && IsRequired())
+ ? container->GetRequiredRadioCount(name) - 1
+ : container->GetRequiredRadioCount(name);
+ }
+
+ bool valueMissing = required && !selected;
+ if (container->GetValueMissingState(name) != valueMissing) {
+ container->SetValueMissingState(name, valueMissing);
+
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING, valueMissing);
+
+ // nsRadioSetValueMissingState will call ElementStateChanged while visiting.
+ nsAutoScriptBlocker scriptBlocker;
+ nsCOMPtr<nsIRadioVisitor> visitor =
+ new nsRadioSetValueMissingState(this, valueMissing);
+ VisitGroup(visitor);
+ }
+}
+
+void HTMLInputElement::UpdateValueMissingValidityState() {
+ if (mType == FormControlType::InputRadio) {
+ UpdateValueMissingValidityStateForRadio(false);
+ return;
+ }
+
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING, IsValueMissing());
+}
+
+void HTMLInputElement::UpdateTypeMismatchValidityState() {
+ SetValidityState(VALIDITY_STATE_TYPE_MISMATCH, HasTypeMismatch());
+}
+
+void HTMLInputElement::UpdatePatternMismatchValidityState() {
+ Maybe<bool> hasMismatch = HasPatternMismatch();
+ // Don't update if the JS engine failed to evaluate it.
+ if (hasMismatch.isSome()) {
+ SetValidityState(VALIDITY_STATE_PATTERN_MISMATCH, hasMismatch.value());
+ }
+}
+
+void HTMLInputElement::UpdateRangeOverflowValidityState() {
+ SetValidityState(VALIDITY_STATE_RANGE_OVERFLOW, IsRangeOverflow());
+ UpdateInRange(true);
+}
+
+void HTMLInputElement::UpdateRangeUnderflowValidityState() {
+ SetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW, IsRangeUnderflow());
+ UpdateInRange(true);
+}
+
+void HTMLInputElement::UpdateStepMismatchValidityState() {
+ SetValidityState(VALIDITY_STATE_STEP_MISMATCH, HasStepMismatch());
+}
+
+void HTMLInputElement::UpdateBadInputValidityState() {
+ SetValidityState(VALIDITY_STATE_BAD_INPUT, HasBadInput());
+}
+
+void HTMLInputElement::UpdateAllValidityStates(bool aNotify) {
+ bool validBefore = IsValid();
+ UpdateAllValidityStatesButNotElementState();
+ if (validBefore != IsValid()) {
+ UpdateValidityElementStates(aNotify);
+ }
+}
+
+void HTMLInputElement::UpdateAllValidityStatesButNotElementState() {
+ UpdateTooLongValidityState();
+ UpdateTooShortValidityState();
+ UpdateValueMissingValidityState();
+ UpdateTypeMismatchValidityState();
+ UpdatePatternMismatchValidityState();
+ UpdateRangeOverflowValidityState();
+ UpdateRangeUnderflowValidityState();
+ UpdateStepMismatchValidityState();
+ UpdateBadInputValidityState();
+}
+
+void HTMLInputElement::UpdateBarredFromConstraintValidation() {
+ // NOTE: readonly attribute causes an element to be barred from constraint
+ // validation even if it doesn't apply to that input type. That's rather
+ // weird, but pre-existing behavior.
+ bool wasCandidate = IsCandidateForConstraintValidation();
+ SetBarredFromConstraintValidation(
+ mType == FormControlType::InputHidden ||
+ mType == FormControlType::InputButton ||
+ mType == FormControlType::InputReset || IsDisabled() ||
+ HasAttr(nsGkAtoms::readonly) ||
+ HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR));
+ if (IsCandidateForConstraintValidation() != wasCandidate) {
+ UpdateInRange(true);
+ }
+}
+
+nsresult HTMLInputElement::GetValidationMessage(nsAString& aValidationMessage,
+ ValidityStateType aType) {
+ return mInputType->GetValidationMessage(aValidationMessage, aType);
+}
+
+bool HTMLInputElement::IsSingleLineTextControl() const {
+ return IsSingleLineTextControl(false);
+}
+
+bool HTMLInputElement::IsTextArea() const { return false; }
+
+bool HTMLInputElement::IsPasswordTextControl() const {
+ return mType == FormControlType::InputPassword;
+}
+
+int32_t HTMLInputElement::GetCols() {
+ // Else we know (assume) it is an input with size attr
+ const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::size);
+ if (attr && attr->Type() == nsAttrValue::eInteger) {
+ int32_t cols = attr->GetIntegerValue();
+ if (cols > 0) {
+ return cols;
+ }
+ }
+
+ return DEFAULT_COLS;
+}
+
+int32_t HTMLInputElement::GetWrapCols() {
+ return 0; // only textarea's can have wrap cols
+}
+
+int32_t HTMLInputElement::GetRows() { return DEFAULT_ROWS; }
+
+void HTMLInputElement::GetDefaultValueFromContent(nsAString& aValue,
+ bool aForDisplay) {
+ if (!GetEditorState()) {
+ return;
+ }
+ GetDefaultValue(aValue);
+ // This is called by the frame to show the value.
+ // We have to sanitize it when needed.
+ // FIXME: Do we want to sanitize even when aForDisplay is false?
+ if (mDoneCreating) {
+ SanitizeValue(aValue, aForDisplay ? SanitizationKind::ForDisplay
+ : SanitizationKind::ForValueGetter);
+ }
+}
+
+bool HTMLInputElement::ValueChanged() const { return mValueChanged; }
+
+void HTMLInputElement::GetTextEditorValue(nsAString& aValue) const {
+ if (TextControlState* state = GetEditorState()) {
+ state->GetValue(aValue, /* aIgnoreWrap = */ true, /* aForDisplay = */ true);
+ }
+}
+
+void HTMLInputElement::InitializeKeyboardEventListeners() {
+ TextControlState* state = GetEditorState();
+ if (state) {
+ state->InitializeKeyboardEventListeners();
+ }
+}
+
+void HTMLInputElement::UpdatePlaceholderShownState() {
+ SetStates(ElementState::PLACEHOLDER_SHOWN,
+ IsValueEmpty() && PlaceholderApplies() &&
+ HasAttr(nsGkAtoms::placeholder));
+}
+
+void HTMLInputElement::OnValueChanged(ValueChangeKind aKind,
+ bool aNewValueEmpty,
+ const nsAString* aKnownNewValue) {
+ MOZ_ASSERT_IF(aKnownNewValue, aKnownNewValue->IsEmpty() == aNewValueEmpty);
+ if (aKind != ValueChangeKind::Internal) {
+ mLastValueChangeWasInteractive = aKind == ValueChangeKind::UserInteraction;
+ }
+
+ if (aNewValueEmpty != IsValueEmpty()) {
+ SetStates(ElementState::VALUE_EMPTY, aNewValueEmpty);
+ UpdatePlaceholderShownState();
+ }
+
+ UpdateAllValidityStates(true);
+
+ if (HasDirAuto()) {
+ SetAutoDirectionality(true, aKnownNewValue);
+ }
+}
+
+bool HTMLInputElement::HasCachedSelection() {
+ TextControlState* state = GetEditorState();
+ if (!state) {
+ return false;
+ }
+ return state->IsSelectionCached() && state->HasNeverInitializedBefore() &&
+ state->GetSelectionProperties().GetStart() !=
+ state->GetSelectionProperties().GetEnd();
+}
+
+void HTMLInputElement::SetRevealPassword(bool aValue) {
+ if (NS_WARN_IF(mType != FormControlType::InputPassword)) {
+ return;
+ }
+ if (aValue == State().HasState(ElementState::REVEALED)) {
+ return;
+ }
+ RefPtr doc = OwnerDoc();
+ // We allow chrome code to prevent this. This is important for about:logins,
+ // which may need to run some OS-dependent authentication code before
+ // revealing the saved passwords.
+ bool defaultAction = true;
+ nsContentUtils::DispatchEventOnlyToChrome(
+ doc, this, u"MozWillToggleReveal"_ns, CanBubble::eYes, Cancelable::eYes,
+ &defaultAction);
+ if (NS_WARN_IF(!defaultAction)) {
+ return;
+ }
+ SetStates(ElementState::REVEALED, aValue);
+}
+
+bool HTMLInputElement::RevealPassword() const {
+ if (NS_WARN_IF(mType != FormControlType::InputPassword)) {
+ return false;
+ }
+ return State().HasState(ElementState::REVEALED);
+}
+
+void HTMLInputElement::FieldSetDisabledChanged(bool aNotify) {
+ // This *has* to be called *before* UpdateBarredFromConstraintValidation and
+ // UpdateValueMissingValidityState because these two functions depend on our
+ // disabled state.
+ nsGenericHTMLFormControlElementWithState::FieldSetDisabledChanged(aNotify);
+
+ UpdateValueMissingValidityState();
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(aNotify);
+}
+
+void HTMLInputElement::SetFilePickerFiltersFromAccept(
+ nsIFilePicker* filePicker) {
+ // We always add |filterAll|
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+
+ NS_ASSERTION(HasAttr(nsGkAtoms::accept),
+ "You should not call SetFilePickerFiltersFromAccept if the"
+ " element has no accept attribute!");
+
+ // Services to retrieve image/*, audio/*, video/* filters
+ nsCOMPtr<nsIStringBundleService> stringService =
+ components::StringBundle::Service();
+ if (!stringService) {
+ return;
+ }
+ nsCOMPtr<nsIStringBundle> filterBundle;
+ if (NS_FAILED(stringService->CreateBundle(
+ "chrome://global/content/filepicker.properties",
+ getter_AddRefs(filterBundle)))) {
+ return;
+ }
+
+ // Service to retrieve mime type information for mime types filters
+ nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
+ if (!mimeService) {
+ return;
+ }
+
+ nsAutoString accept;
+ GetAttr(nsGkAtoms::accept, accept);
+
+ HTMLSplitOnSpacesTokenizer tokenizer(accept, ',');
+
+ nsTArray<nsFilePickerFilter> filters;
+ nsString allExtensionsList;
+
+ // Retrieve all filters
+ while (tokenizer.hasMoreTokens()) {
+ const nsDependentSubstring& token = tokenizer.nextToken();
+
+ if (token.IsEmpty()) {
+ continue;
+ }
+
+ int32_t filterMask = 0;
+ nsString filterName;
+ nsString extensionListStr;
+
+ // First, check for image/audio/video filters...
+ if (token.EqualsLiteral("image/*")) {
+ filterMask = nsIFilePicker::filterImages;
+ filterBundle->GetStringFromName("imageFilter", extensionListStr);
+ } else if (token.EqualsLiteral("audio/*")) {
+ filterMask = nsIFilePicker::filterAudio;
+ filterBundle->GetStringFromName("audioFilter", extensionListStr);
+ } else if (token.EqualsLiteral("video/*")) {
+ filterMask = nsIFilePicker::filterVideo;
+ filterBundle->GetStringFromName("videoFilter", extensionListStr);
+ } else if (token.First() == '.') {
+ if (token.Contains(';') || token.Contains('*')) {
+ // Ignore this filter as it contains reserved characters
+ continue;
+ }
+ extensionListStr = u"*"_ns + token;
+ filterName = extensionListStr;
+ } else {
+ //... if no image/audio/video filter is found, check mime types filters
+ nsCOMPtr<nsIMIMEInfo> mimeInfo;
+ if (NS_FAILED(
+ mimeService->GetFromTypeAndExtension(NS_ConvertUTF16toUTF8(token),
+ ""_ns, // No extension
+ getter_AddRefs(mimeInfo))) ||
+ !mimeInfo) {
+ continue;
+ }
+
+ // Get a name for the filter: first try the description, then the mime
+ // type name if there is no description
+ mimeInfo->GetDescription(filterName);
+ if (filterName.IsEmpty()) {
+ nsCString mimeTypeName;
+ mimeInfo->GetType(mimeTypeName);
+ CopyUTF8toUTF16(mimeTypeName, filterName);
+ }
+
+ // Get extension list
+ nsCOMPtr<nsIUTF8StringEnumerator> extensions;
+ mimeInfo->GetFileExtensions(getter_AddRefs(extensions));
+
+ bool hasMore;
+ while (NS_SUCCEEDED(extensions->HasMore(&hasMore)) && hasMore) {
+ nsCString extension;
+ if (NS_FAILED(extensions->GetNext(extension))) {
+ continue;
+ }
+ if (!extensionListStr.IsEmpty()) {
+ extensionListStr.AppendLiteral("; ");
+ }
+ extensionListStr += u"*."_ns + NS_ConvertUTF8toUTF16(extension);
+ }
+ }
+
+ if (!filterMask && (extensionListStr.IsEmpty() || filterName.IsEmpty())) {
+ // No valid filter found
+ continue;
+ }
+
+ // At this point we're sure the token represents a valid filter, so pass
+ // it directly as a raw filter.
+ filePicker->AppendRawFilter(token);
+
+ // If we arrived here, that means we have a valid filter: let's create it
+ // and add it to our list, if no similar filter is already present
+ nsFilePickerFilter filter;
+ if (filterMask) {
+ filter = nsFilePickerFilter(filterMask);
+ } else {
+ filter = nsFilePickerFilter(filterName, extensionListStr);
+ }
+
+ if (!filters.Contains(filter)) {
+ if (!allExtensionsList.IsEmpty()) {
+ allExtensionsList.AppendLiteral("; ");
+ }
+ allExtensionsList += extensionListStr;
+ filters.AppendElement(filter);
+ }
+ }
+
+ // Remove similar filters
+ // Iterate over a copy, as we might modify the original filters list
+ const nsTArray<nsFilePickerFilter> filtersCopy = filters.Clone();
+ for (uint32_t i = 0; i < filtersCopy.Length(); ++i) {
+ const nsFilePickerFilter& filterToCheck = filtersCopy[i];
+ if (filterToCheck.mFilterMask) {
+ continue;
+ }
+ for (uint32_t j = 0; j < filtersCopy.Length(); ++j) {
+ if (i == j) {
+ continue;
+ }
+ // Check if this filter's extension list is a substring of the other one.
+ // e.g. if filters are "*.jpeg" and "*.jpeg; *.jpg" the first one should
+ // be removed.
+ // Add an extra "; " to be sure the check will work and avoid cases like
+ // "*.xls" being a subtring of "*.xslx" while those are two differents
+ // filters and none should be removed.
+ if (FindInReadable(filterToCheck.mFilter + u";"_ns,
+ filtersCopy[j].mFilter + u";"_ns)) {
+ // We already have a similar, less restrictive filter (i.e.
+ // filterToCheck extensionList is just a subset of another filter
+ // extension list): remove this one
+ filters.RemoveElement(filterToCheck);
+ }
+ }
+ }
+
+ // Add "All Supported Types" filter
+ if (filters.Length() > 1) {
+ nsAutoString title;
+ nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES,
+ "AllSupportedTypes", title);
+ filePicker->AppendFilter(title, allExtensionsList);
+ }
+
+ // Add each filter
+ for (uint32_t i = 0; i < filters.Length(); ++i) {
+ const nsFilePickerFilter& filter = filters[i];
+ if (filter.mFilterMask) {
+ filePicker->AppendFilters(filter.mFilterMask);
+ } else {
+ filePicker->AppendFilter(filter.mTitle, filter.mFilter);
+ }
+ }
+
+ if (filters.Length() >= 1) {
+ // |filterAll| will always use index=0 so we need to set index=1 as the
+ // current filter. This will be "All Supported Types" for multiple filters.
+ filePicker->SetFilterIndex(1);
+ }
+}
+
+Decimal HTMLInputElement::GetStepScaleFactor() const {
+ MOZ_ASSERT(DoesStepApply());
+
+ switch (mType) {
+ case FormControlType::InputDate:
+ return kStepScaleFactorDate;
+ case FormControlType::InputNumber:
+ case FormControlType::InputRange:
+ return kStepScaleFactorNumberRange;
+ case FormControlType::InputTime:
+ case FormControlType::InputDatetimeLocal:
+ return kStepScaleFactorTime;
+ case FormControlType::InputMonth:
+ return kStepScaleFactorMonth;
+ case FormControlType::InputWeek:
+ return kStepScaleFactorWeek;
+ default:
+ MOZ_ASSERT(false, "Unrecognized input type");
+ return Decimal::nan();
+ }
+}
+
+Decimal HTMLInputElement::GetDefaultStep() const {
+ MOZ_ASSERT(DoesStepApply());
+
+ switch (mType) {
+ case FormControlType::InputDate:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputNumber:
+ case FormControlType::InputRange:
+ return kDefaultStep;
+ case FormControlType::InputTime:
+ case FormControlType::InputDatetimeLocal:
+ return kDefaultStepTime;
+ default:
+ MOZ_ASSERT(false, "Unrecognized input type");
+ return Decimal::nan();
+ }
+}
+
+void HTMLInputElement::SetUserInteracted(bool aInteracted) {
+ if (mUserInteracted == aInteracted) {
+ return;
+ }
+ mUserInteracted = aInteracted;
+ UpdateValidityElementStates(true);
+}
+
+void HTMLInputElement::UpdateInRange(bool aNotify) {
+ AutoStateChangeNotifier notifier(*this, aNotify);
+ RemoveStatesSilently(ElementState::INRANGE | ElementState::OUTOFRANGE);
+ if (!mHasRange || !IsCandidateForConstraintValidation()) {
+ return;
+ }
+ bool outOfRange = GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW) ||
+ GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW);
+ AddStatesSilently(outOfRange ? ElementState::OUTOFRANGE
+ : ElementState::INRANGE);
+}
+
+void HTMLInputElement::UpdateHasRange(bool aNotify) {
+ // There is a range if min/max applies for the type and if the element
+ // currently have a valid min or max.
+ const bool newHasRange = [&] {
+ if (!DoesMinMaxApply()) {
+ return false;
+ }
+ return !GetMinimum().isNaN() || !GetMaximum().isNaN();
+ }();
+
+ if (newHasRange == mHasRange) {
+ return;
+ }
+
+ mHasRange = newHasRange;
+ UpdateInRange(aNotify);
+}
+
+void HTMLInputElement::PickerClosed() { mPickerRunning = false; }
+
+JSObject* HTMLInputElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLInputElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+GetFilesHelper* HTMLInputElement::GetOrCreateGetFilesHelper(bool aRecursiveFlag,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mFileData);
+
+ if (aRecursiveFlag) {
+ if (!mFileData->mGetFilesRecursiveHelper) {
+ mFileData->mGetFilesRecursiveHelper = GetFilesHelper::Create(
+ GetFilesOrDirectoriesInternal(), aRecursiveFlag, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+
+ return mFileData->mGetFilesRecursiveHelper;
+ }
+
+ if (!mFileData->mGetFilesNonRecursiveHelper) {
+ mFileData->mGetFilesNonRecursiveHelper = GetFilesHelper::Create(
+ GetFilesOrDirectoriesInternal(), aRecursiveFlag, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+
+ return mFileData->mGetFilesNonRecursiveHelper;
+}
+
+void HTMLInputElement::UpdateEntries(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories) {
+ MOZ_ASSERT(mFileData && mFileData->mEntries.IsEmpty());
+
+ nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+ MOZ_ASSERT(global);
+
+ RefPtr<FileSystem> fs = FileSystem::Create(global);
+ if (NS_WARN_IF(!fs)) {
+ return;
+ }
+
+ Sequence<RefPtr<FileSystemEntry>> entries;
+ for (uint32_t i = 0; i < aFilesOrDirectories.Length(); ++i) {
+ RefPtr<FileSystemEntry> entry =
+ FileSystemEntry::Create(global, aFilesOrDirectories[i], fs);
+ MOZ_ASSERT(entry);
+
+ if (!entries.AppendElement(entry, fallible)) {
+ return;
+ }
+ }
+
+ // The root fileSystem is a DirectoryEntry object that contains only the
+ // dropped fileEntry and directoryEntry objects.
+ fs->CreateRoot(entries);
+
+ mFileData->mEntries = std::move(entries);
+}
+
+void HTMLInputElement::GetWebkitEntries(
+ nsTArray<RefPtr<FileSystemEntry>>& aSequence) {
+ if (NS_WARN_IF(mType != FormControlType::InputFile)) {
+ return;
+ }
+
+ Telemetry::Accumulate(Telemetry::BLINK_FILESYSTEM_USED, true);
+ aSequence.AppendElements(mFileData->mEntries);
+}
+
+already_AddRefed<nsINodeList> HTMLInputElement::GetLabels() {
+ if (!IsLabelable()) {
+ return nullptr;
+ }
+
+ return nsGenericHTMLElement::Labels();
+}
+
+void HTMLInputElement::MaybeFireInputPasswordRemoved() {
+ // We want this event to be fired only when the password field is removed
+ // from the DOM tree, not when it is released (ex, tab is closed). So don't
+ // fire an event when the password input field doesn't have a docshell.
+ Document* doc = GetComposedDoc();
+ nsIDocShell* container = doc ? doc->GetDocShell() : nullptr;
+ if (!container) {
+ return;
+ }
+
+ // Right now, only the password manager listens to the event and only listen
+ // to it under certain circumstances. So don't fire this event unless
+ // necessary.
+ if (!doc->ShouldNotifyFormOrPasswordRemoved()) {
+ return;
+ }
+
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *this, u"DOMInputPasswordRemoved"_ns, CanBubble::eNo,
+ ChromeOnlyDispatch::eYes);
+}
+
+} // namespace mozilla::dom
+
+#undef NS_ORIGINAL_CHECKED_VALUE
diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h
new file mode 100644
index 0000000000..8805ce762b
--- /dev/null
+++ b/dom/html/HTMLInputElement.h
@@ -0,0 +1,1679 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLInputElement_h
+#define mozilla_dom_HTMLInputElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Decimal.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TextControlElement.h"
+#include "mozilla/TextControlState.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Variant.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/HTMLInputElementBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/SingleLineTextInputTypes.h"
+#include "mozilla/dom/NumericInputTypes.h"
+#include "mozilla/dom/CheckableInputTypes.h"
+#include "mozilla/dom/ButtonInputTypes.h"
+#include "mozilla/dom/DateTimeInputTypes.h"
+#include "mozilla/dom/ColorInputType.h"
+#include "mozilla/dom/ConstraintValidation.h"
+#include "mozilla/dom/FileInputType.h"
+#include "mozilla/dom/HiddenInputType.h"
+#include "mozilla/dom/RadioGroupContainer.h"
+#include "nsGenericHTMLElement.h"
+#include "nsImageLoadingContent.h"
+#include "nsCOMPtr.h"
+#include "nsIFilePicker.h"
+#include "nsIContentPrefService2.h"
+#include "nsContentUtils.h"
+
+class nsIEditor;
+class nsIRadioVisitor;
+
+namespace mozilla {
+
+class EventChainPostVisitor;
+class EventChainPreVisitor;
+
+namespace dom {
+
+class AfterSetFilesOrDirectoriesRunnable;
+class Date;
+class DispatchChangeEventCallback;
+class File;
+class FileList;
+class FileSystemEntry;
+class FormData;
+class GetFilesHelper;
+class InputType;
+
+/**
+ * A class we use to create a singleton object that is used to keep track of
+ * the last directory from which the user has picked files (via
+ * <input type=file>) on a per-domain basis. The implementation uses
+ * nsIContentPrefService2/NS_CONTENT_PREF_SERVICE_CONTRACTID to store the last
+ * directory per-domain, and to ensure that whether the directories are
+ * persistently saved (saved across sessions) or not honors whether or not the
+ * page is being viewed in private browsing.
+ */
+class UploadLastDir final : public nsIObserver, public nsSupportsWeakReference {
+ ~UploadLastDir() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ /**
+ * Fetch the last used directory for this location from the content
+ * pref service, and display the file picker opened in that directory.
+ *
+ * @param aDoc current document
+ * @param aFilePicker the file picker to open
+ * @param aFpCallback the callback object to be run when the file is shown.
+ */
+ nsresult FetchDirectoryAndDisplayPicker(
+ Document* aDoc, nsIFilePicker* aFilePicker,
+ nsIFilePickerShownCallback* aFpCallback);
+
+ /**
+ * Store the last used directory for this location using the
+ * content pref service, if it is available
+ * @param aURI URI of the current page
+ * @param aDir Parent directory of the file(s)/directory chosen by the user
+ */
+ nsresult StoreLastUsedDirectory(Document* aDoc, nsIFile* aDir);
+
+ class ContentPrefCallback final : public nsIContentPrefCallback2 {
+ virtual ~ContentPrefCallback() = default;
+
+ public:
+ ContentPrefCallback(nsIFilePicker* aFilePicker,
+ nsIFilePickerShownCallback* aFpCallback)
+ : mFilePicker(aFilePicker), mFpCallback(aFpCallback) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTPREFCALLBACK2
+
+ nsCOMPtr<nsIFilePicker> mFilePicker;
+ nsCOMPtr<nsIFilePickerShownCallback> mFpCallback;
+ nsCOMPtr<nsIContentPref> mResult;
+ };
+};
+
+class HTMLInputElement final : public TextControlElement,
+ public nsImageLoadingContent,
+ public ConstraintValidation {
+ friend class AfterSetFilesOrDirectoriesCallback;
+ friend class DispatchChangeEventCallback;
+ friend class InputType;
+
+ public:
+ using ConstraintValidation::GetValidationMessage;
+ using nsGenericHTMLFormControlElementWithState::GetForm;
+ using nsGenericHTMLFormControlElementWithState::GetFormAction;
+ using ValueSetterOption = TextControlState::ValueSetterOption;
+ using ValueSetterOptions = TextControlState::ValueSetterOptions;
+
+ enum class FromClone { No, Yes };
+
+ HTMLInputElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser,
+ FromClone aFromClone = FromClone::No);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLInputElement, input)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ int32_t TabIndexDefault() override;
+ using nsGenericHTMLElement::Focus;
+
+ // nsINode
+#if !defined(ANDROID) && !defined(XP_MACOSX)
+ bool IsNodeApzAwareInternal() const override;
+#endif
+
+ // Element
+ bool IsInteractiveHTMLContent() const override;
+
+ // nsGenericHTMLElement
+ bool IsDisabledForEvents(WidgetEvent* aEvent) override;
+
+ // nsGenericHTMLFormElement
+ void SaveState() override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool RestoreState(PresState* aState) override;
+
+ // EventTarget
+ void AsyncEventRunning(AsyncEventDispatcher* aEvent) override;
+
+ // Overriden nsIFormControl methods
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD Reset() override;
+ NS_IMETHOD SubmitNamesValues(FormData* aFormData) override;
+
+ void FieldSetDisabledChanged(bool aNotify) override;
+
+ // nsIContent
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ // Note: if this returns false, then attributes may not yet be sanitized
+ // (per SetValueInternal's dependence on mDoneCreating).
+ bool IsDoneCreating() const { return mDoneCreating; }
+
+ bool LastValueChangeWasInteractive() const {
+ return mLastValueChangeWasInteractive;
+ }
+
+ void GetLastInteractiveValue(nsAString&);
+
+ nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+ void LegacyPreActivationBehavior(EventChainVisitor& aVisitor) override;
+ MOZ_CAN_RUN_SCRIPT
+ void ActivationBehavior(EventChainPostVisitor& aVisitor) override;
+ void LegacyCanceledActivationBehavior(
+ EventChainPostVisitor& aVisitor) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult PreHandleEvent(EventChainVisitor& aVisitor) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+ MOZ_CAN_RUN_SCRIPT
+ nsresult MaybeHandleRadioButtonNavigation(EventChainPostVisitor&,
+ uint32_t aKeyCode);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void PostHandleEventForRangeThumb(EventChainPostVisitor& aVisitor);
+ MOZ_CAN_RUN_SCRIPT
+ void StartRangeThumbDrag(WidgetGUIEvent* aEvent);
+ MOZ_CAN_RUN_SCRIPT
+ void FinishRangeThumbDrag(WidgetGUIEvent* aEvent = nullptr);
+ MOZ_CAN_RUN_SCRIPT
+ void CancelRangeThumbDrag(bool aIsForUserEvent = true);
+
+ enum class SnapToTickMarks : bool { No, Yes };
+ MOZ_CAN_RUN_SCRIPT
+ void SetValueOfRangeForUserEvent(Decimal aValue,
+ SnapToTickMarks = SnapToTickMarks::No);
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void DoneCreatingElement() override;
+
+ void DestroyContent() override;
+
+ void SetLastValueChangeWasInteractive(bool);
+
+ // TextControlElement
+ bool IsSingleLineTextControlOrTextArea() const override {
+ return IsSingleLineTextControl(false);
+ }
+ void SetValueChanged(bool aValueChanged) override;
+ bool IsSingleLineTextControl() const override;
+ bool IsTextArea() const override;
+ bool IsPasswordTextControl() const override;
+ int32_t GetCols() override;
+ int32_t GetWrapCols() override;
+ int32_t GetRows() override;
+ void GetDefaultValueFromContent(nsAString& aValue, bool aForDisplay) override;
+ bool ValueChanged() const override;
+ void GetTextEditorValue(nsAString& aValue) const override;
+ MOZ_CAN_RUN_SCRIPT TextEditor* GetTextEditor() override;
+ TextEditor* GetTextEditorWithoutCreation() const override;
+ nsISelectionController* GetSelectionController() override;
+ nsFrameSelection* GetConstFrameSelection() override;
+ TextControlState* GetTextControlState() const override {
+ return GetEditorState();
+ }
+ nsresult BindToFrame(nsTextControlFrame* aFrame) override;
+ MOZ_CAN_RUN_SCRIPT void UnbindFromFrame(nsTextControlFrame* aFrame) override;
+ MOZ_CAN_RUN_SCRIPT nsresult CreateEditor() override;
+ void SetPreviewValue(const nsAString& aValue) override;
+ void GetPreviewValue(nsAString& aValue) override;
+ void EnablePreview() override;
+ bool IsPreviewEnabled() override;
+ void InitializeKeyboardEventListeners() override;
+ void OnValueChanged(ValueChangeKind, bool aNewValueEmpty,
+ const nsAString* aKnownNewValue) override;
+ void GetValueFromSetRangeText(nsAString& aValue) override;
+ MOZ_CAN_RUN_SCRIPT nsresult
+ SetValueFromSetRangeText(const nsAString& aValue) override;
+ bool HasCachedSelection() override;
+ MOZ_CAN_RUN_SCRIPT void SetRevealPassword(bool aValue);
+ bool RevealPassword() const;
+
+ // Methods for nsFormFillController so it can do selection operations on input
+ // types the HTML spec doesn't support them on, like "email".
+ uint32_t GetSelectionStartIgnoringType(ErrorResult& aRv);
+ uint32_t GetSelectionEndIgnoringType(ErrorResult& aRv);
+
+ void GetDisplayFileName(nsAString& aFileName) const;
+
+ const nsTArray<OwningFileOrDirectory>& GetFilesOrDirectoriesInternal() const;
+
+ void SetFilesOrDirectories(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories,
+ bool aSetValueChanged);
+ void SetFiles(FileList* aFiles, bool aSetValueChanged);
+
+ // This method is used for test only. Onces the data is set, a 'change' event
+ // is dispatched.
+ void MozSetDndFilesAndDirectories(
+ const nsTArray<OwningFileOrDirectory>& aSequence);
+
+ // Called when a nsIFilePicker or a nsIColorPicker terminate.
+ void PickerClosed();
+
+ void SetCheckedChangedInternal(bool aCheckedChanged);
+ bool GetCheckedChanged() const { return mCheckedChanged; }
+ void AddToRadioGroup();
+ void RemoveFromRadioGroup();
+ void DisconnectRadioGroupContainer();
+
+ /**
+ * Helper function returning the currently selected button in the radio group.
+ * Returning null if the element is not a button or if there is no selectied
+ * button in the group.
+ *
+ * @return the selected button (or null).
+ */
+ HTMLInputElement* GetSelectedRadioButton() const;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLInputElement, TextControlElement)
+
+ static UploadLastDir* gUploadLastDir;
+ // create and destroy the static UploadLastDir object for remembering
+ // which directory was last used on a site-by-site basis
+ static void InitUploadLastDir();
+ static void DestroyUploadLastDir();
+
+ // If the valueAsDate attribute should be enabled in webIDL
+ static bool ValueAsDateEnabled(JSContext* cx, JSObject* obj);
+
+ void MaybeLoadImage();
+
+ bool HasPatternAttribute() const { return mHasPatternAttribute; }
+
+ // nsIConstraintValidation
+ bool IsTooLong();
+ bool IsTooShort();
+ bool IsValueMissing() const;
+ bool HasTypeMismatch() const;
+ Maybe<bool> HasPatternMismatch() const;
+ bool IsRangeOverflow() const;
+ bool IsRangeUnderflow() const;
+ bool ValueIsStepMismatch(const Decimal& aValue) const;
+ bool HasStepMismatch() const;
+ bool HasBadInput() const;
+ void UpdateTooLongValidityState();
+ void UpdateTooShortValidityState();
+ void UpdateValueMissingValidityState();
+ void UpdateTypeMismatchValidityState();
+ void UpdatePatternMismatchValidityState();
+ void UpdateRangeOverflowValidityState();
+ void UpdateRangeUnderflowValidityState();
+ void UpdateStepMismatchValidityState();
+ void UpdateBadInputValidityState();
+ void UpdatePlaceholderShownState();
+ void UpdateCheckedState(bool aNotify);
+ void UpdateIndeterminateState(bool aNotify);
+ // Update all our validity states and then update our element state
+ // as needed. aNotify controls whether the element state update
+ // needs to notify.
+ void UpdateAllValidityStates(bool aNotify);
+ void UpdateValidityElementStates(bool aNotify);
+ MOZ_CAN_RUN_SCRIPT
+ void MaybeUpdateAllValidityStates(bool aNotify) {
+ // If you need to add new type which supports validationMessage, you should
+ // add test cases into test_MozEditableElement_setUserInput.html.
+ if (mType == FormControlType::InputEmail) {
+ UpdateAllValidityStates(aNotify);
+ }
+ }
+
+ // Update all our validity states without updating element state.
+ // This should be called instead of UpdateAllValidityStates any time
+ // we're guaranteed that element state will be updated anyway.
+ void UpdateAllValidityStatesButNotElementState();
+ void UpdateBarredFromConstraintValidation();
+ nsresult GetValidationMessage(nsAString& aValidationMessage,
+ ValidityStateType aType) override;
+
+ // Override SetCustomValidity so we update our state properly when it's called
+ // via bindings.
+ void SetCustomValidity(const nsAString& aError);
+
+ /**
+ * Update the value missing validity state for radio elements when they have
+ * a group.
+ *
+ * @param aIgnoreSelf Whether the required attribute and the checked state
+ * of the current radio should be ignored.
+ * @note This method shouldn't be called if the radio element hasn't a group.
+ */
+ void UpdateValueMissingValidityStateForRadio(bool aIgnoreSelf);
+
+ /**
+ * Set filters to the filePicker according to the accept attribute value.
+ *
+ * See:
+ * http://dev.w3.org/html5/spec/forms.html#attr-input-accept
+ *
+ * @note You should not call this function if the element has no @accept.
+ * @note "All Files" filter is always set, no matter if there is a valid
+ * filter specified or not.
+ * @note If more than one valid filter is found, the "All Supported Types"
+ * filter is added, which is the concatenation of all valid filters.
+ * @note Duplicate filters and similar filters (i.e. filters whose file
+ * extensions already exist in another filter) are ignored.
+ * @note "All Files" filter will be selected by default if unknown mime types
+ * have been specified and no file extension filter has been specified.
+ * Otherwise, specified filter or "All Supported Types" filter will be
+ * selected by default.
+ * The logic behind is that having unknown mime type means we might restrict
+ * user's input too much, as some filters will be missing.
+ * However, if author has also specified some file extension filters, it's
+ * likely those are fallback for the unusual mime type we haven't been able
+ * to resolve; so it's better to select author specified filters in that case.
+ */
+ void SetFilePickerFiltersFromAccept(nsIFilePicker* filePicker);
+
+ void SetUserInteracted(bool) final;
+
+ /**
+ * Fires change event if mFocusedValue and current value held are unequal and
+ * if a change event may be fired on bluring.
+ * Sets mFocusedValue to value, if a change event is fired.
+ */
+ void FireChangeEventIfNeeded();
+
+ /**
+ * Returns the input element's value as a Decimal.
+ * Returns NaN if the current element's value is not a floating point number.
+ *
+ * @return the input element's value as a Decimal.
+ */
+ Decimal GetValueAsDecimal() const;
+
+ /**
+ * Returns the input's "minimum" (as defined by the HTML5 spec) as a double.
+ * Note this takes account of any default minimum that the type may have.
+ * Returns NaN if the min attribute isn't a valid floating point number and
+ * the input's type does not have a default minimum.
+ *
+ * NOTE: Only call this if you know DoesMinMaxApply() returns true.
+ */
+ Decimal GetMinimum() const;
+
+ /**
+ * Returns the input's "maximum" (as defined by the HTML5 spec) as a double.
+ * Note this takes account of any default maximum that the type may have.
+ * Returns NaN if the max attribute isn't a valid floating point number and
+ * the input's type does not have a default maximum.
+ *
+ * NOTE:Only call this if you know DoesMinMaxApply() returns true.
+ */
+ Decimal GetMaximum() const;
+
+ // WebIDL
+
+ void GetAccept(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::accept, aValue); }
+ void SetAccept(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::accept, aValue, aRv);
+ }
+
+ void GetAlt(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::alt, aValue); }
+ void SetAlt(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::alt, aValue, aRv);
+ }
+
+ void GetAutocomplete(nsAString& aValue);
+ void SetAutocomplete(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::autocomplete, aValue, aRv);
+ }
+
+ void GetAutocompleteInfo(Nullable<AutocompleteInfo>& aInfo);
+
+ void GetCapture(nsAString& aValue);
+ void SetCapture(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::capture, aValue, aRv);
+ }
+
+ bool DefaultChecked() const { return HasAttr(nsGkAtoms::checked); }
+
+ void SetDefaultChecked(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::checked, aValue, aRv);
+ }
+
+ bool Checked() const { return mChecked; }
+ void SetChecked(bool aChecked);
+
+ bool IsRadioOrCheckbox() const {
+ return mType == FormControlType::InputCheckbox ||
+ mType == FormControlType::InputRadio;
+ }
+
+ bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); }
+
+ void SetDisabled(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::disabled, aValue, aRv);
+ }
+
+ FileList* GetFiles();
+ void SetFiles(FileList* aFiles);
+
+ void SetFormAction(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::formaction, aValue, aRv);
+ }
+
+ void GetFormEnctype(nsAString& aValue);
+ void SetFormEnctype(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::formenctype, aValue, aRv);
+ }
+
+ void GetFormMethod(nsAString& aValue);
+ void SetFormMethod(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::formmethod, aValue, aRv);
+ }
+
+ bool FormNoValidate() const { return GetBoolAttr(nsGkAtoms::formnovalidate); }
+
+ void SetFormNoValidate(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::formnovalidate, aValue, aRv);
+ }
+
+ void GetFormTarget(nsAString& aValue) {
+ GetHTMLAttr(nsGkAtoms::formtarget, aValue);
+ }
+ void SetFormTarget(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::formtarget, aValue, aRv);
+ }
+
+ MOZ_CAN_RUN_SCRIPT uint32_t Height();
+
+ void SetHeight(uint32_t aValue, ErrorResult& aRv) {
+ SetUnsignedIntAttr(nsGkAtoms::height, aValue, 0, aRv);
+ }
+
+ bool Indeterminate() const { return mIndeterminate; }
+
+ bool IsDraggingRange() const { return mIsDraggingRange; }
+ void SetIndeterminate(bool aValue);
+
+ HTMLDataListElement* GetList() const;
+
+ void GetMax(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::max, aValue); }
+ void SetMax(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::max, aValue, aRv);
+ }
+
+ int32_t MaxLength() const { return GetIntAttr(nsGkAtoms::maxlength, -1); }
+
+ int32_t UsedMaxLength() const final {
+ if (!mInputType->MinAndMaxLengthApply()) {
+ return -1;
+ }
+ return MaxLength();
+ }
+
+ void SetMaxLength(int32_t aValue, ErrorResult& aRv) {
+ int32_t minLength = MinLength();
+ if (aValue < 0 || (minLength >= 0 && aValue < minLength)) {
+ aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ SetHTMLIntAttr(nsGkAtoms::maxlength, aValue, aRv);
+ }
+
+ int32_t MinLength() const { return GetIntAttr(nsGkAtoms::minlength, -1); }
+
+ void SetMinLength(int32_t aValue, ErrorResult& aRv) {
+ int32_t maxLength = MaxLength();
+ if (aValue < 0 || (maxLength >= 0 && aValue > maxLength)) {
+ aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ SetHTMLIntAttr(nsGkAtoms::minlength, aValue, aRv);
+ }
+
+ void GetMin(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::min, aValue); }
+ void SetMin(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::min, aValue, aRv);
+ }
+
+ bool Multiple() const { return GetBoolAttr(nsGkAtoms::multiple); }
+
+ void SetMultiple(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::multiple, aValue, aRv);
+ }
+
+ void GetName(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); }
+ void SetName(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aValue, aRv);
+ }
+
+ void GetPattern(nsAString& aValue) {
+ GetHTMLAttr(nsGkAtoms::pattern, aValue);
+ }
+ void SetPattern(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::pattern, aValue, aRv);
+ }
+
+ void GetPlaceholder(nsAString& aValue) {
+ GetHTMLAttr(nsGkAtoms::placeholder, aValue);
+ }
+ void SetPlaceholder(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::placeholder, aValue, aRv);
+ }
+
+ bool ReadOnly() const { return GetBoolAttr(nsGkAtoms::readonly); }
+
+ void SetReadOnly(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::readonly, aValue, aRv);
+ }
+
+ bool Required() const { return GetBoolAttr(nsGkAtoms::required); }
+
+ void SetRequired(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::required, aValue, aRv);
+ }
+
+ uint32_t Size() const {
+ return GetUnsignedIntAttr(nsGkAtoms::size, DEFAULT_COLS);
+ }
+
+ void SetSize(uint32_t aValue, ErrorResult& aRv) {
+ if (aValue == 0) {
+ aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ SetUnsignedIntAttr(nsGkAtoms::size, aValue, DEFAULT_COLS, aRv);
+ }
+
+ void GetSrc(nsAString& aValue) {
+ GetURIAttr(nsGkAtoms::src, nullptr, aValue);
+ }
+ void SetSrc(const nsAString& aValue, nsIPrincipal* aTriggeringPrincipal,
+ ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::src, aValue, aTriggeringPrincipal, aRv);
+ }
+
+ void GetStep(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::step, aValue); }
+ void SetStep(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::step, aValue, aRv);
+ }
+
+ void GetType(nsAString& aValue) const;
+ void SetType(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::type, aValue, aRv);
+ }
+
+ void GetDefaultValue(nsAString& aValue) {
+ GetHTMLAttr(nsGkAtoms::value, aValue);
+ }
+ void SetDefaultValue(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::value, aValue, aRv);
+ }
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void SetValue(const nsAString& aValue, CallerType aCallerType,
+ ErrorResult& aRv);
+ void GetValue(nsAString& aValue, CallerType aCallerType);
+
+ void GetValueAsDate(JSContext* aCx, JS::MutableHandle<JSObject*> aObj,
+ ErrorResult& aRv);
+
+ void SetValueAsDate(JSContext* aCx, JS::Handle<JSObject*> aObj,
+ ErrorResult& aRv);
+
+ double ValueAsNumber() const {
+ return DoesValueAsNumberApply() ? GetValueAsDecimal().toDouble()
+ : UnspecifiedNaN<double>();
+ }
+
+ void SetValueAsNumber(double aValue, ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT uint32_t Width();
+
+ void SetWidth(uint32_t aValue, ErrorResult& aRv) {
+ SetUnsignedIntAttr(nsGkAtoms::width, aValue, 0, aRv);
+ }
+
+ void StepUp(int32_t aN, ErrorResult& aRv) { aRv = ApplyStep(aN); }
+
+ void StepDown(int32_t aN, ErrorResult& aRv) { aRv = ApplyStep(-aN); }
+
+ /**
+ * Returns the current step value.
+ * Returns kStepAny if the current step is "any" string.
+ *
+ * @return the current step value.
+ */
+ Decimal GetStep() const;
+
+ // Returns whether the given keyboard event steps up or down the value of an
+ // <input> element.
+ bool StepsInputValue(const WidgetKeyboardEvent&) const;
+
+ already_AddRefed<nsINodeList> GetLabels();
+
+ MOZ_CAN_RUN_SCRIPT void Select();
+
+ Nullable<uint32_t> GetSelectionStart(ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void SetSelectionStart(const Nullable<uint32_t>& aValue,
+ ErrorResult& aRv);
+
+ Nullable<uint32_t> GetSelectionEnd(ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void SetSelectionEnd(const Nullable<uint32_t>& aValue,
+ ErrorResult& aRv);
+
+ void GetSelectionDirection(nsAString& aValue, ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void SetSelectionDirection(const nsAString& aValue,
+ ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT void SetSelectionRange(
+ uint32_t aStart, uint32_t aEnd, const Optional<nsAString>& direction,
+ ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement,
+ ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement,
+ uint32_t aStart, uint32_t aEnd,
+ SelectionMode aSelectMode,
+ ErrorResult& aRv);
+
+ void ShowPicker(ErrorResult& aRv);
+
+ bool WebkitDirectoryAttr() const {
+ return HasAttr(nsGkAtoms::webkitdirectory);
+ }
+
+ void SetWebkitDirectoryAttr(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::webkitdirectory, aValue, aRv);
+ }
+
+ void GetWebkitEntries(nsTArray<RefPtr<FileSystemEntry>>& aSequence);
+
+ already_AddRefed<Promise> GetFilesAndDirectories(ErrorResult& aRv);
+
+ void GetAlign(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::align, aValue); }
+ void SetAlign(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::align, aValue, aRv);
+ }
+
+ void GetUseMap(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::usemap, aValue); }
+ void SetUseMap(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::usemap, aValue, aRv);
+ }
+
+ void GetDirName(nsAString& aValue) {
+ GetHTMLAttr(nsGkAtoms::dirname, aValue);
+ }
+ void SetDirName(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::dirname, aValue, aRv);
+ }
+
+ nsIControllers* GetControllers(ErrorResult& aRv);
+ // XPCOM adapter function widely used throughout code, leaving it as is.
+ nsresult GetControllers(nsIControllers** aResult);
+
+ int32_t InputTextLength(CallerType aCallerType);
+
+ void MozGetFileNameArray(nsTArray<nsString>& aFileNames, ErrorResult& aRv);
+
+ void MozSetFileNameArray(const Sequence<nsString>& aFileNames,
+ ErrorResult& aRv);
+ void MozSetFileArray(const Sequence<OwningNonNull<File>>& aFiles);
+ void MozSetDirectory(const nsAString& aDirectoryPath, ErrorResult& aRv);
+
+ /*
+ * The following functions are called from datetime picker to let input box
+ * know the current state of the picker or to update the input box on changes.
+ */
+ void GetDateTimeInputBoxValue(DateTimeValue& aValue);
+
+ /*
+ * This allows chrome JavaScript to dispatch event to the inner datetimebox
+ * anonymous or UA Widget element.
+ */
+ Element* GetDateTimeBoxElement();
+
+ /*
+ * The following functions are called from datetime input box XBL to control
+ * and update the picker.
+ */
+ void OpenDateTimePicker(const DateTimeValue& aInitialValue);
+ void UpdateDateTimePicker(const DateTimeValue& aValue);
+ void CloseDateTimePicker();
+
+ /*
+ * Called from datetime input box binding when inner text fields are focused
+ * or blurred.
+ */
+ void SetFocusState(bool aIsFocused);
+
+ /*
+ * Called from datetime input box binding when the the user entered value
+ * becomes valid/invalid.
+ */
+ void UpdateValidityState();
+
+ /*
+ * The following are called from datetime input box binding to get the
+ * corresponding computed values.
+ */
+ double GetStepAsDouble() { return GetStep().toDouble(); }
+ double GetStepBaseAsDouble() { return GetStepBase().toDouble(); }
+ double GetMinimumAsDouble() { return GetMinimum().toDouble(); }
+ double GetMaximumAsDouble() { return GetMaximum().toDouble(); }
+
+ void StartNumberControlSpinnerSpin();
+ enum SpinnerStopState { eAllowDispatchingEvents, eDisallowDispatchingEvents };
+ void StopNumberControlSpinnerSpin(
+ SpinnerStopState aState = eAllowDispatchingEvents);
+ MOZ_CAN_RUN_SCRIPT
+ void StepNumberControlForUserEvent(int32_t aDirection);
+
+ /**
+ * The callback function used by the nsRepeatService that we use to spin the
+ * spinner for <input type=number>.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ static void HandleNumberControlSpin(void* aData);
+
+ bool NumberSpinnerUpButtonIsDepressed() const {
+ return mNumberControlSpinnerIsSpinning && mNumberControlSpinnerSpinsUp;
+ }
+
+ bool NumberSpinnerDownButtonIsDepressed() const {
+ return mNumberControlSpinnerIsSpinning && !mNumberControlSpinnerSpinsUp;
+ }
+
+ bool MozIsTextField(bool aExcludePassword);
+
+ MOZ_CAN_RUN_SCRIPT nsIEditor* GetEditorForBindings();
+ // For WebIDL bindings.
+ bool HasEditor() const;
+
+ bool IsInputEventTarget() const { return IsSingleLineTextControl(false); }
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void SetUserInput(const nsAString& aInput, nsIPrincipal& aSubjectPrincipal);
+
+ /**
+ * If aValue contains a valid floating-point number in the format specified
+ * by the HTML 5 spec:
+ *
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#floating-point-numbers
+ *
+ * then this function will return the number parsed as a Decimal, otherwise
+ * it will return a Decimal for which Decimal::isFinite() will return false.
+ */
+ static Decimal StringToDecimal(const nsAString& aValue);
+
+ void UpdateEntries(
+ const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories);
+
+ /**
+ * Returns if the required attribute applies for the current type.
+ */
+ bool DoesRequiredApply() const;
+
+ /**
+ * Returns the current required state of the element. This function differs
+ * from Required() in that this function only returns true for input types
+ * that @required attribute applies and the attribute is set; in contrast,
+ * Required() returns true whenever @required attribute is set.
+ */
+ bool IsRequired() const { return State().HasState(ElementState::REQUIRED); }
+
+ bool HasBeenTypePassword() const { return mHasBeenTypePassword; }
+
+ /**
+ * Returns whether the current value is the empty string. This only makes
+ * sense for some input types; does NOT make sense for file inputs.
+ *
+ * @return whether the current value is the empty string.
+ */
+ bool IsValueEmpty() const {
+ return State().HasState(ElementState::VALUE_EMPTY);
+ }
+
+ // Parse a simple (hex) color.
+ static mozilla::Maybe<nscolor> ParseSimpleColor(const nsAString& aColor);
+
+ protected:
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual ~HTMLInputElement();
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Pull IsSingleLineTextControl into our scope, otherwise it'd be hidden
+ // by the TextControlElement version.
+ using nsGenericHTMLFormControlElementWithState::IsSingleLineTextControl;
+
+ /**
+ * The ValueModeType specifies how the value IDL attribute should behave.
+ *
+ * See: http://dev.w3.org/html5/spec/forms.html#dom-input-value
+ */
+ enum ValueModeType {
+ // On getting, returns the value.
+ // On setting, sets value.
+ VALUE_MODE_VALUE,
+ // On getting, returns the value if present or the empty string.
+ // On setting, sets the value.
+ VALUE_MODE_DEFAULT,
+ // On getting, returns the value if present or "on".
+ // On setting, sets the value.
+ VALUE_MODE_DEFAULT_ON,
+ // On getting, returns "C:\fakepath\" followed by the file name of the
+ // first file of the selected files if any.
+ // On setting the empty string, empties the selected files list, otherwise
+ // throw the INVALID_STATE_ERR exception.
+ VALUE_MODE_FILENAME
+ };
+
+ /**
+ * This helper method convert a sub-string that contains only digits to a
+ * number (unsigned int given that it can't contain a minus sign).
+ * This method will return whether the sub-string is correctly formatted
+ * (ie. contains only digit) and it can be successfuly parsed to generate a
+ * number).
+ * If the method returns true, |aResult| will contained the parsed number.
+ *
+ * @param aValue the string on which the sub-string will be extracted and
+ * parsed.
+ * @param aStart the beginning of the sub-string in aValue.
+ * @param aLen the length of the sub-string.
+ * @param aResult the parsed number.
+ * @return whether the sub-string has been parsed successfully.
+ */
+ static bool DigitSubStringToNumber(const nsAString& aValue, uint32_t aStart,
+ uint32_t aLen, uint32_t* aResult);
+
+ // Helper method
+
+ /**
+ * Setting the value.
+ *
+ * @param aValue String to set.
+ * @param aOldValue Previous value before setting aValue.
+ If previous value is unknown, aOldValue can be nullptr.
+ * @param aOptions See TextControlState::ValueSetterOption.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ SetValueInternal(const nsAString& aValue, const nsAString* aOldValue,
+ const ValueSetterOptions& aOptions);
+ MOZ_CAN_RUN_SCRIPT nsresult SetValueInternal(
+ const nsAString& aValue, const ValueSetterOptions& aOptions) {
+ return SetValueInternal(aValue, nullptr, aOptions);
+ }
+
+ // Generic getter for the value that doesn't do experimental control type
+ // sanitization.
+ void GetValueInternal(nsAString& aValue, CallerType aCallerType) const;
+
+ // A getter for callers that know we're not dealing with a file input, so they
+ // don't have to think about the caller type.
+ void GetNonFileValueInternal(nsAString& aValue) const;
+
+ void ClearFiles(bool aSetValueChanged);
+
+ void SetIndeterminateInternal(bool aValue, bool aShouldInvalidate);
+
+ /**
+ * Called when an attribute is about to be changed
+ */
+ void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+ /**
+ * Called when an attribute has just been changed
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ void BeforeSetForm(HTMLFormElement* aForm, bool aBindToTree) override;
+
+ void AfterClearForm(bool aUnbindOrDelete) override;
+
+ void ResultForDialogSubmit(nsAString& aResult) override;
+
+ void SelectAll(nsPresContext* aPresContext);
+ bool IsImage() const {
+ return AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::image,
+ eIgnoreCase);
+ }
+
+ /**
+ * Visit the group of radio buttons this radio belongs to
+ * @param aVisitor the visitor to visit with
+ */
+ nsresult VisitGroup(nsIRadioVisitor* aVisitor);
+
+ /**
+ * Do all the work that |SetChecked| does (radio button handling, etc.), but
+ * take an |aNotify| parameter.
+ */
+ void DoSetChecked(bool aValue, bool aNotify, bool aSetValueChanged);
+
+ /**
+ * Do all the work that |SetCheckedChanged| does (radio button handling,
+ * etc.), but take an |aNotify| parameter that lets it avoid flushing content
+ * when it can.
+ */
+ void DoSetCheckedChanged(bool aCheckedChanged, bool aNotify);
+
+ /**
+ * Actually set checked and notify the frame of the change.
+ * @param aValue the value of checked to set
+ */
+ void SetCheckedInternal(bool aValue, bool aNotify);
+
+ void RadioSetChecked(bool aNotify);
+ void SetCheckedChanged(bool aCheckedChanged);
+
+ /**
+ * MaybeSubmitForm looks for a submit input or a single text control
+ * and submits the form if either is present.
+ */
+ MOZ_CAN_RUN_SCRIPT void MaybeSubmitForm(nsPresContext* aPresContext);
+
+ /**
+ * Called after calling one of the SetFilesOrDirectories() functions.
+ * This method can explore the directory recursively if needed.
+ */
+ void AfterSetFilesOrDirectories(bool aSetValueChanged);
+
+ /**
+ * Recursively explore the directory and populate mFileOrDirectories correctly
+ * for webkitdirectory.
+ */
+ void ExploreDirectoryRecursively(bool aSetValuechanged);
+
+ /**
+ * Determine whether the editor needs to be initialized explicitly for
+ * a particular event.
+ */
+ bool NeedToInitializeEditorForEvent(EventChainPreVisitor& aVisitor) const;
+
+ /**
+ * Get the value mode of the element, depending of the type.
+ */
+ ValueModeType GetValueMode() const;
+
+ /**
+ * Get the mutable state of the element.
+ * When the element isn't mutable (immutable), the value or checkedness
+ * should not be changed by the user.
+ *
+ * See: https://html.spec.whatwg.org/#concept-fe-mutable
+ */
+ bool IsMutable() const;
+
+ /**
+ * Returns if the min and max attributes apply for the current type.
+ */
+ bool DoesMinMaxApply() const;
+
+ /**
+ * Returns if the step attribute apply for the current type.
+ */
+ bool DoesStepApply() const { return DoesMinMaxApply(); }
+
+ /**
+ * Returns if stepDown and stepUp methods apply for the current type.
+ */
+ bool DoStepDownStepUpApply() const { return DoesStepApply(); }
+
+ /**
+ * Returns if valueAsNumber attribute applies for the current type.
+ */
+ bool DoesValueAsNumberApply() const { return DoesMinMaxApply(); }
+
+ /**
+ * Returns if autocomplete attribute applies for the current type.
+ */
+ bool DoesAutocompleteApply() const;
+
+ MOZ_CAN_RUN_SCRIPT void FreeData();
+ TextControlState* GetEditorState() const;
+ void EnsureEditorState();
+
+ MOZ_CAN_RUN_SCRIPT TextEditor* GetTextEditorFromState();
+
+ /**
+ * Manages the internal data storage across type changes.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ void HandleTypeChange(FormControlType aNewType, bool aNotify);
+
+ /**
+ * If the input range has a list, this function will snap the given value to
+ * the nearest tick mark, but only if the given value is close enough to that
+ * tick mark.
+ */
+ void MaybeSnapToTickMark(Decimal& aValue);
+
+ enum class SanitizationKind { ForValueGetter, ForValueSetter, ForDisplay };
+ /**
+ * Sanitize the value of the element depending of its current type.
+ * See:
+ * http://www.whatwg.org/specs/web-apps/current-work/#value-sanitization-algorithm
+ */
+ void SanitizeValue(nsAString& aValue, SanitizationKind) const;
+
+ /**
+ * Returns whether the placeholder attribute applies for the current type.
+ */
+ bool PlaceholderApplies() const;
+
+ /**
+ * Set the current default value to the value of the input element.
+ * @note You should not call this method if GetValueMode() doesn't return
+ * VALUE_MODE_VALUE.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ nsresult SetDefaultValueAsValue();
+
+ /**
+ * Sets the direction from the input value. if aKnownValue is provided, it
+ * saves a GetValue call.
+ */
+ void SetAutoDirectionality(bool aNotify,
+ const nsAString* aKnownValue = nullptr);
+
+ /**
+ * Returns the radio group container within the DOM tree that the element
+ * is currently a member of, if one exists.
+ */
+ RadioGroupContainer* GetCurrentRadioGroupContainer() const;
+ /**
+ * Returns the radio group container within the DOM tree that the element
+ * should be added into, if one exists.
+ */
+ RadioGroupContainer* FindTreeRadioGroupContainer() const;
+
+ /**
+ * Parse a color string of the form #XXXXXX where X should be hexa characters
+ * @param the string to be parsed.
+ * @return whether the string is a valid simple color.
+ * Note : this function does not consider the empty string as valid.
+ */
+ bool IsValidSimpleColor(const nsAString& aValue) const;
+
+ /**
+ * Parse a week string of the form yyyy-Www
+ * @param the string to be parsed.
+ * @return whether the string is a valid week.
+ * Note : this function does not consider the empty string as valid.
+ */
+ bool IsValidWeek(const nsAString& aValue) const;
+
+ /**
+ * Parse a month string of the form yyyy-mm
+ * @param the string to be parsed.
+ * @return whether the string is a valid month.
+ * Note : this function does not consider the empty string as valid.
+ */
+ bool IsValidMonth(const nsAString& aValue) const;
+
+ /**
+ * Parse a date string of the form yyyy-mm-dd
+ * @param the string to be parsed.
+ * @return whether the string is a valid date.
+ * Note : this function does not consider the empty string as valid.
+ */
+ bool IsValidDate(const nsAString& aValue) const;
+
+ /**
+ * Parse a datetime-local string of the form yyyy-mm-ddThh:mm[:ss.s] or
+ * yyyy-mm-dd hh:mm[:ss.s], where fractions of seconds can be 1 to 3 digits.
+ *
+ * @param the string to be parsed.
+ * @return whether the string is a valid datetime-local string.
+ * Note : this function does not consider the empty string as valid.
+ */
+ bool IsValidDateTimeLocal(const nsAString& aValue) const;
+
+ /**
+ * Parse a year string of the form yyyy
+ *
+ * @param the string to be parsed.
+ *
+ * @return the year in aYear.
+ * @return whether the parsing was successful.
+ */
+ bool ParseYear(const nsAString& aValue, uint32_t* aYear) const;
+
+ /**
+ * Parse a month string of the form yyyy-mm
+ *
+ * @param the string to be parsed.
+ * @return the year and month in aYear and aMonth.
+ * @return whether the parsing was successful.
+ */
+ bool ParseMonth(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth) const;
+
+ /**
+ * Parse a week string of the form yyyy-Www
+ *
+ * @param the string to be parsed.
+ * @return the year and week in aYear and aWeek.
+ * @return whether the parsing was successful.
+ */
+ bool ParseWeek(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aWeek) const;
+ /**
+ * Parse a date string of the form yyyy-mm-dd
+ *
+ * @param the string to be parsed.
+ * @return the date in aYear, aMonth, aDay.
+ * @return whether the parsing was successful.
+ */
+ bool ParseDate(const nsAString& aValue, uint32_t* aYear, uint32_t* aMonth,
+ uint32_t* aDay) const;
+
+ /**
+ * Parse a datetime-local string of the form yyyy-mm-ddThh:mm[:ss.s] or
+ * yyyy-mm-dd hh:mm[:ss.s], where fractions of seconds can be 1 to 3 digits.
+ *
+ * @param the string to be parsed.
+ * @return the date in aYear, aMonth, aDay and time expressed in milliseconds
+ * in aTime.
+ * @return whether the parsing was successful.
+ */
+ bool ParseDateTimeLocal(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth, uint32_t* aDay,
+ uint32_t* aTime) const;
+
+ /**
+ * Normalize the datetime-local string following the HTML specifications:
+ * https://html.spec.whatwg.org/multipage/infrastructure.html#valid-normalised-local-date-and-time-string
+ */
+ void NormalizeDateTimeLocal(nsAString& aValue) const;
+
+ /**
+ * This methods returns the number of days since epoch for a given year and
+ * week.
+ */
+ double DaysSinceEpochFromWeek(uint32_t aYear, uint32_t aWeek) const;
+
+ /**
+ * This methods returns the number of days in a given month, for a given year.
+ */
+ uint32_t NumberOfDaysInMonth(uint32_t aMonth, uint32_t aYear) const;
+
+ /**
+ * This methods returns the number of months between January 1970 and the
+ * given year and month.
+ */
+ int32_t MonthsSinceJan1970(uint32_t aYear, uint32_t aMonth) const;
+
+ /**
+ * This methods returns the day of the week given a date. If @isoWeek is true,
+ * 7=Sunday, otherwise, 0=Sunday.
+ */
+ uint32_t DayOfWeek(uint32_t aYear, uint32_t aMonth, uint32_t aDay,
+ bool isoWeek) const;
+
+ /**
+ * This methods returns the maximum number of week in a given year, the
+ * result is either 52 or 53.
+ */
+ uint32_t MaximumWeekInYear(uint32_t aYear) const;
+
+ /**
+ * This methods returns true if it's a leap year.
+ */
+ bool IsLeapYear(uint32_t aYear) const;
+
+ /**
+ * Returns whether aValue is a valid time as described by HTML specifications:
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-time-string
+ *
+ * @param aValue the string to be tested.
+ * @return Whether the string is a valid time per HTML specifications.
+ */
+ bool IsValidTime(const nsAString& aValue) const;
+
+ /**
+ * Returns the time expressed in milliseconds of |aValue| being parsed as a
+ * time following the HTML specifications:
+ * http://www.whatwg.org/specs/web-apps/current-work/#parse-a-time-string
+ *
+ * Note: |aResult| can be null.
+ *
+ * @param aValue the string to be parsed.
+ * @param aResult the time expressed in milliseconds representing the time
+ * [out]
+ * @return Whether the parsing was successful.
+ */
+ static bool ParseTime(const nsAString& aValue, uint32_t* aResult);
+
+ /**
+ * Sets the value of the element to the string representation of the Decimal.
+ *
+ * @param aValue The Decimal that will be used to set the value.
+ */
+ void SetValue(Decimal aValue, CallerType aCallerType);
+
+ void UpdateHasRange(bool aNotify);
+ // Updates the :in-range / :out-of-range states.
+ void UpdateInRange(bool aNotify);
+
+ /**
+ * Get the step scale value for the current type.
+ * See:
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-input-element-attributes.html#concept-input-step-scale
+ */
+ Decimal GetStepScaleFactor() const;
+
+ /**
+ * Return the base used to compute if a value matches step.
+ * Basically, it's the min attribute if present and a default value otherwise.
+ *
+ * @return The step base.
+ */
+ Decimal GetStepBase() const;
+
+ /**
+ * Returns the default step for the current type.
+ * @return the default step for the current type.
+ */
+ Decimal GetDefaultStep() const;
+
+ enum StepCallerType { CALLED_FOR_USER_EVENT, CALLED_FOR_SCRIPT };
+
+ /**
+ * Sets the aValue outparam to the value that this input would take if
+ * someone tries to step aStep steps and this input's value would change as
+ * a result. Leaves aValue untouched if this inputs value would not change
+ * (e.g. already at max, and asking for the next step up).
+ *
+ * Negative aStep means step down, positive means step up.
+ *
+ * Returns NS_OK or else the error values that should be thrown if this call
+ * was initiated by a stepUp()/stepDown() call from script under conditions
+ * that such a call should throw.
+ */
+ nsresult GetValueIfStepped(int32_t aStepCount, StepCallerType aCallerType,
+ Decimal* aNextStep);
+
+ /**
+ * Apply a step change from stepUp or stepDown by multiplying aStep by the
+ * current step value.
+ *
+ * @param aStep The value used to be multiplied against the step value.
+ */
+ nsresult ApplyStep(int32_t aStep);
+
+ /**
+ * Returns if the current type is an experimental mobile type.
+ */
+ static bool IsExperimentalMobileType(FormControlType);
+
+ /*
+ * Returns if the current type is one of the date/time input types: date,
+ * time, month, week and datetime-local.
+ */
+ static bool IsDateTimeInputType(FormControlType);
+
+ /**
+ * Returns whether getting `.value` as a string should sanitize the value.
+ *
+ * See SanitizeValue.
+ */
+ bool SanitizesOnValueGetter() const;
+
+ /**
+ * Returns true if the element should prevent dispatching another DOMActivate.
+ * This is used in situations where the anonymous subtree should already have
+ * sent a DOMActivate and prevents firing more than once.
+ */
+ bool ShouldPreventDOMActivateDispatch(EventTarget* aOriginalTarget);
+
+ /**
+ * Some input type (color and file) let user choose a value using a picker:
+ * this function checks if it is needed, and if so, open the corresponding
+ * picker (color picker or file picker).
+ */
+ nsresult MaybeInitPickers(EventChainPostVisitor& aVisitor);
+
+ /**
+ * Returns all valid colors in the <datalist> for the input with type=color.
+ */
+ nsTArray<nsString> GetColorsFromList();
+
+ enum FilePickerType { FILE_PICKER_FILE, FILE_PICKER_DIRECTORY };
+ nsresult InitFilePicker(FilePickerType aType);
+ nsresult InitColorPicker();
+
+ GetFilesHelper* GetOrCreateGetFilesHelper(bool aRecursiveFlag,
+ ErrorResult& aRv);
+
+ void ClearGetFilesHelpers();
+
+ /**
+ * nsINode::SetMayBeApzAware() will be invoked in this function if necessary
+ * to prevent default action of APZC so that we can increase/decrease the
+ * value of this InputElement when mouse wheel event comes without scrolling
+ * the page.
+ *
+ * SetMayBeApzAware() will set flag MayBeApzAware which is checked by apzc to
+ * decide whether to add this element into its dispatch-to-content region.
+ */
+ void UpdateApzAwareFlag();
+
+ /**
+ * A helper to get the current selection range. Will throw on the ErrorResult
+ * if we have no editor state.
+ */
+ void GetSelectionRange(uint32_t* aSelectionStart, uint32_t* aSelectionEnd,
+ ErrorResult& aRv);
+
+ /**
+ * Override for nsImageLoadingContent.
+ */
+ nsIContent* AsContent() override { return this; }
+
+ nsCOMPtr<nsIControllers> mControllers;
+
+ /*
+ * In mInputData, the mState field is used if IsSingleLineTextControl returns
+ * true and mValue is used otherwise. We have to be careful when handling it
+ * on a type change.
+ *
+ * Accessing the mState member should be done using the GetEditorState
+ * function, which returns null if the state is not present.
+ */
+ union InputData {
+ /**
+ * The current value of the input if it has been changed from the default
+ */
+ char16_t* mValue;
+ /**
+ * The state of the text editor associated with the text/password input
+ */
+ TextControlState* mState;
+ } mInputData;
+
+ struct FileData;
+ UniquePtr<FileData> mFileData;
+
+ /**
+ * The value of the input element when first initialized and it is updated
+ * when the element is either changed through a script, focused or dispatches
+ * a change event. This is to ensure correct future change event firing.
+ * NB: This is ONLY applicable where the element is a text control. ie,
+ * where type= "date", "time", "text", "email", "search", "tel", "url" or
+ * "password".
+ */
+ nsString mFocusedValue;
+
+ /**
+ * If mIsDraggingRange is true, this is the value that the input had before
+ * the drag started. Used to reset the input to its old value if the drag is
+ * canceled.
+ */
+ Decimal mRangeThumbDragStartValue;
+
+ /**
+ * Current value in the input box, in DateTimeValue dictionary format, see
+ * HTMLInputElement.webidl for details.
+ */
+ UniquePtr<DateTimeValue> mDateTimeInputBoxValue;
+
+ /**
+ * The triggering principal for the src attribute.
+ */
+ nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal;
+
+ /*
+ * InputType object created based on input type.
+ */
+ UniquePtr<InputType, InputType::DoNotDelete> mInputType;
+
+ static constexpr size_t INPUT_TYPE_SIZE =
+ sizeof(Variant<TextInputType, SearchInputType, TelInputType, URLInputType,
+ EmailInputType, PasswordInputType, NumberInputType,
+ RangeInputType, RadioInputType, CheckboxInputType,
+ ButtonInputType, ImageInputType, ResetInputType,
+ SubmitInputType, DateInputType, TimeInputType,
+ WeekInputType, MonthInputType, DateTimeLocalInputType,
+ FileInputType, ColorInputType, HiddenInputType>);
+
+ // Memory allocated for mInputType, reused when type changes.
+ char mInputTypeMem[INPUT_TYPE_SIZE];
+
+ // Step scale factor values, for input types that have one.
+ static const Decimal kStepScaleFactorDate;
+ static const Decimal kStepScaleFactorNumberRange;
+ static const Decimal kStepScaleFactorTime;
+ static const Decimal kStepScaleFactorMonth;
+ static const Decimal kStepScaleFactorWeek;
+
+ // Default step base value when a type do not have specific one.
+ static const Decimal kDefaultStepBase;
+ // Default step base value when type=week does not not have a specific one,
+ // which is −259200000, the start of week 1970-W01.
+ static const Decimal kDefaultStepBaseWeek;
+
+ // Default step used when there is no specified step.
+ static const Decimal kDefaultStep;
+ static const Decimal kDefaultStepTime;
+
+ // Float value returned by GetStep() when the step attribute is set to 'any'.
+ static const Decimal kStepAny;
+
+ // Minimum year limited by HTML standard, year >= 1.
+ static const double kMinimumYear;
+ // Maximum year limited by ECMAScript date object range, year <= 275760.
+ static const double kMaximumYear;
+ // Maximum valid week is 275760-W37.
+ static const double kMaximumWeekInMaximumYear;
+ // Maximum valid day is 275760-09-13.
+ static const double kMaximumDayInMaximumYear;
+ // Maximum valid month is 275760-09.
+ static const double kMaximumMonthInMaximumYear;
+ // Long years in a ISO calendar have 53 weeks in them.
+ static const double kMaximumWeekInYear;
+ // Milliseconds in a day.
+ static const double kMsPerDay;
+
+ nsContentUtils::AutocompleteAttrState mAutocompleteAttrState;
+ nsContentUtils::AutocompleteAttrState mAutocompleteInfoState;
+ bool mDisabledChanged : 1;
+ // https://html.spec.whatwg.org/#concept-fe-dirty
+ // TODO: Maybe rename to match the spec?
+ bool mValueChanged : 1;
+ // https://html.spec.whatwg.org/#user-interacted
+ bool mUserInteracted : 1;
+ bool mLastValueChangeWasInteractive : 1;
+ bool mCheckedChanged : 1;
+ bool mChecked : 1;
+ bool mHandlingSelectEvent : 1;
+ bool mShouldInitChecked : 1;
+ bool mDoneCreating : 1;
+ bool mInInternalActivate : 1;
+ bool mCheckedIsToggled : 1;
+ bool mIndeterminate : 1;
+ bool mInhibitRestoration : 1;
+ bool mHasRange : 1;
+ bool mIsDraggingRange : 1;
+ bool mNumberControlSpinnerIsSpinning : 1;
+ bool mNumberControlSpinnerSpinsUp : 1;
+ bool mPickerRunning : 1;
+ bool mIsPreviewEnabled : 1;
+ bool mHasBeenTypePassword : 1;
+ bool mHasPatternAttribute : 1;
+
+ private:
+ static void ImageInputMapAttributesIntoRule(MappedDeclarationsBuilder&);
+
+ /**
+ * Returns true if this input's type will fire a DOM "change" event when it
+ * loses focus if its value has changed since it gained focus.
+ */
+ bool MayFireChangeOnBlur() const { return MayFireChangeOnBlur(mType); }
+
+ /**
+ * Returns true if selection methods can be called on element
+ */
+ bool SupportsTextSelection() const {
+ switch (mType) {
+ case FormControlType::InputText:
+ case FormControlType::InputSearch:
+ case FormControlType::InputUrl:
+ case FormControlType::InputTel:
+ case FormControlType::InputPassword:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * https://html.spec.whatwg.org/#auto-directionality-form-associated-elements
+ */
+ static bool IsAutoDirectionalityAssociated(FormControlType aType) {
+ switch (aType) {
+ case FormControlType::InputHidden:
+ case FormControlType::InputText:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputUrl:
+ case FormControlType::InputEmail:
+ case FormControlType::InputPassword:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputReset:
+ case FormControlType::InputButton:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ bool IsAutoDirectionalityAssociated() const {
+ return IsAutoDirectionalityAssociated(mType);
+ }
+
+ static bool CreatesDateTimeWidget(FormControlType aType) {
+ return aType == FormControlType::InputDate ||
+ aType == FormControlType::InputTime ||
+ aType == FormControlType::InputDatetimeLocal;
+ }
+
+ bool CreatesDateTimeWidget() const { return CreatesDateTimeWidget(mType); }
+
+ static bool MayFireChangeOnBlur(FormControlType aType) {
+ return IsSingleLineTextControl(false, aType) ||
+ CreatesDateTimeWidget(aType) ||
+ aType == FormControlType::InputRange ||
+ aType == FormControlType::InputNumber;
+ }
+
+ bool CheckActivationBehaviorPreconditions(EventChainVisitor& aVisitor) const;
+
+ /**
+ * Call MaybeDispatchPasswordEvent or MaybeDispatchUsernameEvent
+ * in order to dispatch LoginManager events.
+ */
+ void MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm);
+
+ /**
+ * Fire an event when the password input field is removed from the DOM tree.
+ * This is now only used by the password manager and formautofill.
+ */
+ void MaybeFireInputPasswordRemoved();
+
+ /**
+ * Checks if aDateTimeInputType should be supported.
+ */
+ static bool IsDateTimeTypeSupported(FormControlType);
+
+ /**
+ * The radio group container containing the group the element is a part of.
+ * This allows the element to only access a container it has been added to.
+ */
+ RadioGroupContainer* mRadioGroupContainer;
+
+ struct nsFilePickerFilter {
+ nsFilePickerFilter() : mFilterMask(0) {}
+
+ explicit nsFilePickerFilter(int32_t aFilterMask)
+ : mFilterMask(aFilterMask) {}
+
+ nsFilePickerFilter(const nsString& aTitle, const nsString& aFilter)
+ : mFilterMask(0), mTitle(aTitle), mFilter(aFilter) {}
+
+ nsFilePickerFilter(const nsFilePickerFilter& other) {
+ mFilterMask = other.mFilterMask;
+ mTitle = other.mTitle;
+ mFilter = other.mFilter;
+ }
+
+ bool operator==(const nsFilePickerFilter& other) const {
+ if ((mFilter == other.mFilter) && (mFilterMask == other.mFilterMask)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // Filter mask, using values defined in nsIFilePicker
+ int32_t mFilterMask;
+ // If mFilterMask is defined, mTitle and mFilter are useless and should be
+ // ignored
+ nsString mTitle;
+ nsString mFilter;
+ };
+
+ class nsFilePickerShownCallback : public nsIFilePickerShownCallback {
+ virtual ~nsFilePickerShownCallback() = default;
+
+ public:
+ nsFilePickerShownCallback(HTMLInputElement* aInput,
+ nsIFilePicker* aFilePicker);
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Done(nsIFilePicker::ResultCode aResult) override;
+
+ private:
+ nsCOMPtr<nsIFilePicker> mFilePicker;
+ const RefPtr<HTMLInputElement> mInput;
+ };
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/html/HTMLLIElement.cpp b/dom/html/HTMLLIElement.cpp
new file mode 100644
index 0000000000..fcaa120b03
--- /dev/null
+++ b/dom/html/HTMLLIElement.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLLIElement.h"
+#include "mozilla/dom/HTMLLIElementBinding.h"
+
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(LI)
+
+namespace mozilla::dom {
+
+HTMLLIElement::~HTMLLIElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLLIElement)
+
+// https://html.spec.whatwg.org/#lists
+const nsAttrValue::EnumTable HTMLLIElement::kULTypeTable[] = {
+ {"none", ListStyle::None},
+ {"disc", ListStyle::Disc},
+ {"circle", ListStyle::Circle},
+ {"square", ListStyle::Square},
+ {nullptr, 0}};
+
+// https://html.spec.whatwg.org/#lists
+const nsAttrValue::EnumTable HTMLLIElement::kOLTypeTable[] = {
+ {"A", ListStyle::UpperAlpha}, {"a", ListStyle::LowerAlpha},
+ {"I", ListStyle::UpperRoman}, {"i", ListStyle::LowerRoman},
+ {"1", ListStyle::Decimal}, {nullptr, 0}};
+
+bool HTMLLIElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::type) {
+ return aResult.ParseEnumValue(aValue, kOLTypeTable, true) ||
+ aResult.ParseEnumValue(aValue, kULTypeTable, false);
+ }
+ if (aAttribute == nsGkAtoms::value) {
+ return aResult.ParseIntValue(aValue);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLLIElement::MapAttributesIntoRule(MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty_list_style_type)) {
+ // type: enum
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::type);
+ if (value && value->Type() == nsAttrValue::eEnum) {
+ aBuilder.SetKeywordValue(eCSSProperty_list_style_type,
+ value->GetEnumValue());
+ }
+ }
+
+ // Map <li value=INTEGER> to 'counter-set: list-item INTEGER'.
+ const nsAttrValue* attrVal = aBuilder.GetAttr(nsGkAtoms::value);
+ if (attrVal && attrVal->Type() == nsAttrValue::eInteger) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty_counter_set)) {
+ aBuilder.SetCounterSetListItem(attrVal->GetIntegerValue());
+ }
+ }
+
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLLIElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::type},
+ {nsGkAtoms::value},
+ {nullptr},
+ };
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLLIElement::GetAttributeMappingFunction() const {
+ return &MapAttributesIntoRule;
+}
+
+JSObject* HTMLLIElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLLIElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLLIElement.h b/dom/html/HTMLLIElement.h
new file mode 100644
index 0000000000..e73a49107e
--- /dev/null
+++ b/dom/html/HTMLLIElement.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLLIElement_h
+#define mozilla_dom_HTMLLIElement_h
+
+#include "mozilla/Attributes.h"
+
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLLIElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLLIElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLLIElement, nsGenericHTMLElement)
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // WebIDL API
+ void GetType(DOMString& aType) { GetHTMLAttr(nsGkAtoms::type, aType); }
+ void SetType(const nsAString& aType, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::type, aType, rv);
+ }
+ int32_t Value() const { return GetIntAttr(nsGkAtoms::value, 0); }
+ void SetValue(int32_t aValue, mozilla::ErrorResult& rv) {
+ SetHTMLIntAttr(nsGkAtoms::value, aValue, rv);
+ }
+
+ static const nsAttrValue::EnumTable kULTypeTable[];
+ static const nsAttrValue::EnumTable kOLTypeTable[];
+
+ protected:
+ virtual ~HTMLLIElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLLIElement_h
diff --git a/dom/html/HTMLLabelElement.cpp b/dom/html/HTMLLabelElement.cpp
new file mode 100644
index 0000000000..32acbd06be
--- /dev/null
+++ b/dom/html/HTMLLabelElement.cpp
@@ -0,0 +1,246 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Implementation of HTML <label> elements.
+ */
+#include "HTMLLabelElement.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/dom/HTMLLabelElementBinding.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "nsFocusManager.h"
+#include "nsIFrame.h"
+#include "nsContentUtils.h"
+#include "nsQueryObject.h"
+#include "mozilla/dom/ShadowRoot.h"
+
+// construction, destruction
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Label)
+
+namespace mozilla::dom {
+
+HTMLLabelElement::~HTMLLabelElement() = default;
+
+JSObject* HTMLLabelElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLLabelElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// nsIDOMHTMLLabelElement
+
+NS_IMPL_ELEMENT_CLONE(HTMLLabelElement)
+
+HTMLFormElement* HTMLLabelElement::GetForm() const {
+ nsGenericHTMLElement* control = GetControl();
+ if (!control) {
+ return nullptr;
+ }
+
+ // Not all labeled things have a form association. Stick to the ones that do.
+ nsCOMPtr<nsIFormControl> formControl = do_QueryObject(control);
+ if (!formControl) {
+ return nullptr;
+ }
+
+ return formControl->GetForm();
+}
+
+void HTMLLabelElement::Focus(const FocusOptions& aOptions,
+ const CallerType aCallerType,
+ ErrorResult& aError) {
+ {
+ nsIFrame* frame = GetPrimaryFrame(FlushType::Frames);
+ if (frame && frame->IsFocusable()) {
+ return nsGenericHTMLElement::Focus(aOptions, aCallerType, aError);
+ }
+ }
+
+ if (RefPtr<Element> elem = GetLabeledElement()) {
+ return elem->Focus(aOptions, aCallerType, aError);
+ }
+}
+
+nsresult HTMLLabelElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
+ WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent();
+ if (mHandlingEvent ||
+ (!(mouseEvent && mouseEvent->IsLeftClickEvent()) &&
+ aVisitor.mEvent->mMessage != eMouseDown) ||
+ aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault ||
+ !aVisitor.mPresContext ||
+ // Don't handle the event if it's already been handled by another label
+ aVisitor.mEvent->mFlags.mMultipleActionsPrevented) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<Element> target =
+ do_QueryInterface(aVisitor.mEvent->GetOriginalDOMEventTarget());
+ if (nsContentUtils::IsInInteractiveHTMLContent(target, this)) {
+ return NS_OK;
+ }
+
+ // Strong ref because event dispatch is going to happen.
+ RefPtr<Element> content = GetLabeledElement();
+
+ if (!content || content->IsDisabled()) {
+ return NS_OK;
+ }
+
+ mHandlingEvent = true;
+ switch (aVisitor.mEvent->mMessage) {
+ case eMouseDown:
+ if (mouseEvent->mButton == MouseButton::ePrimary) {
+ // We reset the mouse-down point on every event because there is
+ // no guarantee we will reach the eMouseClick code below.
+ LayoutDeviceIntPoint* curPoint =
+ new LayoutDeviceIntPoint(mouseEvent->mRefPoint);
+ SetProperty(nsGkAtoms::labelMouseDownPtProperty,
+ static_cast<void*>(curPoint),
+ nsINode::DeleteProperty<LayoutDeviceIntPoint>);
+ }
+ break;
+
+ case eMouseClick:
+ if (mouseEvent->IsLeftClickEvent()) {
+ LayoutDeviceIntPoint* mouseDownPoint =
+ static_cast<LayoutDeviceIntPoint*>(
+ GetProperty(nsGkAtoms::labelMouseDownPtProperty));
+
+ bool dragSelect = false;
+ if (mouseDownPoint) {
+ LayoutDeviceIntPoint dragDistance = *mouseDownPoint;
+ RemoveProperty(nsGkAtoms::labelMouseDownPtProperty);
+
+ dragDistance -= mouseEvent->mRefPoint;
+ const int CLICK_DISTANCE = 2;
+ dragSelect = dragDistance.x > CLICK_DISTANCE ||
+ dragDistance.x < -CLICK_DISTANCE ||
+ dragDistance.y > CLICK_DISTANCE ||
+ dragDistance.y < -CLICK_DISTANCE;
+ }
+ // Don't click the for-content if we did drag-select text or if we
+ // have a kbd modifier (which adjusts a selection).
+ if (dragSelect || mouseEvent->IsShift() || mouseEvent->IsControl() ||
+ mouseEvent->IsAlt() || mouseEvent->IsMeta()) {
+ break;
+ }
+ // Only set focus on the first click of multiple clicks to prevent
+ // to prevent immediate de-focus.
+ if (mouseEvent->mClickCount <= 1) {
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ // Use FLAG_BYMOVEFOCUS here so that the label is scrolled to.
+ // Also, within HTMLInputElement::PostHandleEvent, inputs will
+ // be selected only when focused via a key or when the navigation
+ // flag is used and we want to select the text on label clicks as
+ // well.
+ // If the label has been clicked by the user, we also want to
+ // pass FLAG_BYMOUSE so that we get correct focus ring behavior,
+ // but we don't want to pass FLAG_BYMOUSE if this click event was
+ // caused by the user pressing an accesskey.
+ bool byMouse = (mouseEvent->mInputSource !=
+ MouseEvent_Binding::MOZ_SOURCE_KEYBOARD);
+ bool byTouch = (mouseEvent->mInputSource ==
+ MouseEvent_Binding::MOZ_SOURCE_TOUCH);
+ fm->SetFocus(content,
+ nsIFocusManager::FLAG_BYMOVEFOCUS |
+ (byMouse ? nsIFocusManager::FLAG_BYMOUSE : 0) |
+ (byTouch ? nsIFocusManager::FLAG_BYTOUCH : 0));
+ }
+ }
+ // Dispatch a new click event to |content|
+ // (For compatibility with IE, we do only left click. If
+ // we wanted to interpret the HTML spec very narrowly, we
+ // would do nothing. If we wanted to do something
+ // sensible, we might send more events through like
+ // this.) See bug 7554, bug 49897, and bug 96813.
+ nsEventStatus status = aVisitor.mEventStatus;
+ // Ok to use aVisitor.mEvent as parameter because DispatchClickEvent
+ // will actually create a new event.
+ EventFlags eventFlags;
+ eventFlags.mMultipleActionsPrevented = true;
+ DispatchClickEvent(aVisitor.mPresContext, mouseEvent, content, false,
+ &eventFlags, &status);
+ // Do we care about the status this returned? I don't think we do...
+ // Don't run another <label> off of this click
+ mouseEvent->mFlags.mMultipleActionsPrevented = true;
+ }
+ break;
+
+ default:
+ break;
+ }
+ mHandlingEvent = false;
+ return NS_OK;
+}
+
+Result<bool, nsresult> HTMLLabelElement::PerformAccesskey(
+ bool aKeyCausesActivation, bool aIsTrustedEvent) {
+ if (!aKeyCausesActivation) {
+ RefPtr<Element> element = GetLabeledElement();
+ if (element) {
+ return element->PerformAccesskey(aKeyCausesActivation, aIsTrustedEvent);
+ }
+ return Err(NS_ERROR_ABORT);
+ }
+
+ RefPtr<nsPresContext> presContext = GetPresContext(eForUncomposedDoc);
+ if (!presContext) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ // Click on it if the users prefs indicate to do so.
+ AutoHandlingUserInputStatePusher userInputStatePusher(aIsTrustedEvent);
+ AutoPopupStatePusher popupStatePusher(
+ aIsTrustedEvent ? PopupBlocker::openAllowed : PopupBlocker::openAbused);
+ DispatchSimulatedClick(this, aIsTrustedEvent, presContext);
+
+ // XXXedgar, do we need to check whether the focus is really changed?
+ return true;
+}
+
+nsGenericHTMLElement* HTMLLabelElement::GetLabeledElement() const {
+ nsAutoString elementId;
+
+ if (!GetAttr(nsGkAtoms::_for, elementId)) {
+ // No @for, so we are a label for our first form control element.
+ // Do a depth-first traversal to look for the first form control element.
+ return GetFirstLabelableDescendant();
+ }
+
+ // We have a @for. The id has to be linked to an element in the same tree
+ // and this element should be a labelable form control.
+ Element* element = nullptr;
+
+ if (ShadowRoot* shadowRoot = GetContainingShadow()) {
+ element = shadowRoot->GetElementById(elementId);
+ } else if (Document* doc = GetUncomposedDoc()) {
+ element = doc->GetElementById(elementId);
+ } else {
+ element =
+ nsContentUtils::MatchElementId(SubtreeRoot()->AsContent(), elementId);
+ }
+
+ if (element && element->IsLabelable()) {
+ return static_cast<nsGenericHTMLElement*>(element);
+ }
+
+ return nullptr;
+}
+
+nsGenericHTMLElement* HTMLLabelElement::GetFirstLabelableDescendant() const {
+ for (nsIContent* cur = nsINode::GetFirstChild(); cur;
+ cur = cur->GetNextNode(this)) {
+ Element* element = Element::FromNode(cur);
+ if (element && element->IsLabelable()) {
+ return static_cast<nsGenericHTMLElement*>(element);
+ }
+ }
+
+ return nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLLabelElement.h b/dom/html/HTMLLabelElement.h
new file mode 100644
index 0000000000..f6c574cebe
--- /dev/null
+++ b/dom/html/HTMLLabelElement.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Declaration of HTML <label> elements.
+ */
+#ifndef HTMLLabelElement_h
+#define HTMLLabelElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla {
+class EventChainPostVisitor;
+namespace dom {
+
+class HTMLLabelElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLLabelElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)), mHandlingEvent(false) {}
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLLabelElement, label)
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLLabelElement, nsGenericHTMLElement)
+
+ // Element
+ virtual bool IsInteractiveHTMLContent() const override { return true; }
+
+ HTMLFormElement* GetForm() const;
+ void GetHtmlFor(nsString& aHtmlFor) {
+ GetHTMLAttr(nsGkAtoms::_for, aHtmlFor);
+ }
+ void SetHtmlFor(const nsAString& aHtmlFor) {
+ SetHTMLAttr(nsGkAtoms::_for, aHtmlFor);
+ }
+ nsGenericHTMLElement* GetControl() const { return GetLabeledElement(); }
+
+ using nsGenericHTMLElement::Focus;
+ virtual void Focus(const FocusOptions& aOptions,
+ const mozilla::dom::CallerType aCallerType,
+ ErrorResult& aError) override;
+
+ // nsIContent
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+ MOZ_CAN_RUN_SCRIPT
+ virtual Result<bool, nsresult> PerformAccesskey(
+ bool aKeyCausesActivation, bool aIsTrustedEvent) override;
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ nsGenericHTMLElement* GetLabeledElement() const;
+
+ protected:
+ virtual ~HTMLLabelElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsGenericHTMLElement* GetFirstLabelableDescendant() const;
+
+ // XXX It would be nice if we could use an event flag instead.
+ bool mHandlingEvent;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* HTMLLabelElement_h */
diff --git a/dom/html/HTMLLegendElement.cpp b/dom/html/HTMLLegendElement.cpp
new file mode 100644
index 0000000000..2f110dd6ce
--- /dev/null
+++ b/dom/html/HTMLLegendElement.cpp
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLLegendElement.h"
+#include "mozilla/dom/ElementBinding.h"
+#include "mozilla/dom/HTMLLegendElementBinding.h"
+#include "nsFocusManager.h"
+#include "nsIFrame.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Legend)
+
+namespace mozilla::dom {
+
+HTMLLegendElement::~HTMLLegendElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLLegendElement)
+
+nsIContent* HTMLLegendElement::GetFieldSet() const {
+ nsIContent* parent = GetParent();
+
+ if (parent && parent->IsHTMLElement(nsGkAtoms::fieldset)) {
+ return parent;
+ }
+
+ return nullptr;
+}
+
+bool HTMLLegendElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ // this contains center, because IE4 does
+ static const nsAttrValue::EnumTable kAlignTable[] = {
+ {"left", LegendAlignValue::Left},
+ {"right", LegendAlignValue::Right},
+ {"center", LegendAlignValue::Center},
+ {nullptr, 0}};
+
+ if (aAttribute == nsGkAtoms::align && aNamespaceID == kNameSpaceID_None) {
+ return aResult.ParseEnumValue(aValue, kAlignTable, false);
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+nsChangeHint HTMLLegendElement::GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const {
+ nsChangeHint retval =
+ nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType);
+ if (aAttribute == nsGkAtoms::align) {
+ retval |= NS_STYLE_HINT_REFLOW;
+ }
+ return retval;
+}
+
+nsresult HTMLLegendElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ return nsGenericHTMLElement::BindToTree(aContext, aParent);
+}
+
+void HTMLLegendElement::UnbindFromTree(bool aNullParent) {
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+void HTMLLegendElement::Focus(const FocusOptions& aOptions,
+ const CallerType aCallerType,
+ ErrorResult& aError) {
+ nsIFrame* frame = GetPrimaryFrame();
+ if (!frame) {
+ return;
+ }
+
+ if (frame->IsFocusable()) {
+ nsGenericHTMLElement::Focus(aOptions, aCallerType, aError);
+ return;
+ }
+
+ // If the legend isn't focusable, focus whatever is focusable following
+ // the legend instead, bug 81481.
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (!fm) {
+ return;
+ }
+
+ RefPtr<Element> result;
+ aError = fm->MoveFocus(nullptr, this, nsIFocusManager::MOVEFOCUS_FORWARD,
+ nsIFocusManager::FLAG_NOPARENTFRAME |
+ nsFocusManager::ProgrammaticFocusFlags(aOptions),
+ getter_AddRefs(result));
+}
+
+Result<bool, nsresult> HTMLLegendElement::PerformAccesskey(
+ bool aKeyCausesActivation, bool aIsTrustedEvent) {
+ FocusOptions options;
+ ErrorResult rv;
+
+ Focus(options, CallerType::System, rv);
+ if (rv.Failed()) {
+ return Err(rv.StealNSResult());
+ }
+
+ // XXXedgar, do we need to check whether the focus is really changed?
+ return true;
+}
+
+HTMLLegendElement::LegendAlignValue HTMLLegendElement::LogicalAlign(
+ mozilla::WritingMode aCBWM) const {
+ const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::align);
+ if (!attr || attr->Type() != nsAttrValue::eEnum) {
+ return LegendAlignValue::InlineStart;
+ }
+
+ auto value = static_cast<LegendAlignValue>(attr->GetEnumValue());
+ switch (value) {
+ case LegendAlignValue::Left:
+ return aCBWM.IsBidiLTR() ? LegendAlignValue::InlineStart
+ : LegendAlignValue::InlineEnd;
+ case LegendAlignValue::Right:
+ return aCBWM.IsBidiLTR() ? LegendAlignValue::InlineEnd
+ : LegendAlignValue::InlineStart;
+ default:
+ return value;
+ }
+}
+
+HTMLFormElement* HTMLLegendElement::GetForm() const {
+ nsCOMPtr<nsIFormControl> fieldsetControl = do_QueryInterface(GetFieldSet());
+ return fieldsetControl ? fieldsetControl->GetForm() : nullptr;
+}
+
+JSObject* HTMLLegendElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLLegendElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLLegendElement.h b/dom/html/HTMLLegendElement.h
new file mode 100644
index 0000000000..cccab9239b
--- /dev/null
+++ b/dom/html/HTMLLegendElement.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLLegendElement_h
+#define mozilla_dom_HTMLLegendElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "mozilla/dom/HTMLFormElement.h"
+
+namespace mozilla::dom {
+
+class HTMLLegendElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLLegendElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLLegendElement, legend)
+
+ using nsGenericHTMLElement::Focus;
+ virtual void Focus(const FocusOptions& aOptions,
+ const mozilla::dom::CallerType aCallerType,
+ ErrorResult& aError) override;
+
+ virtual Result<bool, nsresult> PerformAccesskey(
+ bool aKeyCausesActivation, bool aIsTrustedEvent) override;
+
+ // nsIContent
+ virtual nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ virtual void UnbindFromTree(bool aNullParent = true) override;
+ virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ virtual nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ enum class LegendAlignValue : uint8_t {
+ Left,
+ Right,
+ Center,
+ Bottom,
+ Top,
+ InlineStart,
+ InlineEnd,
+ };
+
+ /**
+ * Return the align value to use for the given fieldset writing-mode.
+ * (This method resolves Left/Right to the appropriate InlineStart/InlineEnd).
+ * @param aCBWM the fieldset writing-mode
+ * @note we only parse left/right/center, so this method returns Center,
+ * InlineStart or InlineEnd.
+ */
+ LegendAlignValue LogicalAlign(mozilla::WritingMode aCBWM) const;
+
+ /**
+ * WebIDL Interface
+ */
+
+ HTMLFormElement* GetForm() const;
+
+ void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); }
+
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+
+ nsINode* GetScopeChainParent() const override {
+ Element* form = GetForm();
+ return form ? form : nsGenericHTMLElement::GetScopeChainParent();
+ }
+
+ protected:
+ virtual ~HTMLLegendElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ /**
+ * Get the fieldset content element that contains this legend.
+ * Returns null if there is no fieldset containing this legend.
+ */
+ nsIContent* GetFieldSet() const;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLLegendElement_h */
diff --git a/dom/html/HTMLLinkElement.cpp b/dom/html/HTMLLinkElement.cpp
new file mode 100644
index 0000000000..53c2d86d8d
--- /dev/null
+++ b/dom/html/HTMLLinkElement.cpp
@@ -0,0 +1,706 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLLinkElement.h"
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Components.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/HTMLLinkElementBinding.h"
+#include "mozilla/dom/HTMLDNSPrefetch.h"
+#include "mozilla/dom/ReferrerInfo.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "nsContentUtils.h"
+#include "nsDOMTokenList.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "nsIContentInlines.h"
+#include "mozilla/dom/Document.h"
+#include "nsINode.h"
+#include "nsIPrefetchService.h"
+#include "nsISizeOf.h"
+#include "nsPIDOMWindow.h"
+#include "nsReadableUtils.h"
+#include "nsStyleConsts.h"
+#include "nsUnicharUtils.h"
+#include "nsWindowSizes.h"
+#include "nsIContentPolicy.h"
+#include "nsMimeTypes.h"
+#include "imgLoader.h"
+#include "MediaContainerType.h"
+#include "DecoderDoctorDiagnostics.h"
+#include "DecoderTraits.h"
+#include "MediaList.h"
+#include "nsAttrValueInlines.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Link)
+
+namespace mozilla::dom {
+
+HTMLLinkElement::HTMLLinkElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLLinkElement::~HTMLLinkElement() { SupportsDNSPrefetch::Destroyed(*this); }
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLLinkElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLLinkElement,
+ nsGenericHTMLElement)
+ tmp->LinkStyle::Traverse(cb);
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelList)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSizes)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlocking)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLLinkElement,
+ nsGenericHTMLElement)
+ tmp->LinkStyle::Unlink();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mRelList)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSizes)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlocking)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLLinkElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLLinkElement)
+
+bool HTMLLinkElement::Disabled() const {
+ return GetBoolAttr(nsGkAtoms::disabled);
+}
+
+void HTMLLinkElement::SetDisabled(bool aDisabled, ErrorResult& aRv) {
+ return SetHTMLBoolAttr(nsGkAtoms::disabled, aDisabled, aRv);
+}
+
+nsresult HTMLLinkElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsInComposedDoc()) {
+ TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender();
+ }
+
+ LinkStyle::BindToTree();
+
+ if (IsInUncomposedDoc()) {
+ if (AttrValueIs(kNameSpaceID_None, nsGkAtoms::rel, nsGkAtoms::localization,
+ eIgnoreCase)) {
+ aContext.OwnerDoc().LocalizationLinkAdded(this);
+ }
+
+ LinkAdded();
+ }
+
+ return rv;
+}
+
+void HTMLLinkElement::LinkAdded() {
+ CreateAndDispatchEvent(u"DOMLinkAdded"_ns);
+}
+
+void HTMLLinkElement::UnbindFromTree(bool aNullParent) {
+ CancelDNSPrefetch(*this);
+ CancelPrefetchOrPreload();
+
+ // If this is reinserted back into the document it will not be
+ // from the parser.
+ Document* oldDoc = GetUncomposedDoc();
+ ShadowRoot* oldShadowRoot = GetContainingShadow();
+
+ // We want to update the localization but only if the link is removed from a
+ // DOM change, and not because the document is going away.
+ bool ignore;
+ if (oldDoc) {
+ if (oldDoc->GetScriptHandlingObject(ignore) &&
+ AttrValueIs(kNameSpaceID_None, nsGkAtoms::rel, nsGkAtoms::localization,
+ eIgnoreCase)) {
+ oldDoc->LocalizationLinkRemoved(this);
+ }
+ }
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+
+ Unused << UpdateStyleSheetInternal(oldDoc, oldShadowRoot);
+}
+
+bool HTMLLinkElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::crossorigin) {
+ ParseCORSValue(aValue, aResult);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::as) {
+ net::ParseAsValue(aValue, aResult);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::sizes) {
+ aResult.ParseAtomArray(aValue);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::integrity) {
+ aResult.ParseStringOrAtom(aValue);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::fetchpriority) {
+ ParseFetchPriority(aValue, aResult);
+ return true;
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLLinkElement::CreateAndDispatchEvent(const nsAString& aEventName) {
+ MOZ_ASSERT(IsInUncomposedDoc());
+
+ // In the unlikely case that both rev is specified *and* rel=stylesheet,
+ // this code will cause the event to fire, on the principle that maybe the
+ // page really does want to specify that its author is a stylesheet. Since
+ // this should never actually happen and the performance hit is minimal,
+ // doing the "right" thing costs virtually nothing here, even if it doesn't
+ // make much sense.
+ static AttrArray::AttrValuesArray strings[] = {
+ nsGkAtoms::_empty, nsGkAtoms::stylesheet, nullptr};
+
+ if (!nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None,
+ nsGkAtoms::rev) &&
+ FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::rel, strings,
+ eIgnoreCase) != AttrArray::ATTR_VALUE_NO_MATCH) {
+ return;
+ }
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(
+ this, aEventName, CanBubble::eYes, ChromeOnlyDispatch::eYes);
+ // Always run async in order to avoid running script when the content
+ // sink isn't expecting it.
+ asyncDispatcher->PostDOMEvent();
+}
+
+void HTMLLinkElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None &&
+ (aName == nsGkAtoms::href || aName == nsGkAtoms::rel)) {
+ CancelDNSPrefetch(*this);
+ CancelPrefetchOrPreload();
+ }
+
+ return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue,
+ aNotify);
+}
+
+void HTMLLinkElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::href) {
+ mCachedURI = nullptr;
+ if (IsInUncomposedDoc()) {
+ CreateAndDispatchEvent(u"DOMLinkChanged"_ns);
+ }
+ mTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue ? aValue->GetStringValue() : EmptyString(),
+ aSubjectPrincipal);
+
+ // If the link has `rel=localization` and its `href` attribute is changed,
+ // update the list of localization links.
+ if (AttrValueIs(kNameSpaceID_None, nsGkAtoms::rel, nsGkAtoms::localization,
+ eIgnoreCase)) {
+ if (Document* doc = GetUncomposedDoc()) {
+ if (aOldValue) {
+ doc->LocalizationLinkRemoved(this);
+ }
+ if (aValue) {
+ doc->LocalizationLinkAdded(this);
+ }
+ }
+ }
+ }
+
+ // If a link's `rel` attribute was changed from or to `localization`,
+ // update the list of localization links.
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::rel) {
+ if (Document* doc = GetUncomposedDoc()) {
+ if ((aValue && aValue->Equals(nsGkAtoms::localization, eIgnoreCase)) &&
+ (!aOldValue ||
+ !aOldValue->Equals(nsGkAtoms::localization, eIgnoreCase))) {
+ doc->LocalizationLinkAdded(this);
+ } else if ((aOldValue &&
+ aOldValue->Equals(nsGkAtoms::localization, eIgnoreCase)) &&
+ (!aValue ||
+ !aValue->Equals(nsGkAtoms::localization, eIgnoreCase))) {
+ doc->LocalizationLinkRemoved(this);
+ }
+ }
+ }
+
+ if (aValue) {
+ if (aNameSpaceID == kNameSpaceID_None &&
+ (aName == nsGkAtoms::href || aName == nsGkAtoms::rel ||
+ aName == nsGkAtoms::title || aName == nsGkAtoms::media ||
+ aName == nsGkAtoms::type || aName == nsGkAtoms::as ||
+ aName == nsGkAtoms::crossorigin || aName == nsGkAtoms::disabled)) {
+ bool dropSheet = false;
+ if (aName == nsGkAtoms::rel) {
+ nsAutoString value;
+ aValue->ToString(value);
+ uint32_t linkTypes = ParseLinkTypes(value);
+ if (GetSheet()) {
+ dropSheet = !(linkTypes & eSTYLESHEET);
+ }
+ }
+
+ if ((aName == nsGkAtoms::rel || aName == nsGkAtoms::href) &&
+ IsInComposedDoc()) {
+ TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender();
+ }
+
+ if ((aName == nsGkAtoms::as || aName == nsGkAtoms::type ||
+ aName == nsGkAtoms::crossorigin || aName == nsGkAtoms::media) &&
+ IsInComposedDoc()) {
+ UpdatePreload(aName, aValue, aOldValue);
+ }
+
+ const bool forceUpdate =
+ dropSheet || aName == nsGkAtoms::title || aName == nsGkAtoms::media ||
+ aName == nsGkAtoms::type || aName == nsGkAtoms::disabled;
+
+ Unused << UpdateStyleSheetInternal(
+ nullptr, nullptr, forceUpdate ? ForceUpdate::Yes : ForceUpdate::No);
+ }
+ } else {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::disabled) {
+ mExplicitlyEnabled = true;
+ }
+ // Since removing href or rel makes us no longer link to a stylesheet,
+ // force updates for those too.
+ if (aName == nsGkAtoms::href || aName == nsGkAtoms::rel ||
+ aName == nsGkAtoms::title || aName == nsGkAtoms::media ||
+ aName == nsGkAtoms::type || aName == nsGkAtoms::disabled) {
+ Unused << UpdateStyleSheetInternal(nullptr, nullptr, ForceUpdate::Yes);
+ }
+ if ((aName == nsGkAtoms::as || aName == nsGkAtoms::type ||
+ aName == nsGkAtoms::crossorigin || aName == nsGkAtoms::media) &&
+ IsInComposedDoc()) {
+ UpdatePreload(aName, aValue, aOldValue);
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+// Keep this and the arrays below in sync with ToLinkMask in LinkStyle.cpp.
+#define SUPPORTED_REL_VALUES_BASE \
+ "preload", "prefetch", "dns-prefetch", "stylesheet", "next", "alternate", \
+ "preconnect", "icon", "search", nullptr
+
+static const DOMTokenListSupportedToken sSupportedRelValueCombinations[][12] = {
+ {SUPPORTED_REL_VALUES_BASE},
+ {"manifest", SUPPORTED_REL_VALUES_BASE},
+ {"modulepreload", SUPPORTED_REL_VALUES_BASE},
+ {"modulepreload", "manifest", SUPPORTED_REL_VALUES_BASE}};
+#undef SUPPORTED_REL_VALUES_BASE
+
+nsDOMTokenList* HTMLLinkElement::RelList() {
+ if (!mRelList) {
+ int index = (StaticPrefs::dom_manifest_enabled() ? 1 : 0) |
+ (StaticPrefs::network_modulepreload() ? 2 : 0);
+
+ mRelList = new nsDOMTokenList(this, nsGkAtoms::rel,
+ sSupportedRelValueCombinations[index]);
+ }
+ return mRelList;
+}
+
+Maybe<LinkStyle::SheetInfo> HTMLLinkElement::GetStyleSheetInfo() {
+ nsAutoString rel;
+ GetAttr(nsGkAtoms::rel, rel);
+ uint32_t linkTypes = ParseLinkTypes(rel);
+ if (!(linkTypes & eSTYLESHEET)) {
+ return Nothing();
+ }
+
+ if (!IsCSSMimeTypeAttributeForLinkElement(*this)) {
+ return Nothing();
+ }
+
+ if (Disabled()) {
+ return Nothing();
+ }
+
+ nsAutoString title;
+ nsAutoString media;
+ GetTitleAndMediaForElement(*this, title, media);
+
+ bool alternate = linkTypes & eALTERNATE;
+ if (alternate && title.IsEmpty()) {
+ // alternates must have title.
+ return Nothing();
+ }
+
+ if (!HasNonEmptyAttr(nsGkAtoms::href)) {
+ return Nothing();
+ }
+
+ nsAutoString integrity;
+ GetAttr(nsGkAtoms::integrity, integrity);
+
+ nsCOMPtr<nsIURI> uri = GetURI();
+ nsCOMPtr<nsIPrincipal> prin = mTriggeringPrincipal;
+
+ nsAutoString nonce;
+ nsString* cspNonce = static_cast<nsString*>(GetProperty(nsGkAtoms::nonce));
+ if (cspNonce) {
+ nonce = *cspNonce;
+ }
+
+ return Some(SheetInfo{
+ *OwnerDoc(),
+ this,
+ uri.forget(),
+ prin.forget(),
+ MakeAndAddRef<ReferrerInfo>(*this),
+ GetCORSMode(),
+ title,
+ media,
+ integrity,
+ nonce,
+ alternate ? HasAlternateRel::Yes : HasAlternateRel::No,
+ IsInline::No,
+ mExplicitlyEnabled ? IsExplicitlyEnabled::Yes : IsExplicitlyEnabled::No,
+ GetFetchPriority(),
+ });
+}
+
+void HTMLLinkElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes,
+ size_t* aNodeSize) const {
+ nsGenericHTMLElement::AddSizeOfExcludingThis(aSizes, aNodeSize);
+ if (nsCOMPtr<nsISizeOf> iface = do_QueryInterface(mCachedURI)) {
+ *aNodeSize += iface->SizeOfExcludingThis(aSizes.mState.mMallocSizeOf);
+ }
+}
+
+JSObject* HTMLLinkElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLLinkElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void HTMLLinkElement::GetAs(nsAString& aResult) {
+ GetEnumAttr(nsGkAtoms::as, "", aResult);
+}
+
+void HTMLLinkElement::GetContentPolicyMimeTypeMedia(
+ nsAttrValue& aAsAttr, nsContentPolicyType& aPolicyType, nsString& aMimeType,
+ nsAString& aMedia) {
+ nsAutoString as;
+ GetAttr(nsGkAtoms::as, as);
+ net::ParseAsValue(as, aAsAttr);
+ aPolicyType = net::AsValueToContentPolicy(aAsAttr);
+
+ nsAutoString type;
+ GetAttr(nsGkAtoms::type, type);
+ nsAutoString notUsed;
+ nsContentUtils::SplitMimeType(type, aMimeType, notUsed);
+
+ GetAttr(nsGkAtoms::media, aMedia);
+}
+
+void HTMLLinkElement::
+ TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender() {
+ MOZ_ASSERT(IsInComposedDoc());
+ if (!HasAttr(nsGkAtoms::href)) {
+ return;
+ }
+
+ nsAutoString rel;
+ if (!GetAttr(nsGkAtoms::rel, rel)) {
+ return;
+ }
+
+ if (!nsContentUtils::PrefetchPreloadEnabled(OwnerDoc()->GetDocShell())) {
+ return;
+ }
+
+ uint32_t linkTypes = ParseLinkTypes(rel);
+
+ if ((linkTypes & ePREFETCH) || (linkTypes & eNEXT)) {
+ nsCOMPtr<nsIPrefetchService> prefetchService(
+ components::Prefetch::Service());
+ if (prefetchService) {
+ if (nsCOMPtr<nsIURI> uri = GetURI()) {
+ auto referrerInfo = MakeRefPtr<ReferrerInfo>(*this);
+ prefetchService->PrefetchURI(uri, referrerInfo, this,
+ linkTypes & ePREFETCH);
+ return;
+ }
+ }
+ }
+
+ if (linkTypes & ePRELOAD) {
+ if (nsCOMPtr<nsIURI> uri = GetURI()) {
+ nsContentPolicyType policyType;
+
+ nsAttrValue asAttr;
+ nsAutoString mimeType;
+ nsAutoString media;
+ GetContentPolicyMimeTypeMedia(asAttr, policyType, mimeType, media);
+
+ if (policyType == nsIContentPolicy::TYPE_INVALID ||
+ !net::CheckPreloadAttrs(asAttr, mimeType, media, OwnerDoc())) {
+ // Ignore preload with a wrong or empty as attribute.
+ net::WarnIgnoredPreload(*OwnerDoc(), *uri);
+ return;
+ }
+
+ StartPreload(policyType);
+ return;
+ }
+ }
+
+ if (linkTypes & eMODULE_PRELOAD) {
+ ScriptLoader* scriptLoader = OwnerDoc()->ScriptLoader();
+ ModuleLoader* moduleLoader = scriptLoader->GetModuleLoader();
+
+ if (!moduleLoader) {
+ // For the print preview documents, at this moment it doesn't have module
+ // loader yet, as the (print preview) document is not attached to the
+ // nsIDocumentViewer yet, so it doesn't have the GlobalObject.
+ // Also, the script elements won't be processed as they are also cloned
+ // from the original document.
+ // So we simply bail out if the module loader is null.
+ return;
+ }
+
+ if (!StaticPrefs::network_modulepreload()) {
+ // Keep behavior from https://phabricator.services.mozilla.com/D149371,
+ // prior to main implementation of modulepreload
+ moduleLoader->DisallowImportMaps();
+ return;
+ }
+
+ // https://html.spec.whatwg.org/multipage/semantics.html#processing-the-media-attribute
+ // TODO: apply this check for all linkTypes
+ nsAutoString media;
+ if (GetAttr(nsGkAtoms::media, media)) {
+ RefPtr<mozilla::dom::MediaList> mediaList =
+ mozilla::dom::MediaList::Create(NS_ConvertUTF16toUTF8(media));
+ if (!mediaList->Matches(*OwnerDoc())) {
+ return;
+ }
+ }
+
+ // TODO: per spec, apply this check for ePREFETCH as well
+ if (!HasNonEmptyAttr(nsGkAtoms::href)) {
+ return;
+ }
+
+ nsAutoString as;
+ GetAttr(nsGkAtoms::as, as);
+
+ if (!net::IsScriptLikeOrInvalid(as)) {
+ RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(
+ this, u"error"_ns, CanBubble::eNo, ChromeOnlyDispatch::eNo);
+ asyncDispatcher->PostDOMEvent();
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri = GetURI();
+ if (!uri) {
+ return;
+ }
+
+ // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-modulepreload-module-script-graph
+ // Step 1. Disallow further import maps given settings object.
+ moduleLoader->DisallowImportMaps();
+
+ StartPreload(nsIContentPolicy::TYPE_SCRIPT);
+ return;
+ }
+
+ if (linkTypes & ePRECONNECT) {
+ if (nsCOMPtr<nsIURI> uri = GetURI()) {
+ OwnerDoc()->MaybePreconnect(
+ uri, AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin)));
+ return;
+ }
+ }
+
+ if (linkTypes & eDNS_PREFETCH) {
+ TryDNSPrefetch(*this);
+ }
+}
+
+void HTMLLinkElement::UpdatePreload(nsAtom* aName, const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue) {
+ MOZ_ASSERT(IsInComposedDoc());
+
+ if (!HasAttr(nsGkAtoms::href)) {
+ return;
+ }
+
+ nsAutoString rel;
+ if (!GetAttr(nsGkAtoms::rel, rel)) {
+ return;
+ }
+
+ if (!nsContentUtils::PrefetchPreloadEnabled(OwnerDoc()->GetDocShell())) {
+ return;
+ }
+
+ uint32_t linkTypes = ParseLinkTypes(rel);
+
+ if (!(linkTypes & ePRELOAD)) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri = GetURI();
+ if (!uri) {
+ return;
+ }
+
+ nsAttrValue asAttr;
+ nsContentPolicyType asPolicyType;
+ nsAutoString mimeType;
+ nsAutoString media;
+ GetContentPolicyMimeTypeMedia(asAttr, asPolicyType, mimeType, media);
+
+ if (asPolicyType == nsIContentPolicy::TYPE_INVALID ||
+ !net::CheckPreloadAttrs(asAttr, mimeType, media, OwnerDoc())) {
+ // Ignore preload with a wrong or empty as attribute, but be sure to cancel
+ // the old one.
+ CancelPrefetchOrPreload();
+ net::WarnIgnoredPreload(*OwnerDoc(), *uri);
+ return;
+ }
+
+ if (aName == nsGkAtoms::crossorigin) {
+ CORSMode corsMode = AttrValueToCORSMode(aValue);
+ CORSMode oldCorsMode = AttrValueToCORSMode(aOldValue);
+ if (corsMode != oldCorsMode) {
+ CancelPrefetchOrPreload();
+ StartPreload(asPolicyType);
+ }
+ return;
+ }
+
+ nsContentPolicyType oldPolicyType;
+
+ if (aName == nsGkAtoms::as) {
+ if (aOldValue) {
+ oldPolicyType = net::AsValueToContentPolicy(*aOldValue);
+ if (!net::CheckPreloadAttrs(*aOldValue, mimeType, media, OwnerDoc())) {
+ oldPolicyType = nsIContentPolicy::TYPE_INVALID;
+ }
+ } else {
+ oldPolicyType = nsIContentPolicy::TYPE_INVALID;
+ }
+ } else if (aName == nsGkAtoms::type) {
+ nsAutoString oldType;
+ nsAutoString notUsed;
+ if (aOldValue) {
+ aOldValue->ToString(oldType);
+ }
+ nsAutoString oldMimeType;
+ nsContentUtils::SplitMimeType(oldType, oldMimeType, notUsed);
+ if (net::CheckPreloadAttrs(asAttr, oldMimeType, media, OwnerDoc())) {
+ oldPolicyType = asPolicyType;
+ } else {
+ oldPolicyType = nsIContentPolicy::TYPE_INVALID;
+ }
+ } else {
+ MOZ_ASSERT(aName == nsGkAtoms::media);
+ nsAutoString oldMedia;
+ if (aOldValue) {
+ aOldValue->ToString(oldMedia);
+ }
+ if (net::CheckPreloadAttrs(asAttr, mimeType, oldMedia, OwnerDoc())) {
+ oldPolicyType = asPolicyType;
+ } else {
+ oldPolicyType = nsIContentPolicy::TYPE_INVALID;
+ }
+ }
+
+ if (asPolicyType != oldPolicyType &&
+ oldPolicyType != nsIContentPolicy::TYPE_INVALID) {
+ CancelPrefetchOrPreload();
+ }
+
+ // Trigger a new preload if the policy type has changed.
+ if (asPolicyType != oldPolicyType) {
+ StartPreload(asPolicyType);
+ }
+}
+
+void HTMLLinkElement::CancelPrefetchOrPreload() {
+ CancelPreload();
+
+ nsCOMPtr<nsIPrefetchService> prefetchService(components::Prefetch::Service());
+ if (prefetchService) {
+ if (nsCOMPtr<nsIURI> uri = GetURI()) {
+ prefetchService->CancelPrefetchPreloadURI(uri, this);
+ }
+ }
+}
+
+void HTMLLinkElement::StartPreload(nsContentPolicyType aPolicyType) {
+ MOZ_ASSERT(!mPreload, "Forgot to cancel the running preload");
+ RefPtr<PreloaderBase> preload =
+ OwnerDoc()->Preloads().PreloadLinkElement(this, aPolicyType);
+ mPreload = preload.get();
+}
+
+void HTMLLinkElement::CancelPreload() {
+ if (mPreload) {
+ // This will cancel the loading channel if this was the last referred node
+ // and the preload is not used up until now to satisfy a regular tag load
+ // request.
+ mPreload->RemoveLinkPreloadNode(this);
+ mPreload = nullptr;
+ }
+}
+
+bool HTMLLinkElement::IsCSSMimeTypeAttributeForLinkElement(
+ const Element& aSelf) {
+ // Processing the type attribute per
+ // https://html.spec.whatwg.org/multipage/semantics.html#processing-the-type-attribute
+ // for HTML link elements.
+ nsAutoString type;
+ nsAutoString mimeType;
+ nsAutoString notUsed;
+ aSelf.GetAttr(nsGkAtoms::type, type);
+ nsContentUtils::SplitMimeType(type, mimeType, notUsed);
+ return mimeType.IsEmpty() || mimeType.LowerCaseEqualsLiteral("text/css");
+}
+
+nsDOMTokenList* HTMLLinkElement::Blocking() {
+ if (!mBlocking) {
+ mBlocking =
+ new nsDOMTokenList(this, nsGkAtoms::blocking, sSupportedBlockingValues);
+ }
+ return mBlocking;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLLinkElement.h b/dom/html/HTMLLinkElement.h
new file mode 100644
index 0000000000..26683aae7b
--- /dev/null
+++ b/dom/html/HTMLLinkElement.h
@@ -0,0 +1,223 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLLinkElement_h
+#define mozilla_dom_HTMLLinkElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/HTMLDNSPrefetch.h"
+#include "mozilla/dom/LinkStyle.h"
+#include "mozilla/dom/Link.h"
+#include "mozilla/WeakPtr.h"
+#include "nsDOMTokenList.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla {
+class EventChainPostVisitor;
+class EventChainPreVisitor;
+class PreloaderBase;
+
+namespace dom {
+
+class HTMLLinkElement final : public nsGenericHTMLElement,
+ public LinkStyle,
+ public SupportsDNSPrefetch {
+ public:
+ explicit HTMLLinkElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // CC
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLLinkElement,
+ nsGenericHTMLElement)
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLLinkElement, link);
+ NS_DECL_ADDSIZEOFEXCLUDINGTHIS
+
+ void LinkAdded();
+
+ // nsINode
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // nsIContent
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+ void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+ // Element
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ void CreateAndDispatchEvent(const nsAString& aEventName);
+
+ // WebIDL
+ bool Disabled() const;
+ void SetDisabled(bool aDisabled, ErrorResult& aRv);
+
+ nsIURI* GetURI() {
+ if (!mCachedURI) {
+ GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(mCachedURI));
+ }
+ return mCachedURI.get();
+ }
+
+ void GetHref(nsAString& aValue) {
+ GetURIAttr(nsGkAtoms::href, nullptr, aValue);
+ }
+ void SetHref(const nsAString& aHref, nsIPrincipal* aTriggeringPrincipal,
+ ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::href, aHref, aTriggeringPrincipal, aRv);
+ }
+ void SetHref(const nsAString& aHref, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::href, aHref, aRv);
+ }
+ void GetCrossOrigin(nsAString& aResult) {
+ // Null for both missing and invalid defaults is ok, since we
+ // always parse to an enum value, so we don't need an invalid
+ // default, and we _want_ the missing default to be null.
+ GetEnumAttr(nsGkAtoms::crossorigin, nullptr, aResult);
+ }
+ void SetCrossOrigin(const nsAString& aCrossOrigin, ErrorResult& aError) {
+ SetOrRemoveNullableStringAttr(nsGkAtoms::crossorigin, aCrossOrigin, aError);
+ }
+ // nsAString for WebBrowserPersistLocalDocument
+ void GetRel(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::rel, aValue); }
+ void SetRel(const nsAString& aRel, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::rel, aRel, aRv);
+ }
+ nsDOMTokenList* RelList();
+ void GetMedia(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::media, aValue); }
+ void SetMedia(const nsAString& aMedia, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::media, aMedia, aRv);
+ }
+ void GetHreflang(DOMString& aValue) {
+ GetHTMLAttr(nsGkAtoms::hreflang, aValue);
+ }
+ void SetHreflang(const nsAString& aHreflang, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::hreflang, aHreflang, aRv);
+ }
+ void GetAs(nsAString& aResult);
+ void SetAs(const nsAString& aAs, ErrorResult& aRv) {
+ SetAttr(nsGkAtoms::as, aAs, aRv);
+ }
+
+ nsDOMTokenList* Sizes() {
+ if (!mSizes) {
+ mSizes = new nsDOMTokenList(this, nsGkAtoms::sizes);
+ }
+ return mSizes;
+ }
+ void GetType(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::type, aValue); }
+ void SetType(const nsAString& aType, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::type, aType, aRv);
+ }
+ void GetCharset(nsAString& aValue) override {
+ GetHTMLAttr(nsGkAtoms::charset, aValue);
+ }
+ void SetCharset(const nsAString& aCharset, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::charset, aCharset, aRv);
+ }
+ void GetRev(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::rev, aValue); }
+ void SetRev(const nsAString& aRev, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::rev, aRev, aRv);
+ }
+ void GetTarget(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::target, aValue); }
+ void SetTarget(const nsAString& aTarget, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::target, aTarget, aRv);
+ }
+ void GetIntegrity(nsAString& aIntegrity) const {
+ GetHTMLAttr(nsGkAtoms::integrity, aIntegrity);
+ }
+ void SetIntegrity(const nsAString& aIntegrity, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::integrity, aIntegrity, aRv);
+ }
+ void SetReferrerPolicy(const nsAString& aReferrer, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::referrerpolicy, aReferrer, aError);
+ }
+ void GetReferrerPolicy(nsAString& aReferrer) {
+ GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrer);
+ }
+ void GetImageSrcset(nsAString& aImageSrcset) {
+ GetHTMLAttr(nsGkAtoms::imagesrcset, aImageSrcset);
+ }
+ void SetImageSrcset(const nsAString& aImageSrcset, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::imagesrcset, aImageSrcset, aError);
+ }
+ void GetImageSizes(nsAString& aImageSizes) {
+ GetHTMLAttr(nsGkAtoms::imagesizes, aImageSizes);
+ }
+ void SetImageSizes(const nsAString& aImageSizes, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::imagesizes, aImageSizes, aError);
+ }
+
+ CORSMode GetCORSMode() const {
+ return AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin));
+ }
+
+ nsDOMTokenList* Blocking();
+
+ void NodeInfoChanged(Document* aOldDoc) final {
+ mCachedURI = nullptr;
+ nsGenericHTMLElement::NodeInfoChanged(aOldDoc);
+ }
+
+ protected:
+ virtual ~HTMLLinkElement();
+
+ void GetContentPolicyMimeTypeMedia(nsAttrValue& aAsAttr,
+ nsContentPolicyType& aPolicyType,
+ nsString& aMimeType, nsAString& aMedia);
+ void TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender();
+ void UpdatePreload(nsAtom* aName, const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue);
+ void CancelPrefetchOrPreload();
+
+ void StartPreload(nsContentPolicyType policyType);
+ void CancelPreload();
+
+ // Returns whether the type attribute specifies the text/css mime type for
+ // link elements.
+ static bool IsCSSMimeTypeAttributeForLinkElement(
+ const mozilla::dom::Element&);
+
+ // LinkStyle
+ nsIContent& AsContent() final { return *this; }
+ const LinkStyle* AsLinkStyle() const final { return this; }
+ Maybe<SheetInfo> GetStyleSheetInfo() final;
+
+ RefPtr<nsDOMTokenList> mRelList;
+ RefPtr<nsDOMTokenList> mSizes;
+ RefPtr<nsDOMTokenList> mBlocking;
+
+ // A weak reference to our preload is held only to cancel the preload when
+ // this node updates or unbounds from the tree. We want to prevent cycles,
+ // the preload is held alive by other means.
+ WeakPtr<PreloaderBase> mPreload;
+
+ // The cached href attribute value.
+ nsCOMPtr<nsIURI> mCachedURI;
+
+ // The "explicitly enabled" flag. This flag is set whenever the `disabled`
+ // attribute is explicitly unset, and makes alternate stylesheets not be
+ // disabled by default anymore.
+ //
+ // See https://github.com/whatwg/html/issues/3840#issuecomment-481034206.
+ bool mExplicitlyEnabled = false;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_HTMLLinkElement_h
diff --git a/dom/html/HTMLMapElement.cpp b/dom/html/HTMLMapElement.cpp
new file mode 100644
index 0000000000..e4d3ca4f41
--- /dev/null
+++ b/dom/html/HTMLMapElement.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLMapElement.h"
+#include "mozilla/dom/HTMLMapElementBinding.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsContentList.h"
+#include "nsCOMPtr.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Map)
+
+namespace mozilla::dom {
+
+HTMLMapElement::HTMLMapElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLMapElement, nsGenericHTMLElement, mAreas)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLMapElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLMapElement)
+
+nsIHTMLCollection* HTMLMapElement::Areas() {
+ if (!mAreas) {
+ // Not using NS_GetContentList because this should not be cached
+ mAreas = new nsContentList(this, kNameSpaceID_XHTML, nsGkAtoms::area,
+ nsGkAtoms::area, false);
+ }
+
+ return mAreas;
+}
+
+JSObject* HTMLMapElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLMapElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLMapElement.h b/dom/html/HTMLMapElement.h
new file mode 100644
index 0000000000..3a16432a8a
--- /dev/null
+++ b/dom/html/HTMLMapElement.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLMapElement_h
+#define mozilla_dom_HTMLMapElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+
+class nsContentList;
+
+namespace mozilla::dom {
+
+class HTMLMapElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLMapElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLMapElement, nsGenericHTMLElement)
+
+ void GetName(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); }
+ void SetName(const nsAString& aName, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aError);
+ }
+ nsIHTMLCollection* Areas();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ protected:
+ ~HTMLMapElement() = default;
+
+ RefPtr<nsContentList> mAreas;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLMapElement_h
diff --git a/dom/html/HTMLMarqueeElement.cpp b/dom/html/HTMLMarqueeElement.cpp
new file mode 100644
index 0000000000..61308bf03e
--- /dev/null
+++ b/dom/html/HTMLMarqueeElement.cpp
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLMarqueeElement.h"
+#include "nsGenericHTMLElement.h"
+#include "nsStyleConsts.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/HTMLMarqueeElementBinding.h"
+#include "mozilla/dom/CustomEvent.h"
+// This is to pick up the definition of FunctionStringCallback:
+#include "mozilla/dom/DataTransferItemBinding.h"
+#include "mozilla/dom/ShadowRoot.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Marquee)
+
+namespace mozilla::dom {
+
+HTMLMarqueeElement::~HTMLMarqueeElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLMarqueeElement)
+
+static const nsAttrValue::EnumTable kBehaviorTable[] = {
+ {"scroll", 1}, {"slide", 2}, {"alternate", 3}, {nullptr, 0}};
+
+// Default behavior value is "scroll".
+static const nsAttrValue::EnumTable* kDefaultBehavior = &kBehaviorTable[0];
+
+static const nsAttrValue::EnumTable kDirectionTable[] = {
+ {"left", 1}, {"right", 2}, {"up", 3}, {"down", 4}, {nullptr, 0}};
+
+// Default direction value is "left".
+static const nsAttrValue::EnumTable* kDefaultDirection = &kDirectionTable[0];
+
+bool HTMLMarqueeElement::IsEventAttributeNameInternal(nsAtom* aName) {
+ return nsContentUtils::IsEventAttributeName(
+ aName, EventNameType_HTML | EventNameType_HTMLMarqueeOnly);
+}
+
+JSObject* HTMLMarqueeElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::HTMLMarqueeElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsresult HTMLMarqueeElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsInComposedDoc()) {
+ AttachAndSetUAShadowRoot();
+ }
+
+ return rv;
+}
+
+void HTMLMarqueeElement::UnbindFromTree(bool aNullParent) {
+ if (IsInComposedDoc()) {
+ // We don't want to unattach the shadow root because it used to
+ // contain a <slot>.
+ NotifyUAWidgetTeardown(UnattachShadowRoot::No);
+ }
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+void HTMLMarqueeElement::GetBehavior(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::behavior, kDefaultBehavior->tag, aValue);
+}
+
+void HTMLMarqueeElement::GetDirection(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::direction, kDefaultDirection->tag, aValue);
+}
+
+bool HTMLMarqueeElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if ((aAttribute == nsGkAtoms::width) || (aAttribute == nsGkAtoms::height)) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::bgcolor) {
+ return aResult.ParseColor(aValue);
+ }
+ if (aAttribute == nsGkAtoms::behavior) {
+ return aResult.ParseEnumValue(aValue, kBehaviorTable, false,
+ kDefaultBehavior);
+ }
+ if (aAttribute == nsGkAtoms::direction) {
+ return aResult.ParseEnumValue(aValue, kDirectionTable, false,
+ kDefaultDirection);
+ }
+ if (aAttribute == nsGkAtoms::hspace || aAttribute == nsGkAtoms::vspace) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+
+ if (aAttribute == nsGkAtoms::loop) {
+ return aResult.ParseIntValue(aValue);
+ }
+
+ if (aAttribute == nsGkAtoms::scrollamount ||
+ aAttribute == nsGkAtoms::scrolldelay) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLMarqueeElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (IsInComposedDoc() && aNameSpaceID == kNameSpaceID_None &&
+ aName == nsGkAtoms::direction) {
+ NotifyUAWidgetSetupOrChange();
+ }
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void HTMLMarqueeElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ nsGenericHTMLElement::MapImageMarginAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapImageSizeAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapBGColorInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLMarqueeElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry* const map[] = {
+ sImageMarginSizeAttributeMap, sBackgroundColorAttributeMap,
+ sCommonAttributeMap};
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLMarqueeElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+void HTMLMarqueeElement::DispatchEventToShadowRoot(
+ const nsAString& aEventTypeArg) {
+ // Dispatch the event to the UA Widget Shadow Root, make it inaccessible to
+ // document.
+ RefPtr<nsINode> shadow = GetShadowRoot();
+ MOZ_ASSERT(shadow);
+ RefPtr<Event> event = new Event(shadow, nullptr, nullptr);
+ event->InitEvent(aEventTypeArg, false, false);
+ event->SetTrusted(true);
+ shadow->DispatchEvent(*event, IgnoreErrors());
+}
+
+void HTMLMarqueeElement::Start() {
+ if (GetShadowRoot()) {
+ DispatchEventToShadowRoot(u"marquee-start"_ns);
+ }
+}
+
+void HTMLMarqueeElement::Stop() {
+ if (GetShadowRoot()) {
+ DispatchEventToShadowRoot(u"marquee-stop"_ns);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLMarqueeElement.h b/dom/html/HTMLMarqueeElement.h
new file mode 100644
index 0000000000..250d7c2cf9
--- /dev/null
+++ b/dom/html/HTMLMarqueeElement.h
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef HTMLMarqueeElement_h___
+#define HTMLMarqueeElement_h___
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsContentUtils.h"
+
+namespace mozilla::dom {
+class HTMLMarqueeElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLMarqueeElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLMarqueeElement, marquee);
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ static const int kDefaultLoop = -1;
+ static const int kDefaultScrollAmount = 6;
+ static const int kDefaultScrollDelayMS = 85;
+
+ bool IsEventAttributeNameInternal(nsAtom* aName) override;
+
+ void GetBehavior(nsAString& aValue);
+ void SetBehavior(const nsAString& aValue, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::behavior, aValue, aError);
+ }
+
+ void GetDirection(nsAString& aValue);
+ void SetDirection(const nsAString& aValue, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::direction, aValue, aError);
+ }
+
+ void GetBgColor(DOMString& aBgColor) {
+ GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor);
+ }
+ void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError);
+ }
+ void GetHeight(DOMString& aHeight) {
+ GetHTMLAttr(nsGkAtoms::height, aHeight);
+ }
+ void SetHeight(const nsAString& aHeight, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::height, aHeight, aError);
+ }
+ uint32_t Hspace() {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::hspace, 0);
+ }
+ void SetHspace(uint32_t aValue, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::hspace, aValue, 0, aError);
+ }
+ int32_t Loop() {
+ int loop = GetIntAttr(nsGkAtoms::loop, kDefaultLoop);
+ if (loop <= 0) {
+ loop = -1;
+ }
+
+ return loop;
+ }
+ void SetLoop(int32_t aValue, ErrorResult& aError) {
+ if (aValue == -1 || aValue > 0) {
+ SetHTMLIntAttr(nsGkAtoms::loop, aValue, aError);
+ }
+ }
+ uint32_t ScrollAmount() {
+ return GetUnsignedIntAttr(nsGkAtoms::scrollamount, kDefaultScrollAmount);
+ }
+ void SetScrollAmount(uint32_t aValue, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::scrollamount, aValue, kDefaultScrollAmount,
+ aError);
+ }
+ uint32_t ScrollDelay() {
+ return GetUnsignedIntAttr(nsGkAtoms::scrolldelay, kDefaultScrollDelayMS);
+ }
+ void SetScrollDelay(uint32_t aValue, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::scrolldelay, aValue, kDefaultScrollDelayMS,
+ aError);
+ }
+ bool TrueSpeed() const { return GetBoolAttr(nsGkAtoms::truespeed); }
+ void SetTrueSpeed(bool aValue, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::truespeed, aValue, aError);
+ }
+ void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); }
+ void SetWidth(const nsAString& aWidth, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::width, aWidth, aError);
+ }
+ uint32_t Vspace() {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::vspace, 0);
+ }
+ void SetVspace(uint32_t aValue, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::vspace, aValue, 0, aError);
+ }
+
+ void Start();
+ void Stop();
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLMarqueeElement();
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+
+ void DispatchEventToShadowRoot(const nsAString& aEventTypeArg);
+};
+
+} // namespace mozilla::dom
+
+#endif /* HTMLMarqueeElement_h___ */
diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp
new file mode 100644
index 0000000000..78e9a7b861
--- /dev/null
+++ b/dom/html/HTMLMediaElement.cpp
@@ -0,0 +1,7881 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifdef XP_WIN
+# include "objbase.h"
+#endif
+
+#include "mozilla/dom/HTMLMediaElement.h"
+
+#include <unordered_map>
+
+#include "AudioDeviceInfo.h"
+#include "AudioStreamTrack.h"
+#include "AutoplayPolicy.h"
+#include "ChannelMediaDecoder.h"
+#include "CrossGraphPort.h"
+#include "DOMMediaStream.h"
+#include "DecoderDoctorDiagnostics.h"
+#include "DecoderDoctorLogger.h"
+#include "DecoderTraits.h"
+#include "FrameStatistics.h"
+#include "GMPCrashHelper.h"
+#include "GVAutoplayPermissionRequest.h"
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+# include "HLSDecoder.h"
+#endif
+#include "HTMLMediaElement.h"
+#include "ImageContainer.h"
+#include "MP4Decoder.h"
+#include "MediaContainerType.h"
+#include "MediaError.h"
+#include "MediaManager.h"
+#include "MediaMetadataManager.h"
+#include "MediaResource.h"
+#include "MediaShutdownManager.h"
+#include "MediaSourceDecoder.h"
+#include "MediaStreamError.h"
+#include "MediaTrackGraphImpl.h"
+#include "MediaTrackListener.h"
+#include "MediaStreamWindowCapturer.h"
+#include "MediaTrack.h"
+#include "MediaTrackList.h"
+#include "Navigator.h"
+#include "TimeRanges.h"
+#include "VideoFrameContainer.h"
+#include "VideoOutput.h"
+#include "VideoStreamTrack.h"
+#include "base/basictypes.h"
+#include "jsapi.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SVGObserverUtils.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/AudioTrack.h"
+#include "mozilla/dom/AudioTrackList.h"
+#include "mozilla/dom/BlobURLProtocolHandler.h"
+#include "mozilla/dom/ContentMediaController.h"
+#include "mozilla/dom/ElementInlines.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/dom/HTMLAudioElement.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/HTMLMediaElementBinding.h"
+#include "mozilla/dom/HTMLSourceElement.h"
+#include "mozilla/dom/HTMLVideoElement.h"
+#include "mozilla/dom/MediaControlUtils.h"
+#include "mozilla/dom/MediaDevices.h"
+#include "mozilla/dom/MediaEncryptedEvent.h"
+#include "mozilla/dom/MediaErrorBinding.h"
+#include "mozilla/dom/MediaSource.h"
+#include "mozilla/dom/PlayPromise.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/TextTrack.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/VideoPlaybackQuality.h"
+#include "mozilla/dom/VideoTrack.h"
+#include "mozilla/dom/VideoTrackList.h"
+#include "mozilla/dom/WakeLock.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/dom/power/PowerManagerService.h"
+#include "mozilla/net/UrlClassifierFeatureFactory.h"
+#include "nsAttrValueInlines.h"
+#include "nsContentPolicyUtils.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDisplayList.h"
+#include "nsDocShell.h"
+#include "nsError.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsICachingChannel.h"
+#include "nsIClassOfService.h"
+#include "nsIContentPolicy.h"
+#include "nsIDocShell.h"
+#include "mozilla/dom/Document.h"
+#include "nsIFrame.h"
+#include "nsIHttpChannel.h"
+#include "nsIObserverService.h"
+#include "nsIRequest.h"
+#include "nsIScriptError.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIThreadRetargetableStreamListener.h"
+#include "nsITimer.h"
+#include "nsJSUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsMediaFragmentURIParser.h"
+#include "nsMimeTypes.h"
+#include "nsNetUtil.h"
+#include "nsNodeInfoManager.h"
+#include "nsPresContext.h"
+#include "nsQueryObject.h"
+#include "nsRange.h"
+#include "nsSize.h"
+#include "nsThreadUtils.h"
+#include "nsURIHashKey.h"
+#include "nsURLHelper.h"
+#include "nsVideoFrame.h"
+#include "ReferrerInfo.h"
+#include "TimeUnits.h"
+#include "xpcpublic.h"
+#include <algorithm>
+#include <cmath>
+#include <limits>
+#include <type_traits>
+
+mozilla::LazyLogModule gMediaElementLog("HTMLMediaElement");
+mozilla::LazyLogModule gMediaElementEventsLog("HTMLMediaElementEvents");
+
+extern mozilla::LazyLogModule gAutoplayPermissionLog;
+#define AUTOPLAY_LOG(msg, ...) \
+ MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+// avoid redefined macro in unified build
+#undef MEDIACONTROL_LOG
+#define MEDIACONTROL_LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("HTMLMediaElement=%p, " msg, this, ##__VA_ARGS__))
+
+#undef CONTROLLER_TIMER_LOG
+#define CONTROLLER_TIMER_LOG(element, msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("HTMLMediaElement=%p, " msg, element, ##__VA_ARGS__))
+
+#define LOG(type, msg) MOZ_LOG(gMediaElementLog, type, msg)
+#define LOG_EVENT(type, msg) MOZ_LOG(gMediaElementEventsLog, type, msg)
+
+using namespace mozilla::layers;
+using mozilla::net::nsMediaFragmentURIParser;
+using namespace mozilla::dom::HTMLMediaElement_Binding;
+
+namespace mozilla::dom {
+
+using AudibleState = AudioChannelService::AudibleState;
+using SinkInfoPromise = MediaDevices::SinkInfoPromise;
+
+// Number of milliseconds between progress events as defined by spec
+static const uint32_t PROGRESS_MS = 350;
+
+// Number of milliseconds of no data before a stall event is fired as defined by
+// spec
+static const uint32_t STALL_MS = 3000;
+
+// Used by AudioChannel for suppresssing the volume to this ratio.
+#define FADED_VOLUME_RATIO 0.25
+
+// These constants are arbitrary
+// Minimum playbackRate for a media
+static const double MIN_PLAYBACKRATE = 1.0 / 16;
+// Maximum playbackRate for a media
+static const double MAX_PLAYBACKRATE = 16.0;
+
+static double ClampPlaybackRate(double aPlaybackRate) {
+ MOZ_ASSERT(aPlaybackRate >= 0.0);
+
+ if (aPlaybackRate == 0.0) {
+ return aPlaybackRate;
+ }
+ if (aPlaybackRate < MIN_PLAYBACKRATE) {
+ return MIN_PLAYBACKRATE;
+ }
+ if (aPlaybackRate > MAX_PLAYBACKRATE) {
+ return MAX_PLAYBACKRATE;
+ }
+ return aPlaybackRate;
+}
+
+// Media error values. These need to match the ones in MediaError.webidl.
+static const unsigned short MEDIA_ERR_ABORTED = 1;
+static const unsigned short MEDIA_ERR_NETWORK = 2;
+static const unsigned short MEDIA_ERR_DECODE = 3;
+static const unsigned short MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
+
+/**
+ * EventBlocker helps media element to postpone the event delivery by storing
+ * the event runner, and execute them once media element decides not to postpone
+ * the event delivery. If media element never resumes the event delivery, then
+ * those runner would be cancelled.
+ * For example, we postpone the event delivery when media element entering to
+ * the bf-cache.
+ */
+class HTMLMediaElement::EventBlocker final : public nsISupports {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL
+ NS_DECL_CYCLE_COLLECTION_CLASS(EventBlocker)
+
+ explicit EventBlocker(HTMLMediaElement* aElement) : mElement(aElement) {}
+
+ void SetBlockEventDelivery(bool aShouldBlock) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mShouldBlockEventDelivery == aShouldBlock) {
+ return;
+ }
+ LOG_EVENT(LogLevel::Debug,
+ ("%p %s event delivery", mElement.get(),
+ mShouldBlockEventDelivery ? "block" : "unblock"));
+ mShouldBlockEventDelivery = aShouldBlock;
+ if (!mShouldBlockEventDelivery) {
+ DispatchPendingMediaEvents();
+ }
+ }
+
+ void PostponeEvent(nsMediaEventRunner* aRunner) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Element has been CCed, which would break the weak pointer.
+ if (!mElement) {
+ return;
+ }
+ MOZ_ASSERT(mShouldBlockEventDelivery);
+ MOZ_ASSERT(mElement);
+ LOG_EVENT(LogLevel::Debug,
+ ("%p postpone runner %s for %s", mElement.get(),
+ NS_ConvertUTF16toUTF8(aRunner->Name()).get(),
+ NS_ConvertUTF16toUTF8(aRunner->EventName()).get()));
+ mPendingEventRunners.AppendElement(aRunner);
+ }
+
+ void Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (auto& runner : mPendingEventRunners) {
+ runner->Cancel();
+ }
+ mPendingEventRunners.Clear();
+ }
+
+ bool ShouldBlockEventDelivery() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mShouldBlockEventDelivery;
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf& aMallocSizeOf) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ size_t total = 0;
+ for (const auto& runner : mPendingEventRunners) {
+ total += aMallocSizeOf(runner);
+ }
+ return total;
+ }
+
+ private:
+ ~EventBlocker() = default;
+
+ void DispatchPendingMediaEvents() {
+ MOZ_ASSERT(mElement);
+ for (auto& runner : mPendingEventRunners) {
+ LOG_EVENT(LogLevel::Debug,
+ ("%p execute runner %s for %s", mElement.get(),
+ NS_ConvertUTF16toUTF8(runner->Name()).get(),
+ NS_ConvertUTF16toUTF8(runner->EventName()).get()));
+ GetMainThreadSerialEventTarget()->Dispatch(runner.forget());
+ }
+ mPendingEventRunners.Clear();
+ }
+
+ WeakPtr<HTMLMediaElement> mElement;
+ bool mShouldBlockEventDelivery = false;
+ // Contains event runners which should not be run for now because we want
+ // to block all events delivery. They would be dispatched once media element
+ // decides unblocking them.
+ nsTArray<RefPtr<nsMediaEventRunner>> mPendingEventRunners;
+};
+
+NS_IMPL_CYCLE_COLLECTION(HTMLMediaElement::EventBlocker, mPendingEventRunners)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLMediaElement::EventBlocker)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLMediaElement::EventBlocker)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLMediaElement::EventBlocker)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/**
+ * We use MediaControlKeyListener to listen to media control key in order to
+ * play and pause media element when user press media control keys and update
+ * media's playback and audible state to the media controller.
+ *
+ * Use `Start()` to start listening event and use `Stop()` to stop listening
+ * event. In addition, notifying any change to media controller MUST be done
+ * after successfully calling `Start()`.
+ */
+class HTMLMediaElement::MediaControlKeyListener final
+ : public ContentMediaControlKeyReceiver {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaControlKeyListener, override)
+
+ MOZ_INIT_OUTSIDE_CTOR explicit MediaControlKeyListener(
+ HTMLMediaElement* aElement)
+ : mElement(aElement) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aElement);
+ }
+
+ /**
+ * Start listening to the media control keys which would make media being able
+ * to be controlled via pressing media control keys.
+ */
+ void Start() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (IsStarted()) {
+ // We have already been started, do not notify start twice.
+ return;
+ }
+
+ // Fail to init media agent, we are not able to notify the media controller
+ // any update and also are not able to receive media control key events.
+ if (!InitMediaAgent()) {
+ MEDIACONTROL_LOG("Failed to start due to not able to init media agent!");
+ return;
+ }
+
+ NotifyPlaybackStateChanged(MediaPlaybackState::eStarted);
+ // If owner has started playing before the listener starts, we should update
+ // the playing state as well. Eg. media starts inaudily and becomes audible
+ // later.
+ if (!Owner()->Paused()) {
+ NotifyMediaStartedPlaying();
+ }
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ auto dispatcher = MakeRefPtr<AsyncEventDispatcher>(
+ Owner(), u"MozStartMediaControl"_ns, CanBubble::eYes,
+ ChromeOnlyDispatch::eYes);
+ dispatcher->PostDOMEvent();
+ }
+ }
+
+ /**
+ * Stop listening to the media control keys which would make media not be able
+ * to be controlled via pressing media control keys. If we haven't started
+ * listening to the media control keys, then nothing would happen.
+ */
+ void StopIfNeeded() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!IsStarted()) {
+ // We have already been stopped, do not notify stop twice.
+ return;
+ }
+ NotifyMediaStoppedPlaying();
+ NotifyPlaybackStateChanged(MediaPlaybackState::eStopped);
+
+ // Remove ourselves from media agent, which would stop receiving event.
+ mControlAgent->RemoveReceiver(this);
+ mControlAgent = nullptr;
+ }
+
+ bool IsStarted() const { return mState != MediaPlaybackState::eStopped; }
+
+ bool IsPlaying() const override {
+ return Owner() ? !Owner()->Paused() : false;
+ }
+
+ /**
+ * Following methods should only be used after starting listener.
+ */
+ void NotifyMediaStartedPlaying() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsStarted());
+ if (mState == MediaPlaybackState::eStarted ||
+ mState == MediaPlaybackState::ePaused) {
+ NotifyPlaybackStateChanged(MediaPlaybackState::ePlayed);
+ // If media is `inaudible` in the beginning, then we don't need to notify
+ // the state, because notifying `inaudible` should always come after
+ // notifying `audible`.
+ if (mIsOwnerAudible) {
+ NotifyAudibleStateChanged(MediaAudibleState::eAudible);
+ }
+ }
+ }
+
+ void NotifyMediaStoppedPlaying() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsStarted());
+ if (mState == MediaPlaybackState::ePlayed) {
+ NotifyPlaybackStateChanged(MediaPlaybackState::ePaused);
+ // As media are going to be paused, so no sound is possible to be heard.
+ if (mIsOwnerAudible) {
+ NotifyAudibleStateChanged(MediaAudibleState::eInaudible);
+ }
+ }
+ }
+
+ // This method can be called before the listener starts, which would cache
+ // the audible state and update after the listener starts.
+ void UpdateMediaAudibleState(bool aIsOwnerAudible) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mIsOwnerAudible == aIsOwnerAudible) {
+ return;
+ }
+ mIsOwnerAudible = aIsOwnerAudible;
+ MEDIACONTROL_LOG("Media becomes %s",
+ mIsOwnerAudible ? "audible" : "inaudible");
+ // If media hasn't started playing, it doesn't make sense to update media
+ // audible state. Therefore, in that case we would noitfy the audible state
+ // when media starts playing.
+ if (mState == MediaPlaybackState::ePlayed) {
+ NotifyAudibleStateChanged(mIsOwnerAudible
+ ? MediaAudibleState::eAudible
+ : MediaAudibleState::eInaudible);
+ }
+ }
+
+ void SetPictureInPictureModeEnabled(bool aIsEnabled) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mIsPictureInPictureEnabled == aIsEnabled) {
+ return;
+ }
+ // PIP state changes might happen before the listener starts or stops where
+ // we haven't call `InitMediaAgent()` yet. Eg. Reset the PIP video's src,
+ // then cancel the PIP. In addition, not like playback and audible state
+ // which should be restricted to update via the same agent in order to keep
+ // those states correct in each `ContextMediaInfo`, PIP state can be updated
+ // through any browsing context, so we would use `ContentMediaAgent::Get()`
+ // directly to update PIP state.
+ mIsPictureInPictureEnabled = aIsEnabled;
+ if (RefPtr<IMediaInfoUpdater> updater =
+ ContentMediaAgent::Get(GetCurrentBrowsingContext())) {
+ updater->SetIsInPictureInPictureMode(mOwnerBrowsingContextId,
+ mIsPictureInPictureEnabled);
+ }
+ }
+
+ void HandleMediaKey(MediaControlKey aKey) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsStarted());
+ MEDIACONTROL_LOG("HandleEvent '%s'", ToMediaControlKeyStr(aKey));
+ if (aKey == MediaControlKey::Play) {
+ Owner()->Play();
+ } else if (aKey == MediaControlKey::Pause) {
+ Owner()->Pause();
+ } else {
+ MOZ_ASSERT(aKey == MediaControlKey::Stop,
+ "Not supported key for media element!");
+ Owner()->Pause();
+ StopIfNeeded();
+ }
+ }
+
+ void UpdateOwnerBrowsingContextIfNeeded() {
+ // Has not notified any information about the owner context yet.
+ if (!IsStarted()) {
+ return;
+ }
+
+ BrowsingContext* currentBC = GetCurrentBrowsingContext();
+ MOZ_ASSERT(currentBC);
+ // Still in the same browsing context, no need to update.
+ if (currentBC->Id() == mOwnerBrowsingContextId) {
+ return;
+ }
+ MEDIACONTROL_LOG("Change browsing context from %" PRIu64 " to %" PRIu64,
+ mOwnerBrowsingContextId, currentBC->Id());
+ // This situation would happen when we start a media in an original browsing
+ // context, then we move it to another browsing context, such as an iframe,
+ // so its owner browsing context would be changed. Therefore, we should
+ // reset the media status for the previous browsing context by calling
+ // `Stop()`, in which the listener would notify `ePaused` (if it's playing)
+ // and `eStop`. Then calls `Start()`, in which the listener would notify
+ // `eStart` to the new browsing context. If the media was playing before,
+ // we would also notify `ePlayed`.
+ bool wasInPlayingState = mState == MediaPlaybackState::ePlayed;
+ StopIfNeeded();
+ Start();
+ if (wasInPlayingState) {
+ NotifyMediaStartedPlaying();
+ }
+ }
+
+ private:
+ ~MediaControlKeyListener() = default;
+
+ // The media can be moved around different browsing contexts, so this context
+ // might be different from the one that we used to initialize
+ // `ContentMediaAgent`.
+ BrowsingContext* GetCurrentBrowsingContext() const {
+ // Owner has been CCed, which would break the link of the weaker pointer.
+ if (!Owner()) {
+ return nullptr;
+ }
+ nsPIDOMWindowInner* window = Owner()->OwnerDoc()->GetInnerWindow();
+ return window ? window->GetBrowsingContext() : nullptr;
+ }
+
+ bool InitMediaAgent() {
+ MOZ_ASSERT(NS_IsMainThread());
+ BrowsingContext* currentBC = GetCurrentBrowsingContext();
+ mControlAgent = ContentMediaAgent::Get(currentBC);
+ if (!mControlAgent) {
+ return false;
+ }
+ MOZ_ASSERT(currentBC);
+ mOwnerBrowsingContextId = currentBC->Id();
+ MEDIACONTROL_LOG("Init agent in browsing context %" PRIu64,
+ mOwnerBrowsingContextId);
+ mControlAgent->AddReceiver(this);
+ return true;
+ }
+
+ HTMLMediaElement* Owner() const {
+ // `mElement` would be clear during CC unlinked, but it would only happen
+ // after stopping the listener.
+ MOZ_ASSERT(mElement || !IsStarted());
+ return mElement.get();
+ }
+
+ void NotifyPlaybackStateChanged(MediaPlaybackState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mControlAgent);
+ MEDIACONTROL_LOG("NotifyMediaState from state='%s' to state='%s'",
+ ToMediaPlaybackStateStr(mState),
+ ToMediaPlaybackStateStr(aState));
+ MOZ_ASSERT(mState != aState, "Should not notify same state again!");
+ mState = aState;
+ mControlAgent->NotifyMediaPlaybackChanged(mOwnerBrowsingContextId, mState);
+ }
+
+ void NotifyAudibleStateChanged(MediaAudibleState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsStarted());
+ mControlAgent->NotifyMediaAudibleChanged(mOwnerBrowsingContextId, aState);
+ }
+
+ MediaPlaybackState mState = MediaPlaybackState::eStopped;
+ WeakPtr<HTMLMediaElement> mElement;
+ RefPtr<ContentMediaAgent> mControlAgent;
+ bool mIsPictureInPictureEnabled = false;
+ bool mIsOwnerAudible = false;
+ MOZ_INIT_OUTSIDE_CTOR uint64_t mOwnerBrowsingContextId;
+};
+
+class HTMLMediaElement::MediaStreamTrackListener
+ : public DOMMediaStream::TrackListener {
+ public:
+ explicit MediaStreamTrackListener(HTMLMediaElement* aElement)
+ : mElement(aElement) {}
+
+ void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override {
+ if (!mElement) {
+ return;
+ }
+ mElement->NotifyMediaStreamTrackAdded(aTrack);
+ }
+
+ void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override {
+ if (!mElement) {
+ return;
+ }
+ mElement->NotifyMediaStreamTrackRemoved(aTrack);
+ }
+
+ void OnActive() {
+ MOZ_ASSERT(mElement);
+
+ // mediacapture-main says:
+ // Note that once ended equals true the HTMLVideoElement will not play media
+ // even if new MediaStreamTracks are added to the MediaStream (causing it to
+ // return to the active state) unless autoplay is true or the web
+ // application restarts the element, e.g., by calling play().
+ //
+ // This is vague on exactly how to go from becoming active to playing, when
+ // autoplaying. However, per the media element spec, to play an autoplaying
+ // media element, we must load the source and reach readyState
+ // HAVE_ENOUGH_DATA [1]. Hence, a MediaStream being assigned to a media
+ // element and becoming active runs the load algorithm, so that it can
+ // eventually be played.
+ //
+ // [1]
+ // https://html.spec.whatwg.org/multipage/media.html#ready-states:event-media-play
+
+ LOG(LogLevel::Debug, ("%p, mSrcStream %p became active, checking if we "
+ "need to run the load algorithm",
+ mElement.get(), mElement->mSrcStream.get()));
+ if (!mElement->IsPlaybackEnded()) {
+ return;
+ }
+ if (!mElement->Autoplay()) {
+ return;
+ }
+ LOG(LogLevel::Info, ("%p, mSrcStream %p became active on autoplaying, "
+ "ended element. Reloading.",
+ mElement.get(), mElement->mSrcStream.get()));
+ mElement->DoLoad();
+ }
+
+ void NotifyActive() override {
+ if (!mElement) {
+ return;
+ }
+
+ if (!mElement->IsVideo()) {
+ // Audio elements use NotifyAudible().
+ return;
+ }
+
+ OnActive();
+ }
+
+ void NotifyAudible() override {
+ if (!mElement) {
+ return;
+ }
+
+ if (mElement->IsVideo()) {
+ // Video elements use NotifyActive().
+ return;
+ }
+
+ OnActive();
+ }
+
+ void OnInactive() {
+ MOZ_ASSERT(mElement);
+
+ if (mElement->IsPlaybackEnded()) {
+ return;
+ }
+ LOG(LogLevel::Debug, ("%p, mSrcStream %p became inactive", mElement.get(),
+ mElement->mSrcStream.get()));
+
+ mElement->PlaybackEnded();
+ }
+
+ void NotifyInactive() override {
+ if (!mElement) {
+ return;
+ }
+
+ if (!mElement->IsVideo()) {
+ // Audio elements use NotifyInaudible().
+ return;
+ }
+
+ OnInactive();
+ }
+
+ void NotifyInaudible() override {
+ if (!mElement) {
+ return;
+ }
+
+ if (mElement->IsVideo()) {
+ // Video elements use NotifyInactive().
+ return;
+ }
+
+ OnInactive();
+ }
+
+ protected:
+ const WeakPtr<HTMLMediaElement> mElement;
+};
+
+/**
+ * Helper class that manages audio and video outputs for all enabled tracks in a
+ * media element. It also manages calculating the current time when playing a
+ * MediaStream.
+ */
+class HTMLMediaElement::MediaStreamRenderer
+ : public DOMMediaStream::TrackListener {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaStreamRenderer)
+
+ MediaStreamRenderer(AbstractThread* aMainThread,
+ VideoFrameContainer* aVideoContainer,
+ FirstFrameVideoOutput* aFirstFrameVideoOutput,
+ void* aAudioOutputKey)
+ : mVideoContainer(aVideoContainer),
+ mAudioOutputKey(aAudioOutputKey),
+ mWatchManager(this, aMainThread),
+ mFirstFrameVideoOutput(aFirstFrameVideoOutput) {
+ if (mFirstFrameVideoOutput) {
+ mWatchManager.Watch(mFirstFrameVideoOutput->mFirstFrameRendered,
+ &MediaStreamRenderer::SetFirstFrameRendered);
+ }
+ }
+
+ void Shutdown() {
+ for (const auto& t : mAudioTracks.Clone()) {
+ if (t) {
+ RemoveTrack(t->AsAudioStreamTrack());
+ }
+ }
+ if (mVideoTrack) {
+ RemoveTrack(mVideoTrack->AsVideoStreamTrack());
+ }
+ mWatchManager.Shutdown();
+ mFirstFrameVideoOutput = nullptr;
+ }
+
+ void UpdateGraphTime() {
+ mGraphTime =
+ mGraphTimeDummy->mTrack->Graph()->CurrentTime() - *mGraphTimeOffset;
+ }
+
+ void SetFirstFrameRendered() {
+ if (!mFirstFrameVideoOutput) {
+ return;
+ }
+ if (mVideoTrack) {
+ mVideoTrack->AsVideoStreamTrack()->RemoveVideoOutput(
+ mFirstFrameVideoOutput);
+ }
+ mWatchManager.Unwatch(mFirstFrameVideoOutput->mFirstFrameRendered,
+ &MediaStreamRenderer::SetFirstFrameRendered);
+ mFirstFrameVideoOutput = nullptr;
+ }
+
+ void SetProgressingCurrentTime(bool aProgress) {
+ if (aProgress == mProgressingCurrentTime) {
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mGraphTimeDummy);
+ mProgressingCurrentTime = aProgress;
+ MediaTrackGraph* graph = mGraphTimeDummy->mTrack->Graph();
+ if (mProgressingCurrentTime) {
+ mGraphTimeOffset = Some(graph->CurrentTime().Ref() - mGraphTime);
+ mWatchManager.Watch(graph->CurrentTime(),
+ &MediaStreamRenderer::UpdateGraphTime);
+ } else {
+ mWatchManager.Unwatch(graph->CurrentTime(),
+ &MediaStreamRenderer::UpdateGraphTime);
+ }
+ }
+
+ void Start() {
+ if (mRendering) {
+ return;
+ }
+
+ LOG(LogLevel::Info, ("MediaStreamRenderer=%p Start", this));
+ mRendering = true;
+
+ if (!mGraphTimeDummy) {
+ return;
+ }
+
+ for (const auto& t : mAudioTracks) {
+ if (t) {
+ t->AsAudioStreamTrack()->AddAudioOutput(mAudioOutputKey,
+ mAudioOutputSink);
+ t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey,
+ mAudioOutputVolume);
+ }
+ }
+
+ if (mVideoTrack) {
+ mVideoTrack->AsVideoStreamTrack()->AddVideoOutput(mVideoContainer);
+ }
+ }
+
+ void Stop() {
+ if (!mRendering) {
+ return;
+ }
+
+ LOG(LogLevel::Info, ("MediaStreamRenderer=%p Stop", this));
+ mRendering = false;
+
+ if (!mGraphTimeDummy) {
+ return;
+ }
+
+ for (const auto& t : mAudioTracks) {
+ if (t) {
+ t->AsAudioStreamTrack()->RemoveAudioOutput(mAudioOutputKey);
+ }
+ }
+ // There is no longer an audio output that needs the device so the
+ // device may not start. Ensure the promise is resolved.
+ ResolveAudioDevicePromiseIfExists(__func__);
+
+ if (mVideoTrack) {
+ mVideoTrack->AsVideoStreamTrack()->RemoveVideoOutput(mVideoContainer);
+ }
+ }
+
+ void SetAudioOutputVolume(float aVolume) {
+ if (mAudioOutputVolume == aVolume) {
+ return;
+ }
+ mAudioOutputVolume = aVolume;
+ if (!mRendering) {
+ return;
+ }
+ for (const auto& t : mAudioTracks) {
+ if (t) {
+ t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey,
+ mAudioOutputVolume);
+ }
+ }
+ }
+
+ RefPtr<GenericPromise> SetAudioOutputDevice(AudioDeviceInfo* aSink) {
+ MOZ_ASSERT(aSink);
+ MOZ_ASSERT(mAudioOutputSink != aSink);
+ LOG(LogLevel::Info,
+ ("MediaStreamRenderer=%p SetAudioOutputDevice name=%s\n", this,
+ NS_ConvertUTF16toUTF8(aSink->Name()).get()));
+
+ mAudioOutputSink = aSink;
+
+ if (!mRendering) {
+ MOZ_ASSERT(mSetAudioDevicePromise.IsEmpty());
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ nsTArray<RefPtr<GenericPromise>> promises;
+ for (const auto& t : mAudioTracks) {
+ t->AsAudioStreamTrack()->RemoveAudioOutput(mAudioOutputKey);
+ promises.AppendElement(t->AsAudioStreamTrack()->AddAudioOutput(
+ mAudioOutputKey, mAudioOutputSink));
+ t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey,
+ mAudioOutputVolume);
+ }
+ if (!promises.Length()) {
+ // Not active track, save it for later
+ MOZ_ASSERT(mSetAudioDevicePromise.IsEmpty());
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ // Resolve any existing promise for a previous device so that promises
+ // resolve in order of setSinkId() invocation.
+ ResolveAudioDevicePromiseIfExists(__func__);
+
+ RefPtr promise = mSetAudioDevicePromise.Ensure(__func__);
+ GenericPromise::AllSettled(GetCurrentSerialEventTarget(), promises)
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ [self = RefPtr{this},
+ this](const GenericPromise::AllSettledPromiseType::
+ ResolveOrRejectValue& aValue) {
+ // This handler should have been disconnected if
+ // mSetAudioDevicePromise has been settled.
+ MOZ_ASSERT(!mSetAudioDevicePromise.IsEmpty());
+ mDeviceStartedRequest.Complete();
+ // The AudioStreamTrack::AddAudioOutput() promise is rejected
+ // either when the graph no longer needs the device, in which
+ // case this handler would have already been disconnected, or
+ // the graph is force shutdown.
+ // mSetAudioDevicePromise is resolved regardless of whether
+ // the AddAudioOutput() promises resolve or reject because
+ // the underlying device has been changed.
+ LOG(LogLevel::Info,
+ ("MediaStreamRenderer=%p SetAudioOutputDevice settled",
+ this));
+ mSetAudioDevicePromise.Resolve(true, __func__);
+ })
+ ->Track(mDeviceStartedRequest);
+
+ return promise;
+ }
+
+ void AddTrack(AudioStreamTrack* aTrack) {
+ MOZ_DIAGNOSTIC_ASSERT(!mAudioTracks.Contains(aTrack));
+ mAudioTracks.AppendElement(aTrack);
+ EnsureGraphTimeDummy();
+ if (mRendering) {
+ aTrack->AddAudioOutput(mAudioOutputKey, mAudioOutputSink);
+ aTrack->SetAudioOutputVolume(mAudioOutputKey, mAudioOutputVolume);
+ }
+ }
+ void AddTrack(VideoStreamTrack* aTrack) {
+ MOZ_DIAGNOSTIC_ASSERT(!mVideoTrack);
+ if (!mVideoContainer) {
+ return;
+ }
+ mVideoTrack = aTrack;
+ EnsureGraphTimeDummy();
+ if (mFirstFrameVideoOutput) {
+ // Add the first frame output even if we are rendering. It will only
+ // accept one frame. If we are rendering, then the main output will
+ // overwrite that with the same frame (and possibly more frames).
+ aTrack->AddVideoOutput(mFirstFrameVideoOutput);
+ }
+ if (mRendering) {
+ aTrack->AddVideoOutput(mVideoContainer);
+ }
+ }
+
+ void RemoveTrack(AudioStreamTrack* aTrack) {
+ MOZ_DIAGNOSTIC_ASSERT(mAudioTracks.Contains(aTrack));
+ if (mRendering) {
+ aTrack->RemoveAudioOutput(mAudioOutputKey);
+ }
+ mAudioTracks.RemoveElement(aTrack);
+
+ if (mAudioTracks.IsEmpty()) {
+ // There is no longer an audio output that needs the device so the
+ // device may not start. Ensure the promise is resolved.
+ ResolveAudioDevicePromiseIfExists(__func__);
+ }
+ }
+ void RemoveTrack(VideoStreamTrack* aTrack) {
+ MOZ_DIAGNOSTIC_ASSERT(mVideoTrack == aTrack);
+ if (!mVideoContainer) {
+ return;
+ }
+ if (mFirstFrameVideoOutput) {
+ aTrack->RemoveVideoOutput(mFirstFrameVideoOutput);
+ }
+ if (mRendering) {
+ aTrack->RemoveVideoOutput(mVideoContainer);
+ }
+ mVideoTrack = nullptr;
+ }
+
+ double CurrentTime() const {
+ if (!mGraphTimeDummy) {
+ return 0.0;
+ }
+
+ return mGraphTimeDummy->mTrack->GraphImpl()->MediaTimeToSeconds(mGraphTime);
+ }
+
+ Watchable<GraphTime>& CurrentGraphTime() { return mGraphTime; }
+
+ // Set if we're rendering video.
+ const RefPtr<VideoFrameContainer> mVideoContainer;
+
+ // Set if we're rendering audio, nullptr otherwise.
+ void* const mAudioOutputKey;
+
+ private:
+ ~MediaStreamRenderer() { Shutdown(); }
+
+ void EnsureGraphTimeDummy() {
+ if (mGraphTimeDummy) {
+ return;
+ }
+
+ MediaTrackGraph* graph = nullptr;
+ for (const auto& t : mAudioTracks) {
+ if (t && !t->Ended()) {
+ graph = t->Graph();
+ break;
+ }
+ }
+
+ if (!graph && mVideoTrack && !mVideoTrack->Ended()) {
+ graph = mVideoTrack->Graph();
+ }
+
+ if (!graph) {
+ return;
+ }
+
+ // This dummy keeps `graph` alive and ensures access to it.
+ mGraphTimeDummy = MakeRefPtr<SharedDummyTrack>(
+ graph->CreateSourceTrack(MediaSegment::AUDIO));
+ }
+
+ void ResolveAudioDevicePromiseIfExists(const char* aMethodName) {
+ if (mSetAudioDevicePromise.IsEmpty()) {
+ return;
+ }
+ LOG(LogLevel::Info,
+ ("MediaStreamRenderer=%p resolve audio device promise", this));
+ mSetAudioDevicePromise.Resolve(true, aMethodName);
+ mDeviceStartedRequest.Disconnect();
+ }
+
+ // True when all tracks are being rendered, i.e., when the media element is
+ // playing.
+ bool mRendering = false;
+
+ // True while we're progressing mGraphTime. False otherwise.
+ bool mProgressingCurrentTime = false;
+
+ // The audio output volume for all audio tracks.
+ float mAudioOutputVolume = 1.0f;
+
+ // The sink device for all audio tracks.
+ RefPtr<AudioDeviceInfo> mAudioOutputSink;
+ // The promise returned from SetAudioOutputDevice() when an output is
+ // active.
+ MozPromiseHolder<GenericPromise> mSetAudioDevicePromise;
+ // Request tracking the promise to indicate when the device passed to
+ // SetAudioOutputDevice() is running.
+ MozPromiseRequestHolder<GenericPromise::AllSettledPromiseType>
+ mDeviceStartedRequest;
+
+ // WatchManager for mGraphTime.
+ WatchManager<MediaStreamRenderer> mWatchManager;
+
+ // A dummy MediaTrack to guarantee a MediaTrackGraph is kept alive while
+ // we're actively rendering, so we can track the graph's current time. Set
+ // when the first track is added, never unset.
+ RefPtr<SharedDummyTrack> mGraphTimeDummy;
+
+ // Watchable that relays the graph's currentTime updates to the media element
+ // only while we're rendering. This is the current time of the rendering in
+ // GraphTime units.
+ Watchable<GraphTime> mGraphTime = {0, "MediaStreamRenderer::mGraphTime"};
+
+ // Nothing until a track has been added. Then, the current GraphTime at the
+ // time when we were last Start()ed.
+ Maybe<GraphTime> mGraphTimeOffset;
+
+ // Currently enabled (and rendered) audio tracks.
+ nsTArray<WeakPtr<MediaStreamTrack>> mAudioTracks;
+
+ // Currently selected (and rendered) video track.
+ WeakPtr<MediaStreamTrack> mVideoTrack;
+
+ // Holds a reference to the first-frame-getting video output attached to
+ // mVideoTrack. Set by the constructor, unset when the media element tells us
+ // it has rendered the first frame.
+ RefPtr<FirstFrameVideoOutput> mFirstFrameVideoOutput;
+};
+
+static uint32_t sDecoderCaptureSourceId = 0;
+static uint32_t sStreamCaptureSourceId = 0;
+class HTMLMediaElement::MediaElementTrackSource
+ : public MediaStreamTrackSource,
+ public MediaStreamTrackSource::Sink,
+ public MediaStreamTrackConsumer {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaElementTrackSource,
+ MediaStreamTrackSource)
+
+ /* MediaDecoder track source */
+ MediaElementTrackSource(ProcessedMediaTrack* aTrack, nsIPrincipal* aPrincipal,
+ OutputMuteState aMuteState, bool aHasAlpha)
+ : MediaStreamTrackSource(
+ aPrincipal, nsString(),
+ TrackingId(TrackingId::Source::MediaElementDecoder,
+ sDecoderCaptureSourceId++,
+ TrackingId::TrackAcrossProcesses::Yes)),
+ mTrack(aTrack),
+ mIntendedElementMuteState(aMuteState),
+ mElementMuteState(aMuteState),
+ mMediaDecoderHasAlpha(Some(aHasAlpha)) {
+ MOZ_ASSERT(mTrack);
+ }
+
+ /* MediaStream track source */
+ MediaElementTrackSource(MediaStreamTrack* aCapturedTrack,
+ MediaStreamTrackSource* aCapturedTrackSource,
+ ProcessedMediaTrack* aTrack, MediaInputPort* aPort,
+ OutputMuteState aMuteState)
+ : MediaStreamTrackSource(
+ aCapturedTrackSource->GetPrincipal(), nsString(),
+ TrackingId(TrackingId::Source::MediaElementStream,
+ sStreamCaptureSourceId++,
+ TrackingId::TrackAcrossProcesses::Yes)),
+ mCapturedTrack(aCapturedTrack),
+ mCapturedTrackSource(aCapturedTrackSource),
+ mTrack(aTrack),
+ mPort(aPort),
+ mIntendedElementMuteState(aMuteState),
+ mElementMuteState(aMuteState) {
+ MOZ_ASSERT(mTrack);
+ MOZ_ASSERT(mCapturedTrack);
+ MOZ_ASSERT(mCapturedTrackSource);
+ MOZ_ASSERT(mPort);
+
+ mCapturedTrack->AddConsumer(this);
+ mCapturedTrackSource->RegisterSink(this);
+ }
+
+ void SetEnabled(bool aEnabled) {
+ if (!mTrack) {
+ return;
+ }
+ mTrack->SetDisabledTrackMode(aEnabled ? DisabledTrackMode::ENABLED
+ : DisabledTrackMode::SILENCE_FREEZE);
+ }
+
+ void SetPrincipal(RefPtr<nsIPrincipal> aPrincipal) {
+ mPrincipal = std::move(aPrincipal);
+ MediaStreamTrackSource::PrincipalChanged();
+ }
+
+ void SetMutedByElement(OutputMuteState aMuteState) {
+ if (mIntendedElementMuteState == aMuteState) {
+ return;
+ }
+ mIntendedElementMuteState = aMuteState;
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "MediaElementTrackSource::SetMutedByElement",
+ [self = RefPtr<MediaElementTrackSource>(this), this, aMuteState] {
+ mElementMuteState = aMuteState;
+ MediaStreamTrackSource::MutedChanged(Muted());
+ }));
+ }
+
+ void Destroy() override {
+ if (mCapturedTrack) {
+ mCapturedTrack->RemoveConsumer(this);
+ mCapturedTrack = nullptr;
+ }
+ if (mCapturedTrackSource) {
+ mCapturedTrackSource->UnregisterSink(this);
+ mCapturedTrackSource = nullptr;
+ }
+ if (mTrack && !mTrack->IsDestroyed()) {
+ mTrack->Destroy();
+ }
+ if (mPort) {
+ mPort->Destroy();
+ mPort = nullptr;
+ }
+ }
+
+ MediaSourceEnum GetMediaSource() const override {
+ return MediaSourceEnum::Other;
+ }
+
+ void Stop() override {
+ // Do nothing. There may appear new output streams
+ // that need tracks sourced from this source, so we
+ // cannot destroy things yet.
+ }
+
+ /**
+ * Do not keep the track source alive. The source lifetime is controlled by
+ * its associated tracks.
+ */
+ bool KeepsSourceAlive() const override { return false; }
+
+ /**
+ * Do not keep the track source on. It is controlled by its associated tracks.
+ */
+ bool Enabled() const override { return false; }
+
+ void Disable() override {}
+
+ void Enable() override {}
+
+ void PrincipalChanged() override {
+ if (!mCapturedTrackSource) {
+ // This could happen during shutdown.
+ return;
+ }
+
+ SetPrincipal(mCapturedTrackSource->GetPrincipal());
+ }
+
+ void MutedChanged(bool aNewState) override {
+ MediaStreamTrackSource::MutedChanged(Muted());
+ }
+
+ void OverrideEnded() override {
+ Destroy();
+ MediaStreamTrackSource::OverrideEnded();
+ }
+
+ void NotifyEnabledChanged(MediaStreamTrack* aTrack, bool aEnabled) override {
+ MediaStreamTrackSource::MutedChanged(Muted());
+ }
+
+ bool Muted() const {
+ return mElementMuteState == OutputMuteState::Muted ||
+ (mCapturedTrack &&
+ (mCapturedTrack->Muted() || !mCapturedTrack->Enabled()));
+ }
+
+ bool HasAlpha() const override {
+ if (mCapturedTrack) {
+ return mCapturedTrack->AsVideoStreamTrack()
+ ? mCapturedTrack->AsVideoStreamTrack()->HasAlpha()
+ : false;
+ }
+ return mMediaDecoderHasAlpha.valueOr(false);
+ }
+
+ ProcessedMediaTrack* Track() const { return mTrack; }
+
+ private:
+ virtual ~MediaElementTrackSource() { Destroy(); };
+
+ RefPtr<MediaStreamTrack> mCapturedTrack;
+ RefPtr<MediaStreamTrackSource> mCapturedTrackSource;
+ const RefPtr<ProcessedMediaTrack> mTrack;
+ RefPtr<MediaInputPort> mPort;
+ // The mute state as intended by the media element.
+ OutputMuteState mIntendedElementMuteState;
+ // The mute state as applied to this track source. It is applied async, so
+ // needs to be tracked separately from the intended state.
+ OutputMuteState mElementMuteState;
+ // Some<bool> if this is a MediaDecoder track source.
+ const Maybe<bool> mMediaDecoderHasAlpha;
+};
+
+HTMLMediaElement::OutputMediaStream::OutputMediaStream(
+ RefPtr<DOMMediaStream> aStream, bool aCapturingAudioOnly,
+ bool aFinishWhenEnded)
+ : mStream(std::move(aStream)),
+ mCapturingAudioOnly(aCapturingAudioOnly),
+ mFinishWhenEnded(aFinishWhenEnded) {}
+HTMLMediaElement::OutputMediaStream::~OutputMediaStream() = default;
+
+void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
+ HTMLMediaElement::OutputMediaStream& aField,
+ const char* aName, uint32_t aFlags) {
+ ImplCycleCollectionTraverse(aCallback, aField.mStream, "mStream", aFlags);
+ ImplCycleCollectionTraverse(aCallback, aField.mLiveTracks, "mLiveTracks",
+ aFlags);
+ ImplCycleCollectionTraverse(aCallback, aField.mFinishWhenEndedLoadingSrc,
+ "mFinishWhenEndedLoadingSrc", aFlags);
+ ImplCycleCollectionTraverse(aCallback, aField.mFinishWhenEndedAttrStream,
+ "mFinishWhenEndedAttrStream", aFlags);
+ ImplCycleCollectionTraverse(aCallback, aField.mFinishWhenEndedMediaSource,
+ "mFinishWhenEndedMediaSource", aFlags);
+}
+
+void ImplCycleCollectionUnlink(HTMLMediaElement::OutputMediaStream& aField) {
+ ImplCycleCollectionUnlink(aField.mStream);
+ ImplCycleCollectionUnlink(aField.mLiveTracks);
+ ImplCycleCollectionUnlink(aField.mFinishWhenEndedLoadingSrc);
+ ImplCycleCollectionUnlink(aField.mFinishWhenEndedAttrStream);
+ ImplCycleCollectionUnlink(aField.mFinishWhenEndedMediaSource);
+}
+
+NS_IMPL_ADDREF_INHERITED(HTMLMediaElement::MediaElementTrackSource,
+ MediaStreamTrackSource)
+NS_IMPL_RELEASE_INHERITED(HTMLMediaElement::MediaElementTrackSource,
+ MediaStreamTrackSource)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
+ HTMLMediaElement::MediaElementTrackSource)
+NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource)
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement::MediaElementTrackSource)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(
+ HTMLMediaElement::MediaElementTrackSource, MediaStreamTrackSource)
+ tmp->Destroy();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mCapturedTrack)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mCapturedTrackSource)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(
+ HTMLMediaElement::MediaElementTrackSource, MediaStreamTrackSource)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCapturedTrack)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCapturedTrackSource)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+/**
+ * There is a reference cycle involving this class: MediaLoadListener
+ * holds a reference to the HTMLMediaElement, which holds a reference
+ * to an nsIChannel, which holds a reference to this listener.
+ * We break the reference cycle in OnStartRequest by clearing mElement.
+ */
+class HTMLMediaElement::MediaLoadListener final
+ : public nsIChannelEventSink,
+ public nsIInterfaceRequestor,
+ public nsIObserver,
+ public nsIThreadRetargetableStreamListener {
+ ~MediaLoadListener() = default;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
+
+ public:
+ explicit MediaLoadListener(HTMLMediaElement* aElement)
+ : mElement(aElement), mLoadID(aElement->GetCurrentLoadID()) {
+ MOZ_ASSERT(mElement, "Must pass an element to call back");
+ }
+
+ private:
+ RefPtr<HTMLMediaElement> mElement;
+ nsCOMPtr<nsIStreamListener> mNextListener;
+ const uint32_t mLoadID;
+};
+
+NS_IMPL_ISUPPORTS(HTMLMediaElement::MediaLoadListener, nsIRequestObserver,
+ nsIStreamListener, nsIChannelEventSink, nsIInterfaceRequestor,
+ nsIObserver, nsIThreadRetargetableStreamListener)
+
+NS_IMETHODIMP
+HTMLMediaElement::MediaLoadListener::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ nsContentUtils::UnregisterShutdownObserver(this);
+
+ // Clear mElement to break cycle so we don't leak on shutdown
+ mElement = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLMediaElement::MediaLoadListener::OnStartRequest(nsIRequest* aRequest) {
+ nsContentUtils::UnregisterShutdownObserver(this);
+
+ if (!mElement) {
+ // We've been notified by the shutdown observer, and are shutting down.
+ return NS_BINDING_ABORTED;
+ }
+
+ // The element is only needed until we've had a chance to call
+ // InitializeDecoderForChannel. So make sure mElement is cleared here.
+ RefPtr<HTMLMediaElement> element;
+ element.swap(mElement);
+
+ if (mLoadID != element->GetCurrentLoadID()) {
+ // The channel has been cancelled before we had a chance to create
+ // a decoder. Abort, don't dispatch an "error" event, as the new load
+ // may not be in an error state.
+ return NS_BINDING_ABORTED;
+ }
+
+ // Don't continue to load if the request failed or has been canceled.
+ nsresult status;
+ nsresult rv = aRequest->GetStatus(&status);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (NS_FAILED(status)) {
+ if (element) {
+ // Handle media not loading error because source was a tracking URL (or
+ // fingerprinting, cryptomining, etc).
+ // We make a note of this media node by including it in a dedicated
+ // array of blocked tracking nodes under its parent document.
+ if (net::UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(
+ status)) {
+ element->OwnerDoc()->AddBlockedNodeByClassifier(element);
+ }
+ element->NotifyLoadError(
+ nsPrintfCString("%u: %s", uint32_t(status), "Request failed"));
+ }
+ return status;
+ }
+
+ nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(aRequest);
+ bool succeeded;
+ if (hc && NS_SUCCEEDED(hc->GetRequestSucceeded(&succeeded)) && !succeeded) {
+ uint32_t responseStatus = 0;
+ Unused << hc->GetResponseStatus(&responseStatus);
+ nsAutoCString statusText;
+ Unused << hc->GetResponseStatusText(statusText);
+ // we need status text for resist fingerprinting mode's message allowlist
+ if (statusText.IsEmpty()) {
+ net_GetDefaultStatusTextForCode(responseStatus, statusText);
+ }
+ element->NotifyLoadError(
+ nsPrintfCString("%u: %s", responseStatus, statusText.get()));
+
+ nsAutoString code;
+ code.AppendInt(responseStatus);
+ nsAutoString src;
+ element->GetCurrentSrc(src);
+ AutoTArray<nsString, 2> params = {code, src};
+ element->ReportLoadError("MediaLoadHttpError", params);
+ return NS_BINDING_ABORTED;
+ }
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ if (channel &&
+ NS_SUCCEEDED(rv = element->InitializeDecoderForChannel(
+ channel, getter_AddRefs(mNextListener))) &&
+ mNextListener) {
+ rv = mNextListener->OnStartRequest(aRequest);
+ } else {
+ // If InitializeDecoderForChannel() returned an error, fire a network error.
+ if (NS_FAILED(rv) && !mNextListener) {
+ // Load failed, attempt to load the next candidate resource. If there
+ // are none, this will trigger a MEDIA_ERR_SRC_NOT_SUPPORTED error.
+ element->NotifyLoadError("Failed to init decoder"_ns);
+ }
+ // If InitializeDecoderForChannel did not return a listener (but may
+ // have otherwise succeeded), we abort the connection since we aren't
+ // interested in keeping the channel alive ourselves.
+ rv = NS_BINDING_ABORTED;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLMediaElement::MediaLoadListener::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatus) {
+ if (mNextListener) {
+ return mNextListener->OnStopRequest(aRequest, aStatus);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLMediaElement::MediaLoadListener::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aStream,
+ uint64_t aOffset,
+ uint32_t aCount) {
+ if (!mNextListener) {
+ NS_ERROR(
+ "Must have a chained listener; OnStartRequest should have "
+ "canceled this request");
+ return NS_BINDING_ABORTED;
+ }
+ return mNextListener->OnDataAvailable(aRequest, aStream, aOffset, aCount);
+}
+
+NS_IMETHODIMP
+HTMLMediaElement::MediaLoadListener::OnDataFinished(nsresult aStatus) {
+ if (!mNextListener) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable =
+ do_QueryInterface(mNextListener);
+ if (retargetable) {
+ return retargetable->OnDataFinished(aStatus);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLMediaElement::MediaLoadListener::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* cb) {
+ // TODO is this really correct?? See bug #579329.
+ if (mElement) {
+ mElement->OnChannelRedirect(aOldChannel, aNewChannel, aFlags);
+ }
+ nsCOMPtr<nsIChannelEventSink> sink = do_QueryInterface(mNextListener);
+ if (sink) {
+ return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, cb);
+ }
+ cb->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLMediaElement::MediaLoadListener::CheckListenerChain() {
+ MOZ_ASSERT(mNextListener);
+ nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable =
+ do_QueryInterface(mNextListener);
+ if (retargetable) {
+ return retargetable->CheckListenerChain();
+ }
+ return NS_ERROR_NO_INTERFACE;
+}
+
+NS_IMETHODIMP
+HTMLMediaElement::MediaLoadListener::GetInterface(const nsIID& aIID,
+ void** aResult) {
+ return QueryInterface(aIID, aResult);
+}
+
+void HTMLMediaElement::ReportLoadError(const char* aMsg,
+ const nsTArray<nsString>& aParams) {
+ ReportToConsole(nsIScriptError::warningFlag, aMsg, aParams);
+}
+
+void HTMLMediaElement::ReportToConsole(
+ uint32_t aErrorFlags, const char* aMsg,
+ const nsTArray<nsString>& aParams) const {
+ nsContentUtils::ReportToConsole(aErrorFlags, "Media"_ns, OwnerDoc(),
+ nsContentUtils::eDOM_PROPERTIES, aMsg,
+ aParams);
+}
+
+class HTMLMediaElement::AudioChannelAgentCallback final
+ : public nsIAudioChannelAgentCallback {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(AudioChannelAgentCallback)
+
+ explicit AudioChannelAgentCallback(HTMLMediaElement* aOwner)
+ : mOwner(aOwner),
+ mAudioChannelVolume(1.0),
+ mPlayingThroughTheAudioChannel(false),
+ mIsOwnerAudible(IsOwnerAudible()),
+ mIsShutDown(false) {
+ MOZ_ASSERT(mOwner);
+ MaybeCreateAudioChannelAgent();
+ }
+
+ void UpdateAudioChannelPlayingState() {
+ MOZ_ASSERT(!mIsShutDown);
+ bool playingThroughTheAudioChannel = IsPlayingThroughTheAudioChannel();
+
+ if (playingThroughTheAudioChannel != mPlayingThroughTheAudioChannel) {
+ if (!MaybeCreateAudioChannelAgent()) {
+ return;
+ }
+
+ mPlayingThroughTheAudioChannel = playingThroughTheAudioChannel;
+ if (mPlayingThroughTheAudioChannel) {
+ StartAudioChannelAgent();
+ } else {
+ StopAudioChanelAgent();
+ }
+ }
+ }
+
+ void NotifyPlayStateChanged() {
+ MOZ_ASSERT(!mIsShutDown);
+ UpdateAudioChannelPlayingState();
+ }
+
+ NS_IMETHODIMP WindowVolumeChanged(float aVolume, bool aMuted) override {
+ MOZ_ASSERT(mAudioChannelAgent);
+
+ MOZ_LOG(
+ AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
+ ("HTMLMediaElement::AudioChannelAgentCallback, WindowVolumeChanged, "
+ "this = %p, aVolume = %f, aMuted = %s\n",
+ this, aVolume, aMuted ? "true" : "false"));
+
+ if (mAudioChannelVolume != aVolume) {
+ mAudioChannelVolume = aVolume;
+ mOwner->SetVolumeInternal();
+ }
+
+ const uint32_t muted = mOwner->mMuted;
+ if (aMuted && !mOwner->ComputedMuted()) {
+ mOwner->SetMutedInternal(muted | MUTED_BY_AUDIO_CHANNEL);
+ } else if (!aMuted && mOwner->ComputedMuted()) {
+ mOwner->SetMutedInternal(muted & ~MUTED_BY_AUDIO_CHANNEL);
+ }
+
+ return NS_OK;
+ }
+
+ NS_IMETHODIMP WindowSuspendChanged(SuspendTypes aSuspend) override {
+ // Currently this method is only be used for delaying autoplay, and we've
+ // separated related codes to `MediaPlaybackDelayPolicy`.
+ return NS_OK;
+ }
+
+ NS_IMETHODIMP WindowAudioCaptureChanged(bool aCapture) override {
+ MOZ_ASSERT(mAudioChannelAgent);
+ AudioCaptureTrackChangeIfNeeded();
+ return NS_OK;
+ }
+
+ void AudioCaptureTrackChangeIfNeeded() {
+ MOZ_ASSERT(!mIsShutDown);
+ if (!IsPlayingStarted()) {
+ return;
+ }
+
+ MOZ_ASSERT(mAudioChannelAgent);
+ bool isCapturing = mAudioChannelAgent->IsWindowAudioCapturingEnabled();
+ mOwner->AudioCaptureTrackChange(isCapturing);
+ }
+
+ void NotifyAudioPlaybackChanged(AudibleChangedReasons aReason) {
+ MOZ_ASSERT(!mIsShutDown);
+ AudibleState newAudibleState = IsOwnerAudible();
+ MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
+ ("HTMLMediaElement::AudioChannelAgentCallback, "
+ "NotifyAudioPlaybackChanged, this=%p, current=%s, new=%s",
+ this, AudibleStateToStr(mIsOwnerAudible),
+ AudibleStateToStr(newAudibleState)));
+ if (mIsOwnerAudible == newAudibleState) {
+ return;
+ }
+
+ mIsOwnerAudible = newAudibleState;
+ if (IsPlayingStarted()) {
+ mAudioChannelAgent->NotifyStartedAudible(mIsOwnerAudible, aReason);
+ }
+ }
+
+ void Shutdown() {
+ MOZ_ASSERT(!mIsShutDown);
+ if (mAudioChannelAgent && mAudioChannelAgent->IsPlayingStarted()) {
+ StopAudioChanelAgent();
+ }
+ mAudioChannelAgent = nullptr;
+ mIsShutDown = true;
+ }
+
+ float GetEffectiveVolume() const {
+ MOZ_ASSERT(!mIsShutDown);
+ return static_cast<float>(mOwner->Volume()) * mAudioChannelVolume;
+ }
+
+ private:
+ ~AudioChannelAgentCallback() { MOZ_ASSERT(mIsShutDown); };
+
+ bool MaybeCreateAudioChannelAgent() {
+ if (mAudioChannelAgent) {
+ return true;
+ }
+
+ mAudioChannelAgent = new AudioChannelAgent();
+ nsresult rv =
+ mAudioChannelAgent->Init(mOwner->OwnerDoc()->GetInnerWindow(), this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mAudioChannelAgent = nullptr;
+ MOZ_LOG(
+ AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
+ ("HTMLMediaElement::AudioChannelAgentCallback, Fail to initialize "
+ "the audio channel agent, this = %p\n",
+ this));
+ return false;
+ }
+
+ return true;
+ }
+
+ void StartAudioChannelAgent() {
+ MOZ_ASSERT(mAudioChannelAgent);
+ MOZ_ASSERT(!mAudioChannelAgent->IsPlayingStarted());
+ if (NS_WARN_IF(NS_FAILED(
+ mAudioChannelAgent->NotifyStartedPlaying(IsOwnerAudible())))) {
+ return;
+ }
+ mAudioChannelAgent->PullInitialUpdate();
+ }
+
+ void StopAudioChanelAgent() {
+ MOZ_ASSERT(mAudioChannelAgent);
+ MOZ_ASSERT(mAudioChannelAgent->IsPlayingStarted());
+ mAudioChannelAgent->NotifyStoppedPlaying();
+ // If we have started audio capturing before, we have to tell media element
+ // to clear the output capturing track.
+ mOwner->AudioCaptureTrackChange(false);
+ }
+
+ bool IsPlayingStarted() {
+ if (MaybeCreateAudioChannelAgent()) {
+ return mAudioChannelAgent->IsPlayingStarted();
+ }
+ return false;
+ }
+
+ AudibleState IsOwnerAudible() const {
+ // paused media doesn't produce any sound.
+ if (mOwner->mPaused) {
+ return AudibleState::eNotAudible;
+ }
+ return mOwner->IsAudible() ? AudibleState::eAudible
+ : AudibleState::eNotAudible;
+ }
+
+ bool IsPlayingThroughTheAudioChannel() const {
+ // If we have an error, we are not playing.
+ if (mOwner->GetError()) {
+ return false;
+ }
+
+ // We should consider any bfcached page or inactive document as non-playing.
+ if (!mOwner->OwnerDoc()->IsActive()) {
+ return false;
+ }
+
+ // Media is suspended by the docshell.
+ if (mOwner->ShouldBeSuspendedByInactiveDocShell()) {
+ return false;
+ }
+
+ // Are we paused
+ if (mOwner->mPaused) {
+ return false;
+ }
+
+ // No audio track
+ if (!mOwner->HasAudio()) {
+ return false;
+ }
+
+ // A loop always is playing
+ if (mOwner->HasAttr(nsGkAtoms::loop)) {
+ return true;
+ }
+
+ // If we are actually playing...
+ if (mOwner->IsCurrentlyPlaying()) {
+ return true;
+ }
+
+ // If we are playing an external stream.
+ if (mOwner->mSrcAttrStream) {
+ return true;
+ }
+
+ return false;
+ }
+
+ RefPtr<AudioChannelAgent> mAudioChannelAgent;
+ HTMLMediaElement* mOwner;
+
+ // The audio channel volume
+ float mAudioChannelVolume;
+ // Is this media element playing?
+ bool mPlayingThroughTheAudioChannel;
+ // Indicate whether media element is audible for users.
+ AudibleState mIsOwnerAudible;
+ bool mIsShutDown;
+};
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement::AudioChannelAgentCallback)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
+ HTMLMediaElement::AudioChannelAgentCallback)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelAgent)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(
+ HTMLMediaElement::AudioChannelAgentCallback)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelAgent)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
+ HTMLMediaElement::AudioChannelAgentCallback)
+ NS_INTERFACE_MAP_ENTRY(nsIAudioChannelAgentCallback)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLMediaElement::AudioChannelAgentCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLMediaElement::AudioChannelAgentCallback)
+
+class HTMLMediaElement::ChannelLoader final {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ChannelLoader);
+
+ void LoadInternal(HTMLMediaElement* aElement) {
+ if (mCancelled) {
+ return;
+ }
+
+ // determine what security checks need to be performed in AsyncOpen().
+ nsSecurityFlags securityFlags =
+ aElement->ShouldCheckAllowOrigin()
+ ? nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT
+ : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
+
+ if (aElement->GetCORSMode() == CORS_USE_CREDENTIALS) {
+ securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+ }
+
+ securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME;
+
+ MOZ_ASSERT(
+ aElement->IsAnyOfHTMLElements(nsGkAtoms::audio, nsGkAtoms::video));
+ nsContentPolicyType contentPolicyType =
+ aElement->IsHTMLElement(nsGkAtoms::audio)
+ ? nsIContentPolicy::TYPE_INTERNAL_AUDIO
+ : nsIContentPolicy::TYPE_INTERNAL_VIDEO;
+
+ // If aElement has 'triggeringprincipal' attribute, we will use the value as
+ // triggeringPrincipal for the channel, otherwise it will default to use
+ // aElement->NodePrincipal().
+ // This function returns true when aElement has 'triggeringprincipal', so if
+ // setAttrs is true we will override the origin attributes on the channel
+ // later.
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal;
+ bool setAttrs = nsContentUtils::QueryTriggeringPrincipal(
+ aElement, aElement->mLoadingSrcTriggeringPrincipal,
+ getter_AddRefs(triggeringPrincipal));
+
+ nsCOMPtr<nsILoadGroup> loadGroup = aElement->GetDocumentLoadGroup();
+ nsCOMPtr<nsIChannel> channel;
+ nsresult rv = NS_NewChannelWithTriggeringPrincipal(
+ getter_AddRefs(channel), aElement->mLoadingSrc,
+ static_cast<Element*>(aElement), triggeringPrincipal, securityFlags,
+ contentPolicyType,
+ nullptr, // aPerformanceStorage
+ loadGroup,
+ nullptr, // aCallbacks
+ nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY |
+ nsIChannel::LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE |
+ nsIChannel::LOAD_CALL_CONTENT_SNIFFERS);
+
+ if (NS_FAILED(rv)) {
+ // Notify load error so the element will try next resource candidate.
+ aElement->NotifyLoadError("Fail to create channel"_ns);
+ return;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ if (setAttrs) {
+ // The function simply returns NS_OK, so we ignore the return value.
+ Unused << loadInfo->SetOriginAttributes(
+ triggeringPrincipal->OriginAttributesRef());
+ }
+ loadInfo->SetIsMediaRequest(true);
+ loadInfo->SetIsMediaInitialRequest(true);
+
+ nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(channel));
+ if (cos) {
+ if (aElement->mUseUrgentStartForChannel) {
+ cos->AddClassFlags(nsIClassOfService::UrgentStart);
+
+ // Reset the flag to avoid loading again without initiated by user
+ // interaction.
+ aElement->mUseUrgentStartForChannel = false;
+ }
+
+ // Unconditionally disable throttling since we want the media to fluently
+ // play even when we switch the tab to background.
+ cos->AddClassFlags(nsIClassOfService::DontThrottle);
+ }
+
+ // The listener holds a strong reference to us. This creates a
+ // reference cycle, once we've set mChannel, which is manually broken
+ // in the listener's OnStartRequest method after it is finished with
+ // the element. The cycle will also be broken if we get a shutdown
+ // notification before OnStartRequest fires. Necko guarantees that
+ // OnStartRequest will eventually fire if we don't shut down first.
+ RefPtr<MediaLoadListener> loadListener = new MediaLoadListener(aElement);
+
+ channel->SetNotificationCallbacks(loadListener);
+
+ nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(channel);
+ if (hc) {
+ // Use a byte range request from the start of the resource.
+ // This enables us to detect if the stream supports byte range
+ // requests, and therefore seeking, early.
+ rv = hc->SetRequestHeader("Range"_ns, "bytes=0-"_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ aElement->SetRequestHeaders(hc);
+ }
+
+ rv = channel->AsyncOpen(loadListener);
+ if (NS_FAILED(rv)) {
+ // Notify load error so the element will try next resource candidate.
+ aElement->NotifyLoadError("Failed to open channel"_ns);
+ return;
+ }
+
+ // Else the channel must be open and starting to download. If it encounters
+ // a non-catastrophic failure, it will set a new task to continue loading
+ // another candidate. It's safe to set it as mChannel now.
+ mChannel = channel;
+
+ // loadListener will be unregistered either on shutdown or when
+ // OnStartRequest for the channel we just opened fires.
+ nsContentUtils::RegisterShutdownObserver(loadListener);
+ }
+
+ nsresult Load(HTMLMediaElement* aElement) {
+ MOZ_ASSERT(aElement);
+ // Per bug 1235183 comment 8, we can't spin the event loop from stable
+ // state. Defer NS_NewChannel() to a new regular runnable.
+ return aElement->OwnerDoc()->Dispatch(NewRunnableMethod<HTMLMediaElement*>(
+ "ChannelLoader::LoadInternal", this, &ChannelLoader::LoadInternal,
+ aElement));
+ }
+
+ void Cancel() {
+ mCancelled = true;
+ if (mChannel) {
+ mChannel->CancelWithReason(NS_BINDING_ABORTED,
+ "HTMLMediaElement::ChannelLoader::Cancel"_ns);
+ mChannel = nullptr;
+ }
+ }
+
+ void Done() {
+ MOZ_ASSERT(mChannel);
+ // Decoder successfully created, the decoder now owns the MediaResource
+ // which owns the channel.
+ mChannel = nullptr;
+ }
+
+ nsresult Redirect(nsIChannel* aChannel, nsIChannel* aNewChannel,
+ uint32_t aFlags) {
+ NS_ASSERTION(aChannel == mChannel, "Channels should match!");
+ mChannel = aNewChannel;
+
+ // Handle forwarding of Range header so that the intial detection
+ // of seeking support (via result code 206) works across redirects.
+ nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel);
+ NS_ENSURE_STATE(http);
+
+ constexpr auto rangeHdr = "Range"_ns;
+
+ nsAutoCString rangeVal;
+ if (NS_SUCCEEDED(http->GetRequestHeader(rangeHdr, rangeVal))) {
+ NS_ENSURE_STATE(!rangeVal.IsEmpty());
+
+ http = do_QueryInterface(aNewChannel);
+ NS_ENSURE_STATE(http);
+
+ nsresult rv = http->SetRequestHeader(rangeHdr, rangeVal, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ ~ChannelLoader() { MOZ_ASSERT(!mChannel); }
+ // Holds a reference to the first channel we open to the media resource.
+ // Once the decoder is created, control over the channel passes to the
+ // decoder, and we null out this reference. We must store this in case
+ // we need to cancel the channel before control of it passes to the decoder.
+ nsCOMPtr<nsIChannel> mChannel;
+
+ bool mCancelled = false;
+};
+
+class HTMLMediaElement::ErrorSink {
+ public:
+ explicit ErrorSink(HTMLMediaElement* aOwner) : mOwner(aOwner) {
+ MOZ_ASSERT(mOwner);
+ }
+
+ void SetError(uint16_t aErrorCode, const nsACString& aErrorDetails) {
+ // Since we have multiple paths calling into DecodeError, e.g.
+ // MediaKeys::Terminated and EMEH264Decoder::Error. We should take the 1st
+ // one only in order not to fire multiple 'error' events.
+ if (mError) {
+ return;
+ }
+
+ if (!IsValidErrorCode(aErrorCode)) {
+ NS_ASSERTION(false, "Undefined MediaError codes!");
+ return;
+ }
+
+ mError = new MediaError(mOwner, aErrorCode, aErrorDetails);
+ mOwner->DispatchAsyncEvent(u"error"_ns);
+ if (mOwner->ReadyState() == HAVE_NOTHING &&
+ aErrorCode == MEDIA_ERR_ABORTED) {
+ // https://html.spec.whatwg.org/multipage/embedded-content.html#media-data-processing-steps-list
+ // "If the media data fetching process is aborted by the user"
+ mOwner->DispatchAsyncEvent(u"abort"_ns);
+ mOwner->ChangeNetworkState(NETWORK_EMPTY);
+ mOwner->DispatchAsyncEvent(u"emptied"_ns);
+ if (mOwner->mDecoder) {
+ mOwner->ShutdownDecoder();
+ }
+ } else if (aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED) {
+ mOwner->ChangeNetworkState(NETWORK_NO_SOURCE);
+ } else {
+ mOwner->ChangeNetworkState(NETWORK_IDLE);
+ }
+ }
+
+ void ResetError() { mError = nullptr; }
+
+ RefPtr<MediaError> mError;
+
+ private:
+ bool IsValidErrorCode(const uint16_t& aErrorCode) const {
+ return (aErrorCode == MEDIA_ERR_DECODE || aErrorCode == MEDIA_ERR_NETWORK ||
+ aErrorCode == MEDIA_ERR_ABORTED ||
+ aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED);
+ }
+
+ // Media elememt's life cycle would be longer than error sink, so we use the
+ // raw pointer and this class would only be referenced by media element.
+ HTMLMediaElement* mOwner;
+};
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLMediaElement,
+ nsGenericHTMLElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaSource)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcMediaSource)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcStream)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcAttrStream)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourcePointer)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLoadBlockedDoc)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceLoadCandidate)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelWrapper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mErrorSink->mError)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputStreams)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputTrackSources);
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlayed);
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextTrackManager)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioTrackList)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVideoTrackList)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeys)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingMediaKeys)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedVideoStreamTrack)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingPlayPromises)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSeekDOMPromise)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSetMediaKeysDOMPromise)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventBlocker)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement,
+ nsGenericHTMLElement)
+ tmp->RemoveMutationObserver(tmp);
+ if (tmp->mSrcStream) {
+ // Need to unhook everything that EndSrcMediaStreamPlayback would normally
+ // do, without creating any new strong references.
+ if (tmp->mSelectedVideoStreamTrack) {
+ tmp->mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(tmp);
+ }
+ if (tmp->mMediaStreamRenderer) {
+ tmp->mMediaStreamRenderer->Shutdown();
+ // We null out mMediaStreamRenderer here since Shutdown() will shut down
+ // its WatchManager, and UpdateSrcStreamPotentiallyPlaying() contains a
+ // guard for this.
+ tmp->mMediaStreamRenderer = nullptr;
+ }
+ if (tmp->mSecondaryMediaStreamRenderer) {
+ tmp->mSecondaryMediaStreamRenderer->Shutdown();
+ tmp->mSecondaryMediaStreamRenderer = nullptr;
+ }
+ if (tmp->mMediaStreamTrackListener) {
+ tmp->mSrcStream->UnregisterTrackListener(
+ tmp->mMediaStreamTrackListener.get());
+ }
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcStream)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcAttrStream)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcMediaSource)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourcePointer)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoadBlockedDoc)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceLoadCandidate)
+ if (tmp->mAudioChannelWrapper) {
+ tmp->mAudioChannelWrapper->Shutdown();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelWrapper)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mErrorSink->mError)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputStreams)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputTrackSources)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlayed)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioTrackList)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mVideoTrackList)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingMediaKeys)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedVideoStreamTrack)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingPlayPromises)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSeekDOMPromise)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSetMediaKeysDOMPromise)
+ if (tmp->mMediaControlKeyListener) {
+ tmp->mMediaControlKeyListener->StopIfNeeded();
+ }
+ if (tmp->mEventBlocker) {
+ tmp->mEventBlocker->Shutdown();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLMediaElement,
+ nsGenericHTMLElement)
+
+void HTMLMediaElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes,
+ size_t* aNodeSize) const {
+ nsGenericHTMLElement::AddSizeOfExcludingThis(aSizes, aNodeSize);
+
+ // There are many other fields that might be worth reporting, but as seen in
+ // bug 1595603, the event we postpone to dispatch can grow to be very large
+ // sometimes, so at least report that.
+ if (mEventBlocker) {
+ *aNodeSize +=
+ mEventBlocker->SizeOfExcludingThis(aSizes.mState.mMallocSizeOf);
+ }
+}
+
+void HTMLMediaElement::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ if (aChild == mSourcePointer) {
+ mSourcePointer = aPreviousSibling;
+ }
+}
+
+already_AddRefed<MediaSource> HTMLMediaElement::GetMozMediaSourceObject()
+ const {
+ RefPtr<MediaSource> source = mMediaSource;
+ return source.forget();
+}
+
+already_AddRefed<Promise> HTMLMediaElement::MozRequestDebugInfo(
+ ErrorResult& aRv) {
+ RefPtr<Promise> promise = CreateDOMPromise(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ auto result = MakeUnique<dom::HTMLMediaElementDebugInfo>();
+ if (mMediaKeys) {
+ GetEMEInfo(result->mEMEInfo);
+ }
+ if (mVideoFrameContainer) {
+ result->mCompositorDroppedFrames =
+ mVideoFrameContainer->GetDroppedImageCount();
+ }
+ if (mDecoder) {
+ mDecoder->RequestDebugInfo(result->mDecoder)
+ ->Then(
+ AbstractMainThread(), __func__,
+ [promise, ptr = std::move(result)]() {
+ promise->MaybeResolve(ptr.get());
+ },
+ []() {
+ MOZ_ASSERT_UNREACHABLE("Unexpected RequestDebugInfo() rejection");
+ });
+ } else {
+ promise->MaybeResolve(result.get());
+ }
+ return promise.forget();
+}
+
+/* static */
+void HTMLMediaElement::MozEnableDebugLog(const GlobalObject&) {
+ DecoderDoctorLogger::EnableLogging();
+}
+
+already_AddRefed<Promise> HTMLMediaElement::MozRequestDebugLog(
+ ErrorResult& aRv) {
+ RefPtr<Promise> promise = CreateDOMPromise(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ DecoderDoctorLogger::RetrieveMessages(this)->Then(
+ AbstractMainThread(), __func__,
+ [promise](const nsACString& aString) {
+ promise->MaybeResolve(NS_ConvertUTF8toUTF16(aString));
+ },
+ [promise](nsresult rv) { promise->MaybeReject(rv); });
+
+ return promise.forget();
+}
+
+void HTMLMediaElement::SetVisible(bool aVisible) {
+ mForcedHidden = !aVisible;
+ if (mDecoder) {
+ mDecoder->SetForcedHidden(!aVisible);
+ }
+}
+
+bool HTMLMediaElement::IsVideoDecodingSuspended() const {
+ return mDecoder && mDecoder->IsVideoDecodingSuspended();
+}
+
+double HTMLMediaElement::TotalVideoPlayTime() const {
+ return mDecoder ? mDecoder->GetTotalVideoPlayTimeInSeconds() : -1.0;
+}
+
+double HTMLMediaElement::TotalVideoHDRPlayTime() const {
+ return mDecoder ? mDecoder->GetTotalVideoHDRPlayTimeInSeconds() : -1.0;
+}
+
+double HTMLMediaElement::VisiblePlayTime() const {
+ return mDecoder ? mDecoder->GetVisibleVideoPlayTimeInSeconds() : -1.0;
+}
+
+double HTMLMediaElement::InvisiblePlayTime() const {
+ return mDecoder ? mDecoder->GetInvisibleVideoPlayTimeInSeconds() : -1.0;
+}
+
+double HTMLMediaElement::TotalAudioPlayTime() const {
+ return mDecoder ? mDecoder->GetTotalAudioPlayTimeInSeconds() : -1.0;
+}
+
+double HTMLMediaElement::AudiblePlayTime() const {
+ return mDecoder ? mDecoder->GetAudiblePlayTimeInSeconds() : -1.0;
+}
+
+double HTMLMediaElement::InaudiblePlayTime() const {
+ return mDecoder ? mDecoder->GetInaudiblePlayTimeInSeconds() : -1.0;
+}
+
+double HTMLMediaElement::MutedPlayTime() const {
+ return mDecoder ? mDecoder->GetMutedPlayTimeInSeconds() : -1.0;
+}
+
+double HTMLMediaElement::VideoDecodeSuspendedTime() const {
+ return mDecoder ? mDecoder->GetVideoDecodeSuspendedTimeInSeconds() : -1.0;
+}
+
+void HTMLMediaElement::SetFormatDiagnosticsReportForMimeType(
+ const nsAString& aMimeType, DecoderDoctorReportType aType) {
+ DecoderDoctorDiagnostics diagnostics;
+ diagnostics.SetDecoderDoctorReportType(aType);
+ diagnostics.StoreFormatDiagnostics(OwnerDoc(), aMimeType, false /* can play*/,
+ __func__);
+}
+
+void HTMLMediaElement::SetDecodeError(const nsAString& aError,
+ ErrorResult& aRv) {
+ // The reason we use this map-ish structure is because we can't use
+ // `CR.NS_ERROR.*` directly in test. In order to use them in test, we have to
+ // add them into `xpc.msg`. As we won't use `CR.NS_ERROR.*` in the production
+ // code, adding them to `xpc.msg` seems an overdesign and adding maintenance
+ // effort (exposing them in CR also needs to add a description, which is
+ // useless because we won't show them to users)
+ static struct {
+ const char* mName;
+ nsresult mResult;
+ } kSupportedErrorList[] = {
+ {"NS_ERROR_DOM_MEDIA_ABORT_ERR", NS_ERROR_DOM_MEDIA_ABORT_ERR},
+ {"NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR",
+ NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR},
+ {"NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR",
+ NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR},
+ {"NS_ERROR_DOM_MEDIA_DECODE_ERR", NS_ERROR_DOM_MEDIA_DECODE_ERR},
+ {"NS_ERROR_DOM_MEDIA_FATAL_ERR", NS_ERROR_DOM_MEDIA_FATAL_ERR},
+ {"NS_ERROR_DOM_MEDIA_METADATA_ERR", NS_ERROR_DOM_MEDIA_METADATA_ERR},
+ {"NS_ERROR_DOM_MEDIA_OVERFLOW_ERR", NS_ERROR_DOM_MEDIA_OVERFLOW_ERR},
+ {"NS_ERROR_DOM_MEDIA_MEDIASINK_ERR", NS_ERROR_DOM_MEDIA_MEDIASINK_ERR},
+ {"NS_ERROR_DOM_MEDIA_DEMUXER_ERR", NS_ERROR_DOM_MEDIA_DEMUXER_ERR},
+ {"NS_ERROR_DOM_MEDIA_CDM_ERR", NS_ERROR_DOM_MEDIA_CDM_ERR},
+ {"NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR",
+ NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR}};
+ for (auto& error : kSupportedErrorList) {
+ if (strcmp(error.mName, NS_ConvertUTF16toUTF8(aError).get()) == 0) {
+ DecoderDoctorDiagnostics diagnostics;
+ diagnostics.StoreDecodeError(OwnerDoc(), error.mResult, u""_ns, __func__);
+ return;
+ }
+ }
+ aRv.Throw(NS_ERROR_FAILURE);
+}
+
+void HTMLMediaElement::SetAudioSinkFailedStartup() {
+ DecoderDoctorDiagnostics diagnostics;
+ diagnostics.StoreEvent(OwnerDoc(),
+ {DecoderDoctorEvent::eAudioSinkStartup,
+ NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR},
+ __func__);
+}
+
+already_AddRefed<layers::Image> HTMLMediaElement::GetCurrentImage() {
+ MarkAsTainted();
+
+ // TODO: In bug 1345404, handle case when video decoder is already suspended.
+ ImageContainer* container = GetImageContainer();
+ if (!container) {
+ return nullptr;
+ }
+
+ AutoLockImage lockImage(container);
+ RefPtr<layers::Image> image = lockImage.GetImage(TimeStamp::Now());
+ return image.forget();
+}
+
+bool HTMLMediaElement::HasSuspendTaint() const {
+ MOZ_ASSERT(!mDecoder || (mDecoder->HasSuspendTaint() == mHasSuspendTaint));
+ return mHasSuspendTaint;
+}
+
+already_AddRefed<DOMMediaStream> HTMLMediaElement::GetSrcObject() const {
+ return do_AddRef(mSrcAttrStream);
+}
+
+void HTMLMediaElement::SetSrcObject(DOMMediaStream& aValue) {
+ SetSrcObject(&aValue);
+}
+
+void HTMLMediaElement::SetSrcObject(DOMMediaStream* aValue) {
+ for (auto& outputStream : mOutputStreams) {
+ if (aValue == outputStream.mStream) {
+ ReportToConsole(nsIScriptError::warningFlag,
+ "MediaElementStreamCaptureCycle");
+ return;
+ }
+ }
+ mSrcAttrStream = aValue;
+ UpdateAudioChannelPlayingState();
+ DoLoad();
+}
+
+bool HTMLMediaElement::Ended() {
+ return (mDecoder && mDecoder->IsEnded()) ||
+ (mSrcStream && mSrcStreamReportPlaybackEnded);
+}
+
+void HTMLMediaElement::GetCurrentSrc(nsAString& aCurrentSrc) {
+ nsAutoCString src;
+ GetCurrentSpec(src);
+ CopyUTF8toUTF16(src, aCurrentSrc);
+}
+
+nsresult HTMLMediaElement::OnChannelRedirect(nsIChannel* aChannel,
+ nsIChannel* aNewChannel,
+ uint32_t aFlags) {
+ MOZ_ASSERT(mChannelLoader);
+ return mChannelLoader->Redirect(aChannel, aNewChannel, aFlags);
+}
+
+void HTMLMediaElement::ShutdownDecoder() {
+ RemoveMediaElementFromURITable();
+ NS_ASSERTION(mDecoder, "Must have decoder to shut down");
+
+ mWaitingForKeyListener.DisconnectIfExists();
+ if (mMediaSource) {
+ mMediaSource->CompletePendingTransactions();
+ }
+ mDecoder->Shutdown();
+ DDUNLINKCHILD(mDecoder.get());
+ mDecoder = nullptr;
+}
+
+void HTMLMediaElement::AbortExistingLoads() {
+ // Abort any already-running instance of the resource selection algorithm.
+ mLoadWaitStatus = NOT_WAITING;
+
+ // Set a new load ID. This will cause events which were enqueued
+ // with a different load ID to silently be cancelled.
+ mCurrentLoadID++;
+
+ // Immediately reject or resolve the already-dispatched
+ // nsResolveOrRejectPendingPlayPromisesRunners. These runners won't be
+ // executed again later since the mCurrentLoadID had been changed.
+ for (auto& runner : mPendingPlayPromisesRunners) {
+ runner->ResolveOrReject();
+ }
+ mPendingPlayPromisesRunners.Clear();
+
+ if (mChannelLoader) {
+ mChannelLoader->Cancel();
+ mChannelLoader = nullptr;
+ }
+
+ bool fireTimeUpdate = false;
+
+ if (mDecoder) {
+ fireTimeUpdate = mDecoder->GetCurrentTime() != 0.0;
+ ShutdownDecoder();
+ }
+ if (mSrcStream) {
+ EndSrcMediaStreamPlayback();
+ }
+
+ if (mMediaSource) {
+ OwnerDoc()->RemoveMediaElementWithMSE();
+ }
+
+ RemoveMediaElementFromURITable();
+ mLoadingSrcTriggeringPrincipal = nullptr;
+ DDLOG(DDLogCategory::Property, "loading_src", "");
+ DDUNLINKCHILD(mMediaSource.get());
+ mMediaSource = nullptr;
+
+ if (mNetworkState == NETWORK_LOADING || mNetworkState == NETWORK_IDLE) {
+ DispatchAsyncEvent(u"abort"_ns);
+ }
+
+ bool hadVideo = HasVideo();
+ mErrorSink->ResetError();
+ mCurrentPlayRangeStart = -1.0;
+ mPlayed = new TimeRanges(ToSupports(OwnerDoc()));
+ mLoadedDataFired = false;
+ mCanAutoplayFlag = true;
+ mIsLoadingFromSourceChildren = false;
+ mSuspendedAfterFirstFrame = false;
+ mAllowSuspendAfterFirstFrame = true;
+ mHaveQueuedSelectResource = false;
+ mSuspendedForPreloadNone = false;
+ mDownloadSuspendedByCache = false;
+ mMediaInfo = MediaInfo();
+ mIsEncrypted = false;
+ mPendingEncryptedInitData.Reset();
+ mWaitingForKey = NOT_WAITING_FOR_KEY;
+ mSourcePointer = nullptr;
+ mIsBlessed = false;
+ SetAudibleState(false);
+
+ mTags = nullptr;
+
+ if (mNetworkState != NETWORK_EMPTY) {
+ NS_ASSERTION(!mDecoder && !mSrcStream,
+ "How did someone setup a new stream/decoder already?");
+
+ DispatchAsyncEvent(u"emptied"_ns);
+
+ // ChangeNetworkState() will call UpdateAudioChannelPlayingState()
+ // indirectly which depends on mPaused. So we need to update mPaused first.
+ if (!mPaused) {
+ mPaused = true;
+ PlayPromise::RejectPromises(TakePendingPlayPromises(),
+ NS_ERROR_DOM_MEDIA_ABORT_ERR);
+ }
+ ChangeNetworkState(NETWORK_EMPTY);
+ RemoveMediaTracks();
+ UpdateOutputTrackSources();
+ ChangeReadyState(HAVE_NOTHING);
+
+ // TODO: Apply the rules for text track cue rendering Bug 865407
+ if (mTextTrackManager) {
+ mTextTrackManager->GetTextTracks()->SetCuesInactive();
+ }
+
+ if (fireTimeUpdate) {
+ // Since we destroyed the decoder above, the current playback position
+ // will now be reported as 0. The playback position was non-zero when
+ // we destroyed the decoder, so fire a timeupdate event so that the
+ // change will be reflected in the controls.
+ FireTimeUpdate(TimeupdateType::eMandatory);
+ }
+ UpdateAudioChannelPlayingState();
+ }
+
+ if (IsVideo() && hadVideo) {
+ // Ensure we render transparent black after resetting video resolution.
+ Maybe<nsIntSize> size = Some(nsIntSize(0, 0));
+ Invalidate(ImageSizeChanged::Yes, size, ForceInvalidate::No);
+ }
+
+ // As aborting current load would stop current playback, so we have no need to
+ // resume a paused media element.
+ ClearResumeDelayedMediaPlaybackAgentIfNeeded();
+
+ mMediaControlKeyListener->StopIfNeeded();
+
+ // We may have changed mPaused, mCanAutoplayFlag, and other
+ // things which can affect AddRemoveSelfReference
+ AddRemoveSelfReference();
+
+ mIsRunningSelectResource = false;
+
+ AssertReadyStateIsNothing();
+}
+
+void HTMLMediaElement::NoSupportedMediaSourceError(
+ const nsACString& aErrorDetails) {
+ if (mDecoder) {
+ ShutdownDecoder();
+ }
+
+ bool isSameOriginLoad = false;
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+ if (mSrcAttrTriggeringPrincipal && mLoadingSrc) {
+ rv = mSrcAttrTriggeringPrincipal->IsSameOrigin(mLoadingSrc,
+ &isSameOriginLoad);
+ }
+
+ if (NS_SUCCEEDED(rv) && !isSameOriginLoad) {
+ // aErrorDetails can include sensitive details like MimeType or HTTP Status
+ // Code. In case we're loading a 3rd party resource we should not leak this
+ // and pass a Generic Error Message
+ mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED,
+ "Failed to open media"_ns);
+ } else {
+ mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED, aErrorDetails);
+ }
+
+ RemoveMediaTracks();
+ ChangeDelayLoadStatus(false);
+ UpdateAudioChannelPlayingState();
+ PlayPromise::RejectPromises(TakePendingPlayPromises(),
+ NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR);
+}
+
+// Runs a "synchronous section", a function that must run once the event loop
+// has reached a "stable state"
+// http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#synchronous-section
+void HTMLMediaElement::RunInStableState(nsIRunnable* aRunnable) {
+ if (mShuttingDown) {
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "HTMLMediaElement::RunInStableState",
+ [self = RefPtr<HTMLMediaElement>(this), loadId = GetCurrentLoadID(),
+ runnable = RefPtr<nsIRunnable>(aRunnable)]() {
+ if (self->GetCurrentLoadID() != loadId) {
+ return;
+ }
+ runnable->Run();
+ });
+ nsContentUtils::RunInStableState(task.forget());
+}
+
+void HTMLMediaElement::QueueLoadFromSourceTask() {
+ if (!mIsLoadingFromSourceChildren || mShuttingDown) {
+ return;
+ }
+
+ if (mDecoder) {
+ // Reset readyState to HAVE_NOTHING since we're going to load a new decoder.
+ ShutdownDecoder();
+ ChangeReadyState(HAVE_NOTHING);
+ }
+
+ AssertReadyStateIsNothing();
+
+ ChangeDelayLoadStatus(true);
+ ChangeNetworkState(NETWORK_LOADING);
+ RefPtr<Runnable> r =
+ NewRunnableMethod("HTMLMediaElement::LoadFromSourceChildren", this,
+ &HTMLMediaElement::LoadFromSourceChildren);
+ RunInStableState(r);
+}
+
+void HTMLMediaElement::QueueSelectResourceTask() {
+ // Don't allow multiple async select resource calls to be queued.
+ if (mHaveQueuedSelectResource) return;
+ mHaveQueuedSelectResource = true;
+ ChangeNetworkState(NETWORK_NO_SOURCE);
+ RefPtr<Runnable> r =
+ NewRunnableMethod("HTMLMediaElement::SelectResourceWrapper", this,
+ &HTMLMediaElement::SelectResourceWrapper);
+ RunInStableState(r);
+}
+
+static bool HasSourceChildren(nsIContent* aElement) {
+ for (nsIContent* child = aElement->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::source)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static nsCString DocumentOrigin(Document* aDoc) {
+ if (!aDoc) {
+ return "null"_ns;
+ }
+ nsCOMPtr<nsIPrincipal> principal = aDoc->NodePrincipal();
+ if (!principal) {
+ return "null"_ns;
+ }
+ nsCString origin;
+ if (NS_FAILED(principal->GetOrigin(origin))) {
+ return "null"_ns;
+ }
+ return origin;
+}
+
+void HTMLMediaElement::Load() {
+ LOG(LogLevel::Debug,
+ ("%p Load() hasSrcAttrStream=%d hasSrcAttr=%d hasSourceChildren=%d "
+ "handlingInput=%d hasAutoplayAttr=%d AllowedToPlay=%d "
+ "ownerDoc=%p (%s) ownerDocUserActivated=%d "
+ "muted=%d volume=%f",
+ this, !!mSrcAttrStream, HasAttr(nsGkAtoms::src), HasSourceChildren(this),
+ UserActivation::IsHandlingUserInput(), HasAttr(nsGkAtoms::autoplay),
+ AllowedToPlay(), OwnerDoc(), DocumentOrigin(OwnerDoc()).get(),
+ OwnerDoc()->HasBeenUserGestureActivated(), mMuted, mVolume));
+
+ if (mIsRunningLoadMethod) {
+ return;
+ }
+
+ mIsDoingExplicitLoad = true;
+ DoLoad();
+}
+
+void HTMLMediaElement::DoLoad() {
+ // Check if media is allowed for the docshell.
+ nsCOMPtr<nsIDocShell> docShell = OwnerDoc()->GetDocShell();
+ if (docShell && !docShell->GetAllowMedia()) {
+ LOG(LogLevel::Debug, ("%p Media not allowed", this));
+ return;
+ }
+
+ if (mIsRunningLoadMethod) {
+ return;
+ }
+
+ if (UserActivation::IsHandlingUserInput()) {
+ // Detect if user has interacted with element so that play will not be
+ // blocked when initiated by a script. This enables sites to capture user
+ // intent to play by calling load() in the click handler of a "catalog
+ // view" of a gallery of videos.
+ mIsBlessed = true;
+ // Mark the channel as urgent-start when autoplay so that it will play the
+ // media from src after loading enough resource.
+ if (HasAttr(nsGkAtoms::autoplay)) {
+ mUseUrgentStartForChannel = true;
+ }
+ }
+
+ SetPlayedOrSeeked(false);
+ mIsRunningLoadMethod = true;
+ AbortExistingLoads();
+ SetPlaybackRate(mDefaultPlaybackRate, IgnoreErrors());
+ QueueSelectResourceTask();
+ ResetState();
+ mIsRunningLoadMethod = false;
+}
+
+void HTMLMediaElement::ResetState() {
+ // There might be a pending MediaDecoder::PlaybackPositionChanged() which
+ // will overwrite |mMediaInfo.mVideo.mDisplay| in UpdateMediaSize() to give
+ // staled videoWidth and videoHeight. We have to call ForgetElement() here
+ // such that the staled callbacks won't reach us.
+ if (mVideoFrameContainer) {
+ mVideoFrameContainer->ForgetElement();
+ mVideoFrameContainer = nullptr;
+ }
+ if (mMediaStreamRenderer) {
+ // mMediaStreamRenderer, has a strong reference to mVideoFrameContainer.
+ mMediaStreamRenderer->Shutdown();
+ mMediaStreamRenderer = nullptr;
+ }
+ if (mSecondaryMediaStreamRenderer) {
+ // mSecondaryMediaStreamRenderer, has a strong reference to
+ // the secondary VideoFrameContainer.
+ mSecondaryMediaStreamRenderer->Shutdown();
+ mSecondaryMediaStreamRenderer = nullptr;
+ }
+}
+
+void HTMLMediaElement::SelectResourceWrapper() {
+ SelectResource();
+ MaybeBeginCloningVisually();
+ mIsRunningSelectResource = false;
+ mHaveQueuedSelectResource = false;
+ mIsDoingExplicitLoad = false;
+}
+
+void HTMLMediaElement::SelectResource() {
+ if (!mSrcAttrStream && !HasAttr(nsGkAtoms::src) && !HasSourceChildren(this)) {
+ // The media element has neither a src attribute nor any source
+ // element children, abort the load.
+ ChangeNetworkState(NETWORK_EMPTY);
+ ChangeDelayLoadStatus(false);
+ return;
+ }
+
+ ChangeDelayLoadStatus(true);
+
+ ChangeNetworkState(NETWORK_LOADING);
+ DispatchAsyncEvent(u"loadstart"_ns);
+
+ // Delay setting mIsRunningSeletResource until after UpdatePreloadAction
+ // so that we don't lose our state change by bailing out of the preload
+ // state update
+ UpdatePreloadAction();
+ mIsRunningSelectResource = true;
+
+ // If we have a 'src' attribute, use that exclusively.
+ nsAutoString src;
+ if (mSrcAttrStream) {
+ SetupSrcMediaStreamPlayback(mSrcAttrStream);
+ } else if (GetAttr(nsGkAtoms::src, src)) {
+ nsCOMPtr<nsIURI> uri;
+ MediaResult rv = NewURIFromString(src, getter_AddRefs(uri));
+ if (NS_SUCCEEDED(rv)) {
+ LOG(LogLevel::Debug, ("%p Trying load from src=%s", this,
+ NS_ConvertUTF16toUTF8(src).get()));
+ NS_ASSERTION(
+ !mIsLoadingFromSourceChildren,
+ "Should think we're not loading from source children by default");
+
+ RemoveMediaElementFromURITable();
+ if (!mSrcMediaSource) {
+ mLoadingSrc = uri;
+ } else {
+ mLoadingSrc = nullptr;
+ }
+ mLoadingSrcTriggeringPrincipal = mSrcAttrTriggeringPrincipal;
+ DDLOG(DDLogCategory::Property, "loading_src",
+ nsCString(NS_ConvertUTF16toUTF8(src)));
+ bool hadMediaSource = !!mMediaSource;
+ mMediaSource = mSrcMediaSource;
+ if (mMediaSource && !hadMediaSource) {
+ OwnerDoc()->AddMediaElementWithMSE();
+ }
+ DDLINKCHILD("mediasource", mMediaSource.get());
+ UpdatePreloadAction();
+ if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE && !mMediaSource) {
+ // preload:none media, suspend the load here before we make any
+ // network requests.
+ SuspendLoad();
+ return;
+ }
+
+ rv = LoadResource();
+ if (NS_SUCCEEDED(rv)) {
+ return;
+ }
+ } else {
+ AutoTArray<nsString, 1> params = {src};
+ ReportLoadError("MediaLoadInvalidURI", params);
+ rv = MediaResult(rv.Code(), "MediaLoadInvalidURI");
+ }
+ // The media element has neither a src attribute nor a source element child:
+ // set the networkState to NETWORK_EMPTY, and abort these steps; the
+ // synchronous section ends.
+ GetMainThreadSerialEventTarget()->Dispatch(NewRunnableMethod<nsCString>(
+ "HTMLMediaElement::NoSupportedMediaSourceError", this,
+ &HTMLMediaElement::NoSupportedMediaSourceError, rv.Description()));
+ } else {
+ // Otherwise, the source elements will be used.
+ mIsLoadingFromSourceChildren = true;
+ LoadFromSourceChildren();
+ }
+}
+
+void HTMLMediaElement::NotifyLoadError(const nsACString& aErrorDetails) {
+ if (!mIsLoadingFromSourceChildren) {
+ LOG(LogLevel::Debug, ("NotifyLoadError(), no supported media error"));
+ NoSupportedMediaSourceError(aErrorDetails);
+ } else if (mSourceLoadCandidate) {
+ DispatchAsyncSourceError(mSourceLoadCandidate);
+ QueueLoadFromSourceTask();
+ } else {
+ NS_WARNING("Should know the source we were loading from!");
+ }
+}
+
+void HTMLMediaElement::NotifyMediaTrackAdded(dom::MediaTrack* aTrack) {
+ // The set of tracks changed.
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
+}
+
+void HTMLMediaElement::NotifyMediaTrackRemoved(dom::MediaTrack* aTrack) {
+ // The set of tracks changed.
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
+}
+
+void HTMLMediaElement::NotifyMediaTrackEnabled(dom::MediaTrack* aTrack) {
+ MOZ_ASSERT(aTrack);
+ if (!aTrack) {
+ return;
+ }
+#ifdef DEBUG
+ nsString id;
+ aTrack->GetId(id);
+
+ LOG(LogLevel::Debug, ("MediaElement %p %sTrack with id %s enabled", this,
+ aTrack->AsAudioTrack() ? "Audio" : "Video",
+ NS_ConvertUTF16toUTF8(id).get()));
+#endif
+
+ MOZ_ASSERT((aTrack->AsAudioTrack() && aTrack->AsAudioTrack()->Enabled()) ||
+ (aTrack->AsVideoTrack() && aTrack->AsVideoTrack()->Selected()));
+
+ if (aTrack->AsAudioTrack()) {
+ SetMutedInternal(mMuted & ~MUTED_BY_AUDIO_TRACK);
+ } else if (aTrack->AsVideoTrack()) {
+ if (!IsVideo()) {
+ MOZ_ASSERT(false);
+ return;
+ }
+ mDisableVideo = false;
+ } else {
+ MOZ_ASSERT(false, "Unknown track type");
+ }
+
+ if (mSrcStream) {
+ if (AudioTrack* t = aTrack->AsAudioTrack()) {
+ if (mMediaStreamRenderer) {
+ mMediaStreamRenderer->AddTrack(t->GetAudioStreamTrack());
+ }
+ } else if (VideoTrack* t = aTrack->AsVideoTrack()) {
+ MOZ_ASSERT(!mSelectedVideoStreamTrack);
+
+ mSelectedVideoStreamTrack = t->GetVideoStreamTrack();
+ mSelectedVideoStreamTrack->AddPrincipalChangeObserver(this);
+ if (mMediaStreamRenderer) {
+ mMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack);
+ }
+ if (mSecondaryMediaStreamRenderer) {
+ mSecondaryMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack);
+ }
+ if (mMediaInfo.HasVideo()) {
+ mMediaInfo.mVideo.SetAlpha(mSelectedVideoStreamTrack->HasAlpha());
+ }
+ nsContentUtils::CombineResourcePrincipals(
+ &mSrcStreamVideoPrincipal, mSelectedVideoStreamTrack->GetPrincipal());
+ }
+ }
+
+ // The set of enabled/selected tracks changed.
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
+}
+
+void HTMLMediaElement::NotifyMediaTrackDisabled(dom::MediaTrack* aTrack) {
+ MOZ_ASSERT(aTrack);
+ if (!aTrack) {
+ return;
+ }
+
+ nsString id;
+ aTrack->GetId(id);
+
+ LOG(LogLevel::Debug, ("MediaElement %p %sTrack with id %s disabled", this,
+ aTrack->AsAudioTrack() ? "Audio" : "Video",
+ NS_ConvertUTF16toUTF8(id).get()));
+
+ MOZ_ASSERT((!aTrack->AsAudioTrack() || !aTrack->AsAudioTrack()->Enabled()) &&
+ (!aTrack->AsVideoTrack() || !aTrack->AsVideoTrack()->Selected()));
+
+ if (AudioTrack* t = aTrack->AsAudioTrack()) {
+ if (mSrcStream) {
+ if (mMediaStreamRenderer) {
+ mMediaStreamRenderer->RemoveTrack(t->GetAudioStreamTrack());
+ }
+ }
+ // If we don't have any live tracks, we don't need to mute MediaElement.
+ MOZ_DIAGNOSTIC_ASSERT(AudioTracks(), "Element can't have been unlinked");
+ if (AudioTracks()->Length() > 0) {
+ bool shouldMute = true;
+ for (uint32_t i = 0; i < AudioTracks()->Length(); ++i) {
+ if ((*AudioTracks())[i]->Enabled()) {
+ shouldMute = false;
+ break;
+ }
+ }
+
+ if (shouldMute) {
+ SetMutedInternal(mMuted | MUTED_BY_AUDIO_TRACK);
+ }
+ }
+ } else if (aTrack->AsVideoTrack()) {
+ if (mSrcStream) {
+ MOZ_DIAGNOSTIC_ASSERT(mSelectedVideoStreamTrack ==
+ aTrack->AsVideoTrack()->GetVideoStreamTrack());
+ if (mMediaStreamRenderer) {
+ mMediaStreamRenderer->RemoveTrack(mSelectedVideoStreamTrack);
+ }
+ if (mSecondaryMediaStreamRenderer) {
+ mSecondaryMediaStreamRenderer->RemoveTrack(mSelectedVideoStreamTrack);
+ }
+ mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(this);
+ mSelectedVideoStreamTrack = nullptr;
+ }
+ }
+
+ // The set of enabled/selected tracks changed.
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
+}
+
+void HTMLMediaElement::DealWithFailedElement(nsIContent* aSourceElement) {
+ if (mShuttingDown) {
+ return;
+ }
+
+ DispatchAsyncSourceError(aSourceElement);
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NewRunnableMethod("HTMLMediaElement::QueueLoadFromSourceTask", this,
+ &HTMLMediaElement::QueueLoadFromSourceTask));
+}
+
+void HTMLMediaElement::LoadFromSourceChildren() {
+ NS_ASSERTION(mDelayingLoadEvent,
+ "Should delay load event (if in document) during load");
+ NS_ASSERTION(mIsLoadingFromSourceChildren,
+ "Must remember we're loading from source children");
+
+ AddMutationObserverUnlessExists(this);
+
+ RemoveMediaTracks();
+
+ while (true) {
+ HTMLSourceElement* child = GetNextSource();
+ if (!child) {
+ // Exhausted candidates, wait for more candidates to be appended to
+ // the media element.
+ mLoadWaitStatus = WAITING_FOR_SOURCE;
+ ChangeNetworkState(NETWORK_NO_SOURCE);
+ ChangeDelayLoadStatus(false);
+ ReportLoadError("MediaLoadExhaustedCandidates");
+ return;
+ }
+
+ // Must have src attribute.
+ nsAutoString src;
+ if (!child->GetAttr(nsGkAtoms::src, src)) {
+ ReportLoadError("MediaLoadSourceMissingSrc");
+ DealWithFailedElement(child);
+ return;
+ }
+
+ // If we have a type attribute, it must be a supported type.
+ nsAutoString type;
+ if (child->GetAttr(nsGkAtoms::type, type) && !type.IsEmpty()) {
+ DecoderDoctorDiagnostics diagnostics;
+ CanPlayStatus canPlay = GetCanPlay(type, &diagnostics);
+ diagnostics.StoreFormatDiagnostics(OwnerDoc(), type,
+ canPlay != CANPLAY_NO, __func__);
+ if (canPlay == CANPLAY_NO) {
+ // Check that at least one other source child exists and report that
+ // we will try to load that one next.
+ nsIContent* nextChild = mSourcePointer->GetNextSibling();
+ AutoTArray<nsString, 2> params = {type, src};
+
+ while (nextChild) {
+ if (nextChild && nextChild->IsHTMLElement(nsGkAtoms::source)) {
+ ReportLoadError("MediaLoadUnsupportedTypeAttributeLoadingNextChild",
+ params);
+ break;
+ }
+
+ nextChild = nextChild->GetNextSibling();
+ };
+
+ if (!nextChild) {
+ ReportLoadError("MediaLoadUnsupportedTypeAttribute", params);
+ }
+
+ DealWithFailedElement(child);
+ return;
+ }
+ }
+ nsAutoString media;
+ child->GetAttr(nsGkAtoms::media, media);
+ HTMLSourceElement* childSrc = HTMLSourceElement::FromNode(child);
+ MOZ_ASSERT(childSrc, "Expect child to be HTMLSourceElement");
+ if (childSrc && !childSrc->MatchesCurrentMedia()) {
+ AutoTArray<nsString, 2> params = {media, src};
+ ReportLoadError("MediaLoadSourceMediaNotMatched", params);
+ DealWithFailedElement(child);
+ LOG(LogLevel::Debug,
+ ("%p Media did not match from <source>=%s type=%s media=%s", this,
+ NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get(),
+ NS_ConvertUTF16toUTF8(media).get()));
+ return;
+ }
+ LOG(LogLevel::Debug,
+ ("%p Trying load from <source>=%s type=%s media=%s", this,
+ NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get(),
+ NS_ConvertUTF16toUTF8(media).get()));
+
+ nsCOMPtr<nsIURI> uri;
+ NewURIFromString(src, getter_AddRefs(uri));
+ if (!uri) {
+ AutoTArray<nsString, 1> params = {src};
+ ReportLoadError("MediaLoadInvalidURI", params);
+ DealWithFailedElement(child);
+ return;
+ }
+
+ RemoveMediaElementFromURITable();
+ mLoadingSrc = uri;
+ mLoadingSrcTriggeringPrincipal = child->GetSrcTriggeringPrincipal();
+ DDLOG(DDLogCategory::Property, "loading_src",
+ nsCString(NS_ConvertUTF16toUTF8(src)));
+ bool hadMediaSource = !!mMediaSource;
+ mMediaSource = child->GetSrcMediaSource();
+ if (mMediaSource && !hadMediaSource) {
+ OwnerDoc()->AddMediaElementWithMSE();
+ }
+ DDLINKCHILD("mediasource", mMediaSource.get());
+ NS_ASSERTION(mNetworkState == NETWORK_LOADING,
+ "Network state should be loading");
+
+ if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE && !mMediaSource) {
+ // preload:none media, suspend the load here before we make any
+ // network requests.
+ SuspendLoad();
+ return;
+ }
+
+ if (NS_SUCCEEDED(LoadResource())) {
+ return;
+ }
+
+ // If we fail to load, loop back and try loading the next resource.
+ DispatchAsyncSourceError(child);
+ }
+ MOZ_ASSERT_UNREACHABLE("Execution should not reach here!");
+}
+
+void HTMLMediaElement::SuspendLoad() {
+ mSuspendedForPreloadNone = true;
+ ChangeNetworkState(NETWORK_IDLE);
+ ChangeDelayLoadStatus(false);
+}
+
+void HTMLMediaElement::ResumeLoad(PreloadAction aAction) {
+ NS_ASSERTION(mSuspendedForPreloadNone,
+ "Must be halted for preload:none to resume from preload:none "
+ "suspended load.");
+ mSuspendedForPreloadNone = false;
+ mPreloadAction = aAction;
+ ChangeDelayLoadStatus(true);
+ ChangeNetworkState(NETWORK_LOADING);
+ if (!mIsLoadingFromSourceChildren) {
+ // We were loading from the element's src attribute.
+ MediaResult rv = LoadResource();
+ if (NS_FAILED(rv)) {
+ NoSupportedMediaSourceError(rv.Description());
+ }
+ } else {
+ // We were loading from a child <source> element. Try to resume the
+ // load of that child, and if that fails, try the next child.
+ if (NS_FAILED(LoadResource())) {
+ LoadFromSourceChildren();
+ }
+ }
+}
+
+bool HTMLMediaElement::AllowedToPlay() const {
+ return media::AutoplayPolicy::IsAllowedToPlay(*this);
+}
+
+uint32_t HTMLMediaElement::GetPreloadDefault() const {
+ if (mMediaSource) {
+ return HTMLMediaElement::PRELOAD_ATTR_METADATA;
+ }
+ if (OnCellularConnection()) {
+ return Preferences::GetInt("media.preload.default.cellular",
+ HTMLMediaElement::PRELOAD_ATTR_NONE);
+ }
+ return Preferences::GetInt("media.preload.default",
+ HTMLMediaElement::PRELOAD_ATTR_METADATA);
+}
+
+uint32_t HTMLMediaElement::GetPreloadDefaultAuto() const {
+ if (OnCellularConnection()) {
+ return Preferences::GetInt("media.preload.auto.cellular",
+ HTMLMediaElement::PRELOAD_ATTR_METADATA);
+ }
+ return Preferences::GetInt("media.preload.auto",
+ HTMLMediaElement::PRELOAD_ENOUGH);
+}
+
+void HTMLMediaElement::UpdatePreloadAction() {
+ PreloadAction nextAction = PRELOAD_UNDEFINED;
+ // If autoplay is set, or we're playing, we should always preload data,
+ // as we'll need it to play.
+ if ((AllowedToPlay() && HasAttr(nsGkAtoms::autoplay)) || !mPaused) {
+ nextAction = HTMLMediaElement::PRELOAD_ENOUGH;
+ } else {
+ // Find the appropriate preload action by looking at the attribute.
+ const nsAttrValue* val =
+ mAttrs.GetAttr(nsGkAtoms::preload, kNameSpaceID_None);
+ // MSE doesn't work if preload is none, so it ignores the pref when src is
+ // from MSE.
+ uint32_t preloadDefault = GetPreloadDefault();
+ uint32_t preloadAuto = GetPreloadDefaultAuto();
+ if (!val) {
+ // Attribute is not set. Use the preload action specified by the
+ // media.preload.default pref, or just preload metadata if not present.
+ nextAction = static_cast<PreloadAction>(preloadDefault);
+ } else if (val->Type() == nsAttrValue::eEnum) {
+ PreloadAttrValue attr =
+ static_cast<PreloadAttrValue>(val->GetEnumValue());
+ if (attr == HTMLMediaElement::PRELOAD_ATTR_EMPTY ||
+ attr == HTMLMediaElement::PRELOAD_ATTR_AUTO) {
+ nextAction = static_cast<PreloadAction>(preloadAuto);
+ } else if (attr == HTMLMediaElement::PRELOAD_ATTR_METADATA) {
+ nextAction = HTMLMediaElement::PRELOAD_METADATA;
+ } else if (attr == HTMLMediaElement::PRELOAD_ATTR_NONE) {
+ nextAction = HTMLMediaElement::PRELOAD_NONE;
+ }
+ } else {
+ // Use the suggested "missing value default" of "metadata", or the value
+ // specified by the media.preload.default, if present.
+ nextAction = static_cast<PreloadAction>(preloadDefault);
+ }
+ }
+
+ if (nextAction == HTMLMediaElement::PRELOAD_NONE && mIsDoingExplicitLoad) {
+ nextAction = HTMLMediaElement::PRELOAD_METADATA;
+ }
+
+ mPreloadAction = nextAction;
+
+ if (nextAction == HTMLMediaElement::PRELOAD_ENOUGH) {
+ if (mSuspendedForPreloadNone) {
+ // Our load was previouly suspended due to the media having preload
+ // value "none". The preload value has changed to preload:auto, so
+ // resume the load.
+ ResumeLoad(PRELOAD_ENOUGH);
+ } else {
+ // Preload as much of the video as we can, i.e. don't suspend after
+ // the first frame.
+ StopSuspendingAfterFirstFrame();
+ }
+
+ } else if (nextAction == HTMLMediaElement::PRELOAD_METADATA) {
+ // Ensure that the video can be suspended after first frame.
+ mAllowSuspendAfterFirstFrame = true;
+ if (mSuspendedForPreloadNone) {
+ // Our load was previouly suspended due to the media having preload
+ // value "none". The preload value has changed to preload:metadata, so
+ // resume the load. We'll pause the load again after we've read the
+ // metadata.
+ ResumeLoad(PRELOAD_METADATA);
+ }
+ }
+}
+
+MediaResult HTMLMediaElement::LoadResource() {
+ NS_ASSERTION(mDelayingLoadEvent,
+ "Should delay load event (if in document) during load");
+
+ if (mChannelLoader) {
+ mChannelLoader->Cancel();
+ mChannelLoader = nullptr;
+ }
+
+ // Set the media element's CORS mode only when loading a resource
+ mCORSMode = AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin));
+
+ HTMLMediaElement* other = LookupMediaElementURITable(mLoadingSrc);
+ if (other && other->mDecoder) {
+ // Clone it.
+ // TODO: remove the cast by storing ChannelMediaDecoder in the URI table.
+ nsresult rv = InitializeDecoderAsClone(
+ static_cast<ChannelMediaDecoder*>(other->mDecoder.get()));
+ if (NS_SUCCEEDED(rv)) return rv;
+ }
+
+ if (mMediaSource) {
+ MediaDecoderInit decoderInit(
+ this, this, mMuted ? 0.0 : mVolume, mPreservesPitch,
+ ClampPlaybackRate(mPlaybackRate),
+ mPreloadAction == HTMLMediaElement::PRELOAD_METADATA, mHasSuspendTaint,
+ HasAttr(nsGkAtoms::loop),
+ MediaContainerType(MEDIAMIMETYPE("application/x.mediasource")));
+
+ RefPtr<MediaSourceDecoder> decoder = new MediaSourceDecoder(decoderInit);
+ if (!mMediaSource->Attach(decoder)) {
+ // TODO: Handle failure: run "If the media data cannot be fetched at
+ // all, due to network errors, causing the user agent to give up
+ // trying to fetch the resource" section of resource fetch algorithm.
+ decoder->Shutdown();
+ return MediaResult(NS_ERROR_FAILURE, "Failed to attach MediaSource");
+ }
+ ChangeDelayLoadStatus(false);
+ nsresult rv = decoder->Load(mMediaSource->GetPrincipal());
+ if (NS_FAILED(rv)) {
+ decoder->Shutdown();
+ LOG(LogLevel::Debug,
+ ("%p Failed to load for decoder %p", this, decoder.get()));
+ return MediaResult(rv, "Fail to load decoder");
+ }
+ rv = FinishDecoderSetup(decoder);
+ return MediaResult(rv, "Failed to set up decoder");
+ }
+
+ AssertReadyStateIsNothing();
+
+ RefPtr<ChannelLoader> loader = new ChannelLoader;
+ nsresult rv = loader->Load(this);
+ if (NS_SUCCEEDED(rv)) {
+ mChannelLoader = std::move(loader);
+ }
+ return MediaResult(rv, "Failed to load channel");
+}
+
+nsresult HTMLMediaElement::LoadWithChannel(nsIChannel* aChannel,
+ nsIStreamListener** aListener) {
+ NS_ENSURE_ARG_POINTER(aChannel);
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ *aListener = nullptr;
+
+ // Make sure we don't reenter during synchronous abort events.
+ if (mIsRunningLoadMethod) return NS_OK;
+ mIsRunningLoadMethod = true;
+ AbortExistingLoads();
+ mIsRunningLoadMethod = false;
+
+ mLoadingSrcTriggeringPrincipal = nullptr;
+ nsresult rv = aChannel->GetOriginalURI(getter_AddRefs(mLoadingSrc));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ChangeDelayLoadStatus(true);
+ rv = InitializeDecoderForChannel(aChannel, aListener);
+ if (NS_FAILED(rv)) {
+ ChangeDelayLoadStatus(false);
+ return rv;
+ }
+
+ SetPlaybackRate(mDefaultPlaybackRate, IgnoreErrors());
+ DispatchAsyncEvent(u"loadstart"_ns);
+
+ return NS_OK;
+}
+
+bool HTMLMediaElement::Seeking() const {
+ return mDecoder && mDecoder->IsSeeking();
+}
+
+double HTMLMediaElement::CurrentTime() const {
+ if (mMediaStreamRenderer) {
+ return ToMicrosecondResolution(mMediaStreamRenderer->CurrentTime());
+ }
+
+ if (mDefaultPlaybackStartPosition == 0.0 && mDecoder) {
+ return std::clamp(mDecoder->GetCurrentTime(), 0.0, mDecoder->GetDuration());
+ }
+
+ return mDefaultPlaybackStartPosition;
+}
+
+void HTMLMediaElement::FastSeek(double aTime, ErrorResult& aRv) {
+ LOG(LogLevel::Debug, ("%p FastSeek(%f) called by JS", this, aTime));
+ Seek(aTime, SeekTarget::PrevSyncPoint, IgnoreErrors());
+}
+
+already_AddRefed<Promise> HTMLMediaElement::SeekToNextFrame(ErrorResult& aRv) {
+ /* This will cause JIT code to be kept around longer, to help performance
+ * when using SeekToNextFrame to iterate through every frame of a video.
+ */
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
+
+ if (win) {
+ if (JSObject* obj = win->AsGlobal()->GetGlobalJSObject()) {
+ js::NotifyAnimationActivity(obj);
+ }
+ }
+
+ Seek(CurrentTime(), SeekTarget::NextFrame, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ mSeekDOMPromise = CreateDOMPromise(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return do_AddRef(mSeekDOMPromise);
+}
+
+void HTMLMediaElement::SetCurrentTime(double aCurrentTime, ErrorResult& aRv) {
+ LOG(LogLevel::Debug,
+ ("%p SetCurrentTime(%lf) called by JS", this, aCurrentTime));
+ Seek(aCurrentTime, SeekTarget::Accurate, IgnoreErrors());
+}
+
+/**
+ * Check if aValue is inside a range of aRanges, and if so returns true
+ * and puts the range index in aIntervalIndex. If aValue is not
+ * inside a range, returns false, and aIntervalIndex
+ * is set to the index of the range which starts immediately after aValue
+ * (and can be aRanges.Length() if aValue is after the last range).
+ */
+static bool IsInRanges(TimeRanges& aRanges, double aValue,
+ uint32_t& aIntervalIndex) {
+ uint32_t length = aRanges.Length();
+
+ for (uint32_t i = 0; i < length; i++) {
+ double start = aRanges.Start(i);
+ if (start > aValue) {
+ aIntervalIndex = i;
+ return false;
+ }
+ double end = aRanges.End(i);
+ if (aValue <= end) {
+ aIntervalIndex = i;
+ return true;
+ }
+ }
+ aIntervalIndex = length;
+ return false;
+}
+
+void HTMLMediaElement::Seek(double aTime, SeekTarget::Type aSeekType,
+ ErrorResult& aRv) {
+ // Note: Seek is called both by synchronous code that expects errors thrown in
+ // aRv, as well as asynchronous code that expects a promise. Make sure all
+ // synchronous errors are returned using aRv, not promise rejections.
+
+ // aTime should be non-NaN.
+ MOZ_ASSERT(!std::isnan(aTime));
+
+ // Seeking step1, Set the media element's show poster flag to false.
+ // https://html.spec.whatwg.org/multipage/media.html#dom-media-seek
+ mShowPoster = false;
+
+ // Detect if user has interacted with element by seeking so that
+ // play will not be blocked when initiated by a script.
+ if (UserActivation::IsHandlingUserInput()) {
+ mIsBlessed = true;
+ }
+
+ StopSuspendingAfterFirstFrame();
+
+ if (mSrcAttrStream) {
+ // do nothing since media streams have an empty Seekable range.
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (mPlayed && mCurrentPlayRangeStart != -1.0) {
+ double rangeEndTime = CurrentTime();
+ LOG(LogLevel::Debug, ("%p Adding \'played\' a range : [%f, %f]", this,
+ mCurrentPlayRangeStart, rangeEndTime));
+ // Multiple seek without playing, or seek while playing.
+ if (mCurrentPlayRangeStart != rangeEndTime) {
+ // Don't round the left of the interval: it comes from script and needs
+ // to be exact.
+ mPlayed->Add(mCurrentPlayRangeStart, rangeEndTime);
+ }
+ // Reset the current played range start time. We'll re-set it once
+ // the seek completes.
+ mCurrentPlayRangeStart = -1.0;
+ }
+
+ if (mReadyState == HAVE_NOTHING) {
+ mDefaultPlaybackStartPosition = aTime;
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (!mDecoder) {
+ // mDecoder must always be set in order to reach this point.
+ NS_ASSERTION(mDecoder, "SetCurrentTime failed: no decoder");
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // Clamp the seek target to inside the seekable ranges.
+ media::TimeRanges seekableRanges = mDecoder->GetSeekableTimeRanges();
+ if (seekableRanges.IsInvalid()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ RefPtr<TimeRanges> seekable =
+ new TimeRanges(ToSupports(OwnerDoc()), seekableRanges);
+ uint32_t length = seekable->Length();
+ if (length == 0) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // If the position we want to seek to is not in a seekable range, we seek
+ // to the closest position in the seekable ranges instead. If two positions
+ // are equally close, we seek to the closest position from the currentTime.
+ // See seeking spec, point 7 :
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#seeking
+ uint32_t range = 0;
+ bool isInRange = IsInRanges(*seekable, aTime, range);
+ if (!isInRange) {
+ if (range == 0) {
+ // aTime is before the first range in |seekable|, the closest point we can
+ // seek to is the start of the first range.
+ aTime = seekable->Start(0);
+ } else if (range == length) {
+ // Seek target is after the end last range in seekable data.
+ // Clamp the seek target to the end of the last seekable range.
+ aTime = seekable->End(length - 1);
+ } else {
+ double leftBound = seekable->End(range - 1);
+ double rightBound = seekable->Start(range);
+ double distanceLeft = Abs(leftBound - aTime);
+ double distanceRight = Abs(rightBound - aTime);
+ if (distanceLeft == distanceRight) {
+ double currentTime = CurrentTime();
+ distanceLeft = Abs(leftBound - currentTime);
+ distanceRight = Abs(rightBound - currentTime);
+ }
+ aTime = (distanceLeft < distanceRight) ? leftBound : rightBound;
+ }
+ }
+
+ // TODO: The spec requires us to update the current time to reflect the
+ // actual seek target before beginning the synchronous section, but
+ // that requires changing all MediaDecoderReaders to support telling
+ // us the fastSeek target, and it's currently not possible to get
+ // this information as we don't yet control the demuxer for all
+ // MediaDecoderReaders.
+
+ mPlayingBeforeSeek = IsPotentiallyPlaying();
+
+ // The media backend is responsible for dispatching the timeupdate
+ // event if it changes the playback position as a result of the seek.
+ LOG(LogLevel::Debug, ("%p SetCurrentTime(%f) starting seek", this, aTime));
+ mDecoder->Seek(aTime, aSeekType);
+
+ // We changed whether we're seeking so we need to AddRemoveSelfReference.
+ AddRemoveSelfReference();
+}
+
+double HTMLMediaElement::Duration() const {
+ if (mSrcStream) {
+ if (mSrcStreamPlaybackEnded) {
+ return CurrentTime();
+ }
+ return std::numeric_limits<double>::infinity();
+ }
+
+ if (mDecoder) {
+ return mDecoder->GetDuration();
+ }
+
+ return std::numeric_limits<double>::quiet_NaN();
+}
+
+already_AddRefed<TimeRanges> HTMLMediaElement::Seekable() const {
+ media::TimeRanges seekable =
+ mDecoder ? mDecoder->GetSeekableTimeRanges() : media::TimeRanges();
+ RefPtr<TimeRanges> ranges = new TimeRanges(
+ ToSupports(OwnerDoc()), seekable.ToMicrosecondResolution());
+ return ranges.forget();
+}
+
+already_AddRefed<TimeRanges> HTMLMediaElement::Played() {
+ RefPtr<TimeRanges> ranges = new TimeRanges(ToSupports(OwnerDoc()));
+
+ uint32_t timeRangeCount = 0;
+ if (mPlayed) {
+ timeRangeCount = mPlayed->Length();
+ }
+ for (uint32_t i = 0; i < timeRangeCount; i++) {
+ double begin = mPlayed->Start(i);
+ double end = mPlayed->End(i);
+ ranges->Add(begin, end);
+ }
+
+ if (mCurrentPlayRangeStart != -1.0) {
+ double now = CurrentTime();
+ if (mCurrentPlayRangeStart != now) {
+ // Don't round the left of the interval: it comes from script and needs
+ // to be exact.
+ ranges->Add(mCurrentPlayRangeStart, now);
+ }
+ }
+
+ ranges->Normalize();
+ return ranges.forget();
+}
+
+void HTMLMediaElement::Pause(ErrorResult& aRv) {
+ LOG(LogLevel::Debug, ("%p Pause() called by JS", this));
+ if (mNetworkState == NETWORK_EMPTY) {
+ LOG(LogLevel::Debug, ("Loading due to Pause()"));
+ DoLoad();
+ }
+ PauseInternal();
+}
+
+void HTMLMediaElement::PauseInternal() {
+ if (mDecoder && mNetworkState != NETWORK_EMPTY) {
+ mDecoder->Pause();
+ }
+ bool oldPaused = mPaused;
+ mPaused = true;
+ // Step 1,
+ // https://html.spec.whatwg.org/multipage/media.html#internal-pause-steps
+ mCanAutoplayFlag = false;
+ // We changed mPaused and mCanAutoplayFlag which can affect
+ // AddRemoveSelfReference
+ AddRemoveSelfReference();
+ UpdateSrcMediaStreamPlaying();
+ if (mAudioChannelWrapper) {
+ mAudioChannelWrapper->NotifyPlayStateChanged();
+ }
+
+ // We don't need to resume media which is paused explicitly by user.
+ ClearResumeDelayedMediaPlaybackAgentIfNeeded();
+
+ if (!oldPaused) {
+ FireTimeUpdate(TimeupdateType::eMandatory);
+ DispatchAsyncEvent(u"pause"_ns);
+ AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_ABORT_ERR);
+ }
+}
+
+void HTMLMediaElement::SetVolume(double aVolume, ErrorResult& aRv) {
+ LOG(LogLevel::Debug, ("%p SetVolume(%f) called by JS", this, aVolume));
+
+ if (aVolume < 0.0 || aVolume > 1.0) {
+ aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ if (aVolume == mVolume) return;
+
+ mVolume = aVolume;
+
+ // Here we want just to update the volume.
+ SetVolumeInternal();
+
+ DispatchAsyncEvent(u"volumechange"_ns);
+
+ // We allow inaudible autoplay. But changing our volume may make this
+ // media audible. So pause if we are no longer supposed to be autoplaying.
+ PauseIfShouldNotBePlaying();
+}
+
+void HTMLMediaElement::MozGetMetadata(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aResult,
+ ErrorResult& aRv) {
+ if (mReadyState < HAVE_METADATA) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ JS::Rooted<JSObject*> tags(aCx, JS_NewPlainObject(aCx));
+ if (!tags) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ if (mTags) {
+ for (const auto& entry : *mTags) {
+ nsString wideValue;
+ CopyUTF8toUTF16(entry.GetData(), wideValue);
+ JS::Rooted<JSString*> string(aCx,
+ JS_NewUCStringCopyZ(aCx, wideValue.Data()));
+ if (!string || !JS_DefineProperty(aCx, tags, entry.GetKey().Data(),
+ string, JSPROP_ENUMERATE)) {
+ NS_WARNING("couldn't create metadata object!");
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ }
+ }
+
+ aResult.set(tags);
+}
+
+void HTMLMediaElement::SetMutedInternal(uint32_t aMuted) {
+ uint32_t oldMuted = mMuted;
+ mMuted = aMuted;
+
+ if (!!aMuted == !!oldMuted) {
+ return;
+ }
+
+ SetVolumeInternal();
+}
+
+void HTMLMediaElement::PauseIfShouldNotBePlaying() {
+ if (GetPaused()) {
+ return;
+ }
+ if (!AllowedToPlay()) {
+ AUTOPLAY_LOG("pause because not allowed to play, element=%p", this);
+ ErrorResult rv;
+ Pause(rv);
+ }
+}
+
+void HTMLMediaElement::SetVolumeInternal() {
+ float effectiveVolume = ComputedVolume();
+
+ if (mDecoder) {
+ mDecoder->SetVolume(effectiveVolume);
+ } else if (mMediaStreamRenderer) {
+ mMediaStreamRenderer->SetAudioOutputVolume(effectiveVolume);
+ }
+
+ NotifyAudioPlaybackChanged(
+ AudioChannelService::AudibleChangedReasons::eVolumeChanged);
+}
+
+void HTMLMediaElement::SetMuted(bool aMuted) {
+ LOG(LogLevel::Debug, ("%p SetMuted(%d) called by JS", this, aMuted));
+ if (aMuted == Muted()) {
+ return;
+ }
+
+ if (aMuted) {
+ SetMutedInternal(mMuted | MUTED_BY_CONTENT);
+ } else {
+ SetMutedInternal(mMuted & ~MUTED_BY_CONTENT);
+ }
+
+ DispatchAsyncEvent(u"volumechange"_ns);
+
+ // We allow inaudible autoplay. But changing our mute status may make this
+ // media audible. So pause if we are no longer supposed to be autoplaying.
+ PauseIfShouldNotBePlaying();
+}
+
+void HTMLMediaElement::GetAllEnabledMediaTracks(
+ nsTArray<RefPtr<MediaTrack>>& aTracks) {
+ if (AudioTrackList* tracks = AudioTracks()) {
+ for (size_t i = 0; i < tracks->Length(); ++i) {
+ AudioTrack* track = (*tracks)[i];
+ if (track->Enabled()) {
+ aTracks.AppendElement(track);
+ }
+ }
+ }
+ if (IsVideo()) {
+ if (VideoTrackList* tracks = VideoTracks()) {
+ for (size_t i = 0; i < tracks->Length(); ++i) {
+ VideoTrack* track = (*tracks)[i];
+ if (track->Selected()) {
+ aTracks.AppendElement(track);
+ }
+ }
+ }
+ }
+}
+
+void HTMLMediaElement::SetCapturedOutputStreamsEnabled(bool aEnabled) {
+ for (const auto& entry : mOutputTrackSources.Values()) {
+ entry->SetEnabled(aEnabled);
+ }
+}
+
+HTMLMediaElement::OutputMuteState HTMLMediaElement::OutputTracksMuted() {
+ return mPaused || mReadyState <= HAVE_CURRENT_DATA ? OutputMuteState::Muted
+ : OutputMuteState::Unmuted;
+}
+
+void HTMLMediaElement::UpdateOutputTracksMuting() {
+ for (const auto& entry : mOutputTrackSources.Values()) {
+ entry->SetMutedByElement(OutputTracksMuted());
+ }
+}
+
+void HTMLMediaElement::AddOutputTrackSourceToOutputStream(
+ MediaElementTrackSource* aSource, OutputMediaStream& aOutputStream,
+ AddTrackMode aMode) {
+ if (aOutputStream.mStream == mSrcStream) {
+ // Cycle detected. This can happen since tracks are added async.
+ // We avoid forwarding it to the output here or we'd get into an infloop.
+ LOG(LogLevel::Warning,
+ ("NOT adding output track source %p to output stream "
+ "%p -- cycle detected",
+ aSource, aOutputStream.mStream.get()));
+ return;
+ }
+
+ LOG(LogLevel::Debug, ("Adding output track source %p to output stream %p",
+ aSource, aOutputStream.mStream.get()));
+
+ RefPtr<MediaStreamTrack> domTrack;
+ if (aSource->Track()->mType == MediaSegment::AUDIO) {
+ domTrack = new AudioStreamTrack(
+ aOutputStream.mStream->GetOwner(), aSource->Track(), aSource,
+ MediaStreamTrackState::Live, aSource->Muted());
+ } else {
+ domTrack = new VideoStreamTrack(
+ aOutputStream.mStream->GetOwner(), aSource->Track(), aSource,
+ MediaStreamTrackState::Live, aSource->Muted());
+ }
+
+ aOutputStream.mLiveTracks.AppendElement(domTrack);
+
+ switch (aMode) {
+ case AddTrackMode::ASYNC:
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NewRunnableMethod<StoreRefPtrPassByPtr<MediaStreamTrack>>(
+ "DOMMediaStream::AddTrackInternal", aOutputStream.mStream,
+ &DOMMediaStream::AddTrackInternal, domTrack));
+ break;
+ case AddTrackMode::SYNC:
+ aOutputStream.mStream->AddTrackInternal(domTrack);
+ break;
+ default:
+ MOZ_CRASH("Unexpected mode");
+ }
+
+ LOG(LogLevel::Debug,
+ ("Created capture %s track %p",
+ domTrack->AsAudioStreamTrack() ? "audio" : "video", domTrack.get()));
+}
+
+void HTMLMediaElement::UpdateOutputTrackSources() {
+ // This updates the track sources in mOutputTrackSources so they're in sync
+ // with the tracks being currently played, and state saying whether we should
+ // be capturing tracks. This method is long so here is a breakdown:
+ // - Figure out the tracks that should be captured
+ // - Diff those against currently captured tracks (mOutputTrackSources), into
+ // tracks-to-add, and tracks-to-remove
+ // - Remove the tracks in tracks-to-remove and dispatch "removetrack" and
+ // "ended" events for them
+ // - If playback has ended, or there is no longer a media provider object,
+ // remove any OutputMediaStreams that have the finish-when-ended flag set
+ // - Create track sources for, and add to OutputMediaStreams, the tracks in
+ // tracks-to-add
+
+ const bool shouldHaveTrackSources = mTracksCaptured.Ref() &&
+ !IsPlaybackEnded() &&
+ mReadyState >= HAVE_METADATA;
+
+ // Add track sources for all enabled/selected MediaTracks.
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ if (!window) {
+ return;
+ }
+
+ if (mDecoder) {
+ if (!mTracksCaptured.Ref()) {
+ mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::None);
+ } else if (!AudioTracks() || !VideoTracks() || !shouldHaveTrackSources) {
+ // We've been unlinked, or tracks are not yet known.
+ mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::Halt);
+ } else {
+ mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::Capture,
+ mTracksCaptured.Ref().get());
+ }
+ }
+
+ // Start with all MediaTracks
+ AutoTArray<RefPtr<MediaTrack>, 4> mediaTracksToAdd;
+ if (shouldHaveTrackSources) {
+ GetAllEnabledMediaTracks(mediaTracksToAdd);
+ }
+
+ // ...and all MediaElementTrackSources.
+ auto trackSourcesToRemove =
+ ToTArray<AutoTArray<nsString, 4>>(mOutputTrackSources.Keys());
+
+ // Then work out the differences.
+ mediaTracksToAdd.RemoveLastElements(
+ mediaTracksToAdd.end() -
+ std::remove_if(mediaTracksToAdd.begin(), mediaTracksToAdd.end(),
+ [this, &trackSourcesToRemove](const auto& track) {
+ const bool remove =
+ mOutputTrackSources.GetWeak(track->GetId());
+ if (remove) {
+ trackSourcesToRemove.RemoveElement(track->GetId());
+ }
+ return remove;
+ }));
+
+ // First remove stale track sources.
+ for (const auto& id : trackSourcesToRemove) {
+ RefPtr<MediaElementTrackSource> source = mOutputTrackSources.GetWeak(id);
+
+ LOG(LogLevel::Debug, ("Removing output track source %p for track %s",
+ source.get(), NS_ConvertUTF16toUTF8(id).get()));
+
+ if (mDecoder) {
+ mDecoder->RemoveOutputTrack(source->Track());
+ }
+
+ // The source of this track just ended. Force-notify that it ended.
+ // If we bounce it to the MediaTrackGraph it might not be picked up,
+ // for instance if the MediaInputPort was destroyed in the same
+ // iteration as it was added.
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NewRunnableMethod("MediaElementTrackSource::OverrideEnded", source,
+ &MediaElementTrackSource::OverrideEnded));
+
+ // Remove the track from the MediaStream after it ended.
+ for (OutputMediaStream& ms : mOutputStreams) {
+ if (source->Track()->mType == MediaSegment::VIDEO &&
+ ms.mCapturingAudioOnly) {
+ continue;
+ }
+ DebugOnly<size_t> length = ms.mLiveTracks.Length();
+ ms.mLiveTracks.RemoveElementsBy(
+ [&](const RefPtr<MediaStreamTrack>& aTrack) {
+ if (&aTrack->GetSource() != source) {
+ return false;
+ }
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NewRunnableMethod<RefPtr<MediaStreamTrack>>(
+ "DOMMediaStream::RemoveTrackInternal", ms.mStream,
+ &DOMMediaStream::RemoveTrackInternal, aTrack));
+ return true;
+ });
+ MOZ_ASSERT(ms.mLiveTracks.Length() == length - 1);
+ }
+
+ mOutputTrackSources.Remove(id);
+ }
+
+ // Then update finish-when-ended output streams as needed.
+ for (size_t i = mOutputStreams.Length(); i-- > 0;) {
+ if (!mOutputStreams[i].mFinishWhenEnded) {
+ continue;
+ }
+
+ if (!mOutputStreams[i].mFinishWhenEndedLoadingSrc &&
+ !mOutputStreams[i].mFinishWhenEndedAttrStream &&
+ !mOutputStreams[i].mFinishWhenEndedMediaSource) {
+ // This finish-when-ended stream has not seen any source loaded yet.
+ // Update the loading src if it's time.
+ if (!IsPlaybackEnded()) {
+ if (mLoadingSrc) {
+ mOutputStreams[i].mFinishWhenEndedLoadingSrc = mLoadingSrc;
+ } else if (mSrcAttrStream) {
+ mOutputStreams[i].mFinishWhenEndedAttrStream = mSrcAttrStream;
+ } else if (mSrcMediaSource) {
+ mOutputStreams[i].mFinishWhenEndedMediaSource = mSrcMediaSource;
+ }
+ }
+ continue;
+ }
+
+ // Discard finish-when-ended output streams with a loading src set as
+ // needed.
+ if (!IsPlaybackEnded() &&
+ mLoadingSrc == mOutputStreams[i].mFinishWhenEndedLoadingSrc) {
+ continue;
+ }
+ if (!IsPlaybackEnded() &&
+ mSrcAttrStream == mOutputStreams[i].mFinishWhenEndedAttrStream) {
+ continue;
+ }
+ if (!IsPlaybackEnded() &&
+ mSrcMediaSource == mOutputStreams[i].mFinishWhenEndedMediaSource) {
+ continue;
+ }
+ LOG(LogLevel::Debug,
+ ("Playback ended or source changed. Discarding stream %p",
+ mOutputStreams[i].mStream.get()));
+ mOutputStreams.RemoveElementAt(i);
+ if (mOutputStreams.IsEmpty()) {
+ mTracksCaptured = nullptr;
+ // mTracksCaptured is one of the Watchables triggering this method.
+ // Unsetting it here means we'll run through this method again very soon.
+ return;
+ }
+ }
+
+ // Finally add new MediaTracks.
+ for (const auto& mediaTrack : mediaTracksToAdd) {
+ nsAutoString id;
+ mediaTrack->GetId(id);
+
+ MediaSegment::Type type;
+ if (mediaTrack->AsAudioTrack()) {
+ type = MediaSegment::AUDIO;
+ } else if (mediaTrack->AsVideoTrack()) {
+ type = MediaSegment::VIDEO;
+ } else {
+ MOZ_CRASH("Unknown track type");
+ }
+
+ RefPtr<ProcessedMediaTrack> track;
+ RefPtr<MediaElementTrackSource> source;
+ if (mDecoder) {
+ track = mTracksCaptured.Ref()->mTrack->Graph()->CreateForwardedInputTrack(
+ type);
+ RefPtr<nsIPrincipal> principal = GetCurrentPrincipal();
+ if (!principal || IsCORSSameOrigin()) {
+ principal = NodePrincipal();
+ }
+ source = MakeAndAddRef<MediaElementTrackSource>(
+ track, principal, OutputTracksMuted(),
+ type == MediaSegment::VIDEO
+ ? HTMLVideoElement::FromNode(this)->HasAlpha()
+ : false);
+ mDecoder->AddOutputTrack(track);
+ } else if (mSrcStream) {
+ MediaStreamTrack* inputTrack;
+ if (AudioTrack* t = mediaTrack->AsAudioTrack()) {
+ inputTrack = t->GetAudioStreamTrack();
+ } else if (VideoTrack* t = mediaTrack->AsVideoTrack()) {
+ inputTrack = t->GetVideoStreamTrack();
+ } else {
+ MOZ_CRASH("Unknown track type");
+ }
+ MOZ_ASSERT(inputTrack);
+ if (!inputTrack) {
+ NS_ERROR("Input track not found in source stream");
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(!inputTrack->Ended());
+
+ track = inputTrack->Graph()->CreateForwardedInputTrack(type);
+ RefPtr<MediaInputPort> port = inputTrack->ForwardTrackContentsTo(track);
+ source = MakeAndAddRef<MediaElementTrackSource>(
+ inputTrack, &inputTrack->GetSource(), track, port,
+ OutputTracksMuted());
+
+ // Track is muted initially, so we don't leak data if it's added while
+ // paused and an MTG iteration passes before the mute comes into effect.
+ source->SetEnabled(mSrcStreamIsPlaying);
+ } else {
+ MOZ_CRASH("Unknown source");
+ }
+
+ LOG(LogLevel::Debug, ("Adding output track source %p for track %s",
+ source.get(), NS_ConvertUTF16toUTF8(id).get()));
+
+ track->QueueSetAutoend(false);
+ MOZ_DIAGNOSTIC_ASSERT(!mOutputTrackSources.Contains(id));
+ mOutputTrackSources.InsertOrUpdate(id, RefPtr{source});
+
+ // Add the new track source to any existing output streams
+ for (OutputMediaStream& ms : mOutputStreams) {
+ if (source->Track()->mType == MediaSegment::VIDEO &&
+ ms.mCapturingAudioOnly) {
+ // If the output stream is for audio only we ignore video sources.
+ continue;
+ }
+ AddOutputTrackSourceToOutputStream(source, ms);
+ }
+ }
+}
+
+bool HTMLMediaElement::CanBeCaptured(StreamCaptureType aCaptureType) {
+ // Don't bother capturing when the document has gone away
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ if (!window) {
+ return false;
+ }
+
+ // Prevent capturing restricted video
+ if (aCaptureType == StreamCaptureType::CAPTURE_ALL_TRACKS &&
+ ContainsRestrictedContent()) {
+ return false;
+ }
+ return true;
+}
+
+already_AddRefed<DOMMediaStream> HTMLMediaElement::CaptureStreamInternal(
+ StreamCaptureBehavior aFinishBehavior, StreamCaptureType aStreamCaptureType,
+ MediaTrackGraph* aGraph) {
+ MOZ_ASSERT(CanBeCaptured(aStreamCaptureType));
+
+ LogVisibility(CallerAPI::CAPTURE_STREAM);
+ MarkAsTainted();
+
+ if (mTracksCaptured.Ref()) {
+ // Already have an output stream. Check whether the graph rate matches if
+ // specified.
+ if (aGraph && aGraph != mTracksCaptured.Ref()->mTrack->Graph()) {
+ return nullptr;
+ }
+ } else {
+ // This is the first output stream, or there are no tracks. If the former,
+ // start capturing all tracks. If the latter, they will be added later.
+ MediaTrackGraph* graph = aGraph;
+ if (!graph) {
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ if (!window) {
+ return nullptr;
+ }
+
+ MediaTrackGraph::GraphDriverType graphDriverType =
+ HasAudio() ? MediaTrackGraph::AUDIO_THREAD_DRIVER
+ : MediaTrackGraph::SYSTEM_THREAD_DRIVER;
+ graph = MediaTrackGraph::GetInstance(
+ graphDriverType, window, MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ MediaTrackGraph::DEFAULT_OUTPUT_DEVICE);
+ }
+ mTracksCaptured = MakeRefPtr<SharedDummyTrack>(
+ graph->CreateSourceTrack(MediaSegment::AUDIO));
+ UpdateOutputTrackSources();
+ }
+
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ OutputMediaStream* out = mOutputStreams.EmplaceBack(
+ MakeRefPtr<DOMMediaStream>(window),
+ aStreamCaptureType == StreamCaptureType::CAPTURE_AUDIO,
+ aFinishBehavior == StreamCaptureBehavior::FINISH_WHEN_ENDED);
+
+ if (aFinishBehavior == StreamCaptureBehavior::FINISH_WHEN_ENDED &&
+ !mOutputTrackSources.IsEmpty()) {
+ // This output stream won't receive any more tracks when playback of the
+ // current src of this media element ends, or when the src of this media
+ // element changes. If we're currently playing something (i.e., if there are
+ // tracks currently captured), set the current src on the output stream so
+ // this can be tracked. If we're not playing anything,
+ // UpdateOutputTrackSources will set the current src when it becomes
+ // available later.
+ if (mLoadingSrc) {
+ out->mFinishWhenEndedLoadingSrc = mLoadingSrc;
+ }
+ if (mSrcAttrStream) {
+ out->mFinishWhenEndedAttrStream = mSrcAttrStream;
+ }
+ if (mSrcMediaSource) {
+ out->mFinishWhenEndedMediaSource = mSrcMediaSource;
+ }
+ MOZ_ASSERT(out->mFinishWhenEndedLoadingSrc ||
+ out->mFinishWhenEndedAttrStream ||
+ out->mFinishWhenEndedMediaSource);
+ }
+
+ if (aStreamCaptureType == StreamCaptureType::CAPTURE_AUDIO) {
+ if (mSrcStream) {
+ // We don't support applying volume and mute to the captured stream, when
+ // capturing a MediaStream.
+ ReportToConsole(nsIScriptError::errorFlag,
+ "MediaElementAudioCaptureOfMediaStreamError");
+ }
+
+ // mAudioCaptured tells the user that the audio played by this media element
+ // is being routed to the captureStreams *instead* of being played to
+ // speakers.
+ mAudioCaptured = true;
+ }
+
+ for (const RefPtr<MediaElementTrackSource>& source :
+ mOutputTrackSources.Values()) {
+ if (source->Track()->mType == MediaSegment::VIDEO) {
+ // Only add video tracks if we're a video element and the output stream
+ // wants video.
+ if (!IsVideo()) {
+ continue;
+ }
+ if (out->mCapturingAudioOnly) {
+ continue;
+ }
+ }
+ AddOutputTrackSourceToOutputStream(source, *out, AddTrackMode::SYNC);
+ }
+
+ return do_AddRef(out->mStream);
+}
+
+already_AddRefed<DOMMediaStream> HTMLMediaElement::CaptureAudio(
+ ErrorResult& aRv, MediaTrackGraph* aGraph) {
+ MOZ_RELEASE_ASSERT(aGraph);
+
+ if (!CanBeCaptured(StreamCaptureType::CAPTURE_AUDIO)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<DOMMediaStream> stream =
+ CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED,
+ StreamCaptureType::CAPTURE_AUDIO, aGraph);
+ if (!stream) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ return stream.forget();
+}
+
+RefPtr<GenericNonExclusivePromise> HTMLMediaElement::GetAllowedToPlayPromise() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mOutputStreams.IsEmpty(),
+ "This method should only be called during stream capturing!");
+ if (AllowedToPlay()) {
+ AUTOPLAY_LOG("MediaElement %p has allowed to play, resolve promise", this);
+ return GenericNonExclusivePromise::CreateAndResolve(true, __func__);
+ }
+ AUTOPLAY_LOG("create allow-to-play promise for MediaElement %p", this);
+ return mAllowedToPlayPromise.Ensure(__func__);
+}
+
+already_AddRefed<DOMMediaStream> HTMLMediaElement::MozCaptureStream(
+ ErrorResult& aRv) {
+ if (!CanBeCaptured(StreamCaptureType::CAPTURE_ALL_TRACKS)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<DOMMediaStream> stream =
+ CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED,
+ StreamCaptureType::CAPTURE_ALL_TRACKS, nullptr);
+ if (!stream) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ return stream.forget();
+}
+
+already_AddRefed<DOMMediaStream> HTMLMediaElement::MozCaptureStreamUntilEnded(
+ ErrorResult& aRv) {
+ if (!CanBeCaptured(StreamCaptureType::CAPTURE_ALL_TRACKS)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<DOMMediaStream> stream =
+ CaptureStreamInternal(StreamCaptureBehavior::FINISH_WHEN_ENDED,
+ StreamCaptureType::CAPTURE_ALL_TRACKS, nullptr);
+ if (!stream) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ return stream.forget();
+}
+
+class MediaElementSetForURI : public nsURIHashKey {
+ public:
+ explicit MediaElementSetForURI(const nsIURI* aKey) : nsURIHashKey(aKey) {}
+ MediaElementSetForURI(MediaElementSetForURI&& aOther) noexcept
+ : nsURIHashKey(std::move(aOther)),
+ mElements(std::move(aOther.mElements)) {}
+ nsTArray<HTMLMediaElement*> mElements;
+};
+
+using MediaElementURITable = nsTHashtable<MediaElementSetForURI>;
+// Elements in this table must have non-null mDecoder and mLoadingSrc, and those
+// can't change while the element is in the table. The table is keyed by
+// the element's mLoadingSrc. Each entry has a list of all elements with the
+// same mLoadingSrc.
+static MediaElementURITable* gElementTable;
+
+#ifdef DEBUG
+static bool URISafeEquals(nsIURI* a1, nsIURI* a2) {
+ if (!a1 || !a2) {
+ // Consider two empty URIs *not* equal!
+ return false;
+ }
+ bool equal = false;
+ nsresult rv = a1->Equals(a2, &equal);
+ return NS_SUCCEEDED(rv) && equal;
+}
+// Returns the number of times aElement appears in the media element table
+// for aURI. If this returns other than 0 or 1, there's a bug somewhere!
+static unsigned MediaElementTableCount(HTMLMediaElement* aElement,
+ nsIURI* aURI) {
+ if (!gElementTable || !aElement) {
+ return 0;
+ }
+ uint32_t uriCount = 0;
+ uint32_t otherCount = 0;
+ for (const auto& entry : *gElementTable) {
+ uint32_t count = 0;
+ for (const auto& elem : entry.mElements) {
+ if (elem == aElement) {
+ count++;
+ }
+ }
+ if (URISafeEquals(aURI, entry.GetKey())) {
+ uriCount = count;
+ } else {
+ otherCount += count;
+ }
+ }
+ NS_ASSERTION(otherCount == 0, "Should not have entries for unknown URIs");
+ return uriCount;
+}
+#endif
+
+void HTMLMediaElement::AddMediaElementToURITable() {
+ NS_ASSERTION(mDecoder, "Call this only with decoder Load called");
+ NS_ASSERTION(
+ MediaElementTableCount(this, mLoadingSrc) == 0,
+ "Should not have entry for element in element table before addition");
+ if (!gElementTable) {
+ gElementTable = new MediaElementURITable();
+ }
+ MediaElementSetForURI* entry = gElementTable->PutEntry(mLoadingSrc);
+ entry->mElements.AppendElement(this);
+ NS_ASSERTION(
+ MediaElementTableCount(this, mLoadingSrc) == 1,
+ "Should have a single entry for element in element table after addition");
+}
+
+void HTMLMediaElement::RemoveMediaElementFromURITable() {
+ if (!mDecoder || !mLoadingSrc || !gElementTable) {
+ return;
+ }
+ MediaElementSetForURI* entry = gElementTable->GetEntry(mLoadingSrc);
+ if (!entry) {
+ return;
+ }
+ entry->mElements.RemoveElement(this);
+ if (entry->mElements.IsEmpty()) {
+ gElementTable->RemoveEntry(entry);
+ if (gElementTable->Count() == 0) {
+ delete gElementTable;
+ gElementTable = nullptr;
+ }
+ }
+ NS_ASSERTION(MediaElementTableCount(this, mLoadingSrc) == 0,
+ "After remove, should no longer have an entry in element table");
+}
+
+HTMLMediaElement* HTMLMediaElement::LookupMediaElementURITable(nsIURI* aURI) {
+ if (!gElementTable) {
+ return nullptr;
+ }
+ MediaElementSetForURI* entry = gElementTable->GetEntry(aURI);
+ if (!entry) {
+ return nullptr;
+ }
+ for (uint32_t i = 0; i < entry->mElements.Length(); ++i) {
+ HTMLMediaElement* elem = entry->mElements[i];
+ bool equal;
+ // Look for elements that have the same principal and CORS mode.
+ // Ditto for anything else that could cause us to send different headers.
+ if (NS_SUCCEEDED(elem->NodePrincipal()->Equals(NodePrincipal(), &equal)) &&
+ equal && elem->mCORSMode == mCORSMode) {
+ // See SetupDecoder() below. We only add a element to the table when
+ // mDecoder is a ChannelMediaDecoder.
+ auto* decoder = static_cast<ChannelMediaDecoder*>(elem->mDecoder.get());
+ NS_ASSERTION(decoder, "Decoder gone");
+ if (decoder->CanClone()) {
+ return elem;
+ }
+ }
+ }
+ return nullptr;
+}
+
+class HTMLMediaElement::ShutdownObserver : public nsIObserver {
+ enum class Phase : int8_t { Init, Subscribed, Unsubscribed };
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Observe(nsISupports*, const char* aTopic,
+ const char16_t*) override {
+ if (mPhase != Phase::Subscribed) {
+ // Bail out if we are not subscribed for this might be called even after
+ // |nsContentUtils::UnregisterShutdownObserver(this)|.
+ return NS_OK;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(mWeak);
+ if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
+ mWeak->NotifyShutdownEvent();
+ }
+ return NS_OK;
+ }
+ void Subscribe(HTMLMediaElement* aPtr) {
+ MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Init);
+ MOZ_DIAGNOSTIC_ASSERT(!mWeak);
+ mWeak = aPtr;
+ nsContentUtils::RegisterShutdownObserver(this);
+ mPhase = Phase::Subscribed;
+ }
+ void Unsubscribe() {
+ MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Subscribed);
+ MOZ_DIAGNOSTIC_ASSERT(mWeak);
+ MOZ_DIAGNOSTIC_ASSERT(!mAddRefed,
+ "ReleaseMediaElement should have been called first");
+ mWeak = nullptr;
+ nsContentUtils::UnregisterShutdownObserver(this);
+ mPhase = Phase::Unsubscribed;
+ }
+ void AddRefMediaElement() {
+ MOZ_DIAGNOSTIC_ASSERT(mWeak);
+ MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, "Should only ever AddRef once");
+ mWeak->AddRef();
+ mAddRefed = true;
+ }
+ void ReleaseMediaElement() {
+ MOZ_DIAGNOSTIC_ASSERT(mWeak);
+ MOZ_DIAGNOSTIC_ASSERT(mAddRefed, "Should only release after AddRef");
+ mWeak->Release();
+ mAddRefed = false;
+ }
+
+ private:
+ virtual ~ShutdownObserver() {
+ MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Unsubscribed);
+ MOZ_DIAGNOSTIC_ASSERT(!mWeak);
+ MOZ_DIAGNOSTIC_ASSERT(!mAddRefed,
+ "ReleaseMediaElement should have been called first");
+ }
+ // Guaranteed to be valid by HTMLMediaElement.
+ HTMLMediaElement* mWeak = nullptr;
+ Phase mPhase = Phase::Init;
+ bool mAddRefed = false;
+};
+
+NS_IMPL_ISUPPORTS(HTMLMediaElement::ShutdownObserver, nsIObserver)
+
+class HTMLMediaElement::TitleChangeObserver final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit TitleChangeObserver(HTMLMediaElement* aElement)
+ : mElement(aElement) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aElement);
+ }
+
+ NS_IMETHOD Observe(nsISupports*, const char* aTopic,
+ const char16_t*) override {
+ if (mElement) {
+ mElement->UpdateStreamName();
+ }
+
+ return NS_OK;
+ }
+
+ void Subscribe() {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "document-title-changed", false);
+ }
+ }
+
+ void Unsubscribe() {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, "document-title-changed");
+ }
+ }
+
+ private:
+ ~TitleChangeObserver() = default;
+
+ WeakPtr<HTMLMediaElement> mElement;
+};
+
+NS_IMPL_ISUPPORTS(HTMLMediaElement::TitleChangeObserver, nsIObserver)
+
+HTMLMediaElement::HTMLMediaElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)),
+ mWatchManager(this, AbstractThread::MainThread()),
+ mShutdownObserver(new ShutdownObserver),
+ mTitleChangeObserver(new TitleChangeObserver(this)),
+ mEventBlocker(new EventBlocker(this)),
+ mPlayed(new TimeRanges(ToSupports(OwnerDoc()))),
+ mTracksCaptured(nullptr, "HTMLMediaElement::mTracksCaptured"),
+ mErrorSink(new ErrorSink(this)),
+ mAudioChannelWrapper(new AudioChannelAgentCallback(this)),
+ mSink(std::pair(nsString(), RefPtr<AudioDeviceInfo>())),
+ mShowPoster(IsVideo()),
+ mMediaControlKeyListener(new MediaControlKeyListener(this)) {
+ MOZ_ASSERT(GetMainThreadSerialEventTarget());
+ // Please don't add anything to this constructor or the initialization
+ // list that can cause AddRef to be called. This prevents subclasses
+ // from overriding AddRef in a way that works with our refcount
+ // logging mechanisms. Put these things inside of the ::Init method
+ // instead.
+}
+
+void HTMLMediaElement::Init() {
+ MOZ_ASSERT(mRefCnt == 0 && !mRefCnt.IsPurple(),
+ "HTMLMediaElement::Init called when AddRef has been called "
+ "at least once already, probably in the constructor. Please "
+ "see the documentation in the HTMLMediaElement constructor.");
+ MOZ_ASSERT(!mRefCnt.IsPurple());
+
+ mAudioTrackList = new AudioTrackList(OwnerDoc()->GetParentObject(), this);
+ mVideoTrackList = new VideoTrackList(OwnerDoc()->GetParentObject(), this);
+
+ DecoderDoctorLogger::LogConstruction(this);
+
+ mWatchManager.Watch(mPaused, &HTMLMediaElement::UpdateWakeLock);
+ mWatchManager.Watch(mPaused, &HTMLMediaElement::UpdateOutputTracksMuting);
+ mWatchManager.Watch(
+ mPaused, &HTMLMediaElement::NotifyMediaControlPlaybackStateChanged);
+ mWatchManager.Watch(mReadyState, &HTMLMediaElement::UpdateOutputTracksMuting);
+
+ mWatchManager.Watch(mTracksCaptured,
+ &HTMLMediaElement::UpdateOutputTrackSources);
+ mWatchManager.Watch(mReadyState, &HTMLMediaElement::UpdateOutputTrackSources);
+
+ mWatchManager.Watch(mDownloadSuspendedByCache,
+ &HTMLMediaElement::UpdateReadyStateInternal);
+ mWatchManager.Watch(mFirstFrameLoaded,
+ &HTMLMediaElement::UpdateReadyStateInternal);
+ mWatchManager.Watch(mSrcStreamPlaybackEnded,
+ &HTMLMediaElement::UpdateReadyStateInternal);
+
+ ErrorResult rv;
+
+ double defaultVolume = Preferences::GetFloat("media.default_volume", 1.0);
+ SetVolume(defaultVolume, rv);
+
+ RegisterActivityObserver();
+ NotifyOwnerDocumentActivityChanged();
+
+ // We initialize the MediaShutdownManager as the HTMLMediaElement is always
+ // constructed on the main thread, and not during stable state.
+ // (MediaShutdownManager make use of nsIAsyncShutdownClient which is written
+ // in JS)
+ MediaShutdownManager::InitStatics();
+
+#if defined(MOZ_WIDGET_ANDROID)
+ GVAutoplayPermissionRequestor::AskForPermissionIfNeeded(
+ OwnerDoc()->GetInnerWindow());
+#endif
+
+ OwnerDoc()->SetDocTreeHadMedia();
+ mShutdownObserver->Subscribe(this);
+ mInitialized = true;
+}
+
+HTMLMediaElement::~HTMLMediaElement() {
+ MOZ_ASSERT(mInitialized,
+ "HTMLMediaElement must be initialized before it is destroyed.");
+ NS_ASSERTION(
+ !mHasSelfReference,
+ "How can we be destroyed if we're still holding a self reference?");
+
+ mWatchManager.Shutdown();
+
+ mShutdownObserver->Unsubscribe();
+
+ mTitleChangeObserver->Unsubscribe();
+
+ if (mVideoFrameContainer) {
+ mVideoFrameContainer->ForgetElement();
+ }
+ UnregisterActivityObserver();
+
+ mSetCDMRequest.DisconnectIfExists();
+ mAllowedToPlayPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
+
+ if (mDecoder) {
+ ShutdownDecoder();
+ }
+ if (mProgressTimer) {
+ StopProgress();
+ }
+ if (mSrcStream) {
+ EndSrcMediaStreamPlayback();
+ }
+
+ NS_ASSERTION(MediaElementTableCount(this, mLoadingSrc) == 0,
+ "Destroyed media element should no longer be in element table");
+
+ if (mChannelLoader) {
+ mChannelLoader->Cancel();
+ }
+
+ if (mAudioChannelWrapper) {
+ mAudioChannelWrapper->Shutdown();
+ mAudioChannelWrapper = nullptr;
+ }
+
+ if (mResumeDelayedPlaybackAgent) {
+ mResumePlaybackRequest.DisconnectIfExists();
+ mResumeDelayedPlaybackAgent = nullptr;
+ }
+
+ mMediaControlKeyListener->StopIfNeeded();
+ mMediaControlKeyListener = nullptr;
+
+ WakeLockRelease();
+
+ DecoderDoctorLogger::LogDestruction(this);
+}
+
+void HTMLMediaElement::StopSuspendingAfterFirstFrame() {
+ mAllowSuspendAfterFirstFrame = false;
+ if (!mSuspendedAfterFirstFrame) return;
+ mSuspendedAfterFirstFrame = false;
+ if (mDecoder) {
+ mDecoder->Resume();
+ }
+}
+
+void HTMLMediaElement::SetPlayedOrSeeked(bool aValue) {
+ if (aValue == mHasPlayedOrSeeked) {
+ return;
+ }
+
+ mHasPlayedOrSeeked = aValue;
+
+ // Force a reflow so that the poster frame hides or shows immediately.
+ nsIFrame* frame = GetPrimaryFrame();
+ if (!frame) {
+ return;
+ }
+ frame->PresShell()->FrameNeedsReflow(frame, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_IS_DIRTY);
+}
+
+void HTMLMediaElement::NotifyXPCOMShutdown() { ShutdownDecoder(); }
+
+already_AddRefed<Promise> HTMLMediaElement::Play(ErrorResult& aRv) {
+ LOG(LogLevel::Debug,
+ ("%p Play() called by JS readyState=%d", this, mReadyState.Ref()));
+
+ // 4.8.12.8
+ // When the play() method on a media element is invoked, the user agent must
+ // run the following steps.
+
+ RefPtr<PlayPromise> promise = CreatePlayPromise(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // 4.8.12.8 - Step 1:
+ // If the media element is not allowed to play, return a promise rejected
+ // with a "NotAllowedError" DOMException and abort these steps.
+ // NOTE: we may require requesting permission from the user, so we do the
+ // "not allowed" check below.
+
+ // 4.8.12.8 - Step 2:
+ // If the media element's error attribute is not null and its code
+ // attribute has the value MEDIA_ERR_SRC_NOT_SUPPORTED, return a promise
+ // rejected with a "NotSupportedError" DOMException and abort these steps.
+ if (GetError() && GetError()->Code() == MEDIA_ERR_SRC_NOT_SUPPORTED) {
+ LOG(LogLevel::Debug,
+ ("%p Play() promise rejected because source not supported.", this));
+ promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR);
+ return promise.forget();
+ }
+
+ // 4.8.12.8 - Step 3:
+ // Let promise be a new promise and append promise to the list of pending
+ // play promises.
+ // Note: Promise appended to list of pending promises as needed below.
+
+ if (ShouldBeSuspendedByInactiveDocShell()) {
+ LOG(LogLevel::Debug, ("%p no allow to play by the docShell for now", this));
+ mPendingPlayPromises.AppendElement(promise);
+ return promise.forget();
+ }
+
+ // We may delay starting playback of a media resource for an unvisited tab
+ // until it's going to foreground or being resumed by the play tab icon.
+ if (MediaPlaybackDelayPolicy::ShouldDelayPlayback(this)) {
+ CreateResumeDelayedMediaPlaybackAgentIfNeeded();
+ LOG(LogLevel::Debug, ("%p delay Play() call", this));
+ MaybeDoLoad();
+ // When play is delayed, save a reference to the promise, and return it.
+ // The promise will be resolved when we resume play by either the tab is
+ // brought to the foreground, or the audio tab indicator is clicked.
+ mPendingPlayPromises.AppendElement(promise);
+ return promise.forget();
+ }
+
+ const bool handlingUserInput = UserActivation::IsHandlingUserInput();
+ mPendingPlayPromises.AppendElement(promise);
+
+ if (AllowedToPlay()) {
+ AUTOPLAY_LOG("allow MediaElement %p to play", this);
+ mAllowedToPlayPromise.ResolveIfExists(true, __func__);
+ PlayInternal(handlingUserInput);
+ UpdateCustomPolicyAfterPlayed();
+ } else {
+ AUTOPLAY_LOG("reject MediaElement %p to play", this);
+ AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
+ }
+ return promise.forget();
+}
+
+void HTMLMediaElement::DispatchEventsWhenPlayWasNotAllowed() {
+ if (StaticPrefs::media_autoplay_block_event_enabled()) {
+ DispatchAsyncEvent(u"blocked"_ns);
+ }
+ DispatchBlockEventForVideoControl();
+ if (!mHasEverBeenBlockedForAutoplay) {
+ MaybeNotifyAutoplayBlocked();
+ ReportToConsole(nsIScriptError::warningFlag, "BlockAutoplayError");
+ mHasEverBeenBlockedForAutoplay = true;
+ }
+}
+
+void HTMLMediaElement::MaybeNotifyAutoplayBlocked() {
+ // This event is used to notify front-end side that we've blocked autoplay,
+ // so front-end side should show blocking icon as well.
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(OwnerDoc(), u"GloballyAutoplayBlocked"_ns,
+ CanBubble::eYes, ChromeOnlyDispatch::eYes);
+ asyncDispatcher->PostDOMEvent();
+}
+
+void HTMLMediaElement::DispatchBlockEventForVideoControl() {
+#if defined(MOZ_WIDGET_ANDROID)
+ nsVideoFrame* videoFrame = do_QueryFrame(GetPrimaryFrame());
+ if (!videoFrame || !videoFrame->GetVideoControls()) {
+ return;
+ }
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(
+ videoFrame->GetVideoControls(), u"MozNoControlsBlockedVideo"_ns,
+ CanBubble::eYes);
+ asyncDispatcher->PostDOMEvent();
+#endif
+}
+
+void HTMLMediaElement::PlayInternal(bool aHandlingUserInput) {
+ if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE) {
+ // The media load algorithm will be initiated by a user interaction.
+ // We want to boost the channel priority for better responsiveness.
+ // Note this must be done before UpdatePreloadAction() which will
+ // update |mPreloadAction|.
+ mUseUrgentStartForChannel = true;
+ }
+
+ StopSuspendingAfterFirstFrame();
+ SetPlayedOrSeeked(true);
+
+ // 4.8.12.8 - Step 4:
+ // If the media element's networkState attribute has the value NETWORK_EMPTY,
+ // invoke the media element's resource selection algorithm.
+ MaybeDoLoad();
+ if (mSuspendedForPreloadNone) {
+ ResumeLoad(PRELOAD_ENOUGH);
+ }
+
+ // 4.8.12.8 - Step 5:
+ // If the playback has ended and the direction of playback is forwards,
+ // seek to the earliest possible position of the media resource.
+
+ // Even if we just did Load() or ResumeLoad(), we could already have a decoder
+ // here if we managed to clone an existing decoder.
+ if (mDecoder) {
+ if (mDecoder->IsEnded()) {
+ SetCurrentTime(0);
+ }
+ if (!mSuspendedByInactiveDocOrDocshell) {
+ mDecoder->Play();
+ }
+ }
+
+ if (mCurrentPlayRangeStart == -1.0) {
+ mCurrentPlayRangeStart = CurrentTime();
+ }
+
+ const bool oldPaused = mPaused;
+ mPaused = false;
+ // Step 5,
+ // https://html.spec.whatwg.org/multipage/media.html#internal-play-steps
+ mCanAutoplayFlag = false;
+
+ // We changed mPaused and mCanAutoplayFlag which can affect
+ // AddRemoveSelfReference and our preload status.
+ AddRemoveSelfReference();
+ UpdatePreloadAction();
+ UpdateSrcMediaStreamPlaying();
+ StartMediaControlKeyListenerIfNeeded();
+
+ // Once play() has been called in a user generated event handler,
+ // it is allowed to autoplay. Note: we can reach here when not in
+ // a user generated event handler if our readyState has not yet
+ // reached HAVE_METADATA.
+ mIsBlessed |= aHandlingUserInput;
+
+ // TODO: If the playback has ended, then the user agent must set
+ // seek to the effective start.
+
+ // 4.8.12.8 - Step 6:
+ // If the media element's paused attribute is true, run the following steps:
+ if (oldPaused) {
+ // 6.1. Change the value of paused to false. (Already done.)
+ // This step is uplifted because the "block-media-playback" feature needs
+ // the mPaused to be false before UpdateAudioChannelPlayingState() being
+ // called.
+
+ // 6.2. If the show poster flag is true, set the element's show poster flag
+ // to false and run the time marches on steps.
+ if (mShowPoster) {
+ mShowPoster = false;
+ if (mTextTrackManager) {
+ mTextTrackManager->TimeMarchesOn();
+ }
+ }
+
+ // 6.3. Queue a task to fire a simple event named play at the element.
+ DispatchAsyncEvent(u"play"_ns);
+
+ // 6.4. If the media element's readyState attribute has the value
+ // HAVE_NOTHING, HAVE_METADATA, or HAVE_CURRENT_DATA, queue a task to
+ // fire a simple event named waiting at the element.
+ // Otherwise, the media element's readyState attribute has the value
+ // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA: notify about playing for the
+ // element.
+ switch (mReadyState) {
+ case HAVE_NOTHING:
+ DispatchAsyncEvent(u"waiting"_ns);
+ break;
+ case HAVE_METADATA:
+ case HAVE_CURRENT_DATA:
+ DispatchAsyncEvent(u"waiting"_ns);
+ break;
+ case HAVE_FUTURE_DATA:
+ case HAVE_ENOUGH_DATA:
+ NotifyAboutPlaying();
+ break;
+ }
+ } else if (mReadyState >= HAVE_FUTURE_DATA) {
+ // 7. Otherwise, if the media element's readyState attribute has the value
+ // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, take pending play promises and
+ // queue a task to resolve pending play promises with the result.
+ AsyncResolvePendingPlayPromises();
+ }
+
+ // 8. Set the media element's autoplaying flag to false. (Already done.)
+
+ // 9. Return promise.
+ // (Done in caller.)
+}
+
+void HTMLMediaElement::MaybeDoLoad() {
+ if (mNetworkState == NETWORK_EMPTY) {
+ DoLoad();
+ }
+}
+
+void HTMLMediaElement::UpdateWakeLock() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Ensure we have a wake lock if we're playing audibly. This ensures the
+ // device doesn't sleep while playing.
+ bool playing = !mPaused;
+ bool isAudible = Volume() > 0.0 && !mMuted && mIsAudioTrackAudible;
+ // WakeLock when playing audible media.
+ if (playing && isAudible) {
+ CreateAudioWakeLockIfNeeded();
+ } else {
+ ReleaseAudioWakeLockIfExists();
+ }
+}
+
+void HTMLMediaElement::CreateAudioWakeLockIfNeeded() {
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ return;
+ }
+ if (!mWakeLock) {
+ RefPtr<power::PowerManagerService> pmService =
+ power::PowerManagerService::GetInstance();
+ NS_ENSURE_TRUE_VOID(pmService);
+
+ ErrorResult rv;
+ mWakeLock = pmService->NewWakeLock(u"audio-playing"_ns,
+ OwnerDoc()->GetInnerWindow(), rv);
+ }
+}
+
+void HTMLMediaElement::ReleaseAudioWakeLockIfExists() {
+ if (mWakeLock) {
+ ErrorResult rv;
+ mWakeLock->Unlock(rv);
+ rv.SuppressException();
+ mWakeLock = nullptr;
+ }
+}
+
+void HTMLMediaElement::WakeLockRelease() { ReleaseAudioWakeLockIfExists(); }
+
+void HTMLMediaElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ if (!this->Controls() || !aVisitor.mEvent->mFlags.mIsTrusted) {
+ nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+ return;
+ }
+
+ // We will need to trap pointer, touch, and mouse events within the media
+ // element, allowing media control exclusive consumption on these events,
+ // and preventing the content from handling them.
+ switch (aVisitor.mEvent->mMessage) {
+ case ePointerDown:
+ case ePointerUp:
+ case eTouchEnd:
+ // Always prevent touchmove captured in video element from being handled by
+ // content, since we always do that for touchstart.
+ case eTouchMove:
+ case eTouchStart:
+ case eMouseClick:
+ case eMouseDoubleClick:
+ case eMouseDown:
+ case eMouseUp:
+ aVisitor.mCanHandle = false;
+ return;
+
+ // The *move events however are only comsumed when the range input is being
+ // dragged.
+ case ePointerMove:
+ case eMouseMove: {
+ nsINode* node =
+ nsINode::FromEventTargetOrNull(aVisitor.mEvent->mOriginalTarget);
+ if (MOZ_UNLIKELY(!node)) {
+ return;
+ }
+ HTMLInputElement* el = nullptr;
+ if (node->ChromeOnlyAccess()) {
+ if (node->IsHTMLElement(nsGkAtoms::input)) {
+ // The node is a <input type="range">
+ el = static_cast<HTMLInputElement*>(node);
+ } else if (node->GetParentNode() &&
+ node->GetParentNode()->IsHTMLElement(nsGkAtoms::input)) {
+ // The node is a child of <input type="range">
+ el = static_cast<HTMLInputElement*>(node->GetParentNode());
+ }
+ }
+ if (el && el->IsDraggingRange()) {
+ aVisitor.mCanHandle = false;
+ return;
+ }
+ nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+ return;
+ }
+ default:
+ nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+ return;
+ }
+}
+
+bool HTMLMediaElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ // Mappings from 'preload' attribute strings to an enumeration.
+ static const nsAttrValue::EnumTable kPreloadTable[] = {
+ {"", HTMLMediaElement::PRELOAD_ATTR_EMPTY},
+ {"none", HTMLMediaElement::PRELOAD_ATTR_NONE},
+ {"metadata", HTMLMediaElement::PRELOAD_ATTR_METADATA},
+ {"auto", HTMLMediaElement::PRELOAD_ATTR_AUTO},
+ {nullptr, 0}};
+
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::crossorigin) {
+ ParseCORSValue(aValue, aResult);
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::preload) {
+ return aResult.ParseEnumValue(aValue, kPreloadTable, false);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLMediaElement::DoneCreatingElement() {
+ if (HasAttr(nsGkAtoms::muted)) {
+ mMuted |= MUTED_BY_CONTENT;
+ }
+}
+
+bool HTMLMediaElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ if (nsGenericHTMLElement::IsHTMLFocusable(aWithMouse, aIsFocusable,
+ aTabIndex)) {
+ return true;
+ }
+
+ *aIsFocusable = true;
+ return false;
+}
+
+int32_t HTMLMediaElement::TabIndexDefault() { return 0; }
+
+void HTMLMediaElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::src) {
+ mSrcMediaSource = nullptr;
+ mSrcAttrTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue ? aValue->GetStringValue() : EmptyString(),
+ aMaybeScriptedPrincipal);
+ if (aValue) {
+ nsString srcStr = aValue->GetStringValue();
+ nsCOMPtr<nsIURI> uri;
+ NewURIFromString(srcStr, getter_AddRefs(uri));
+ if (uri && IsMediaSourceURI(uri)) {
+ nsresult rv = NS_GetSourceForMediaSourceURI(
+ uri, getter_AddRefs(mSrcMediaSource));
+ if (NS_FAILED(rv)) {
+ nsAutoString spec;
+ GetCurrentSrc(spec);
+ AutoTArray<nsString, 1> params = {spec};
+ ReportLoadError("MediaLoadInvalidURI", params);
+ }
+ }
+ }
+ } else if (aName == nsGkAtoms::autoplay) {
+ if (aNotify) {
+ if (aValue) {
+ StopSuspendingAfterFirstFrame();
+ CheckAutoplayDataReady();
+ }
+ // This attribute can affect AddRemoveSelfReference
+ AddRemoveSelfReference();
+ UpdatePreloadAction();
+ }
+ } else if (aName == nsGkAtoms::preload) {
+ UpdatePreloadAction();
+ } else if (aName == nsGkAtoms::loop) {
+ if (mDecoder) {
+ mDecoder->SetLooping(!!aValue);
+ }
+ } else if (aName == nsGkAtoms::controls && IsInComposedDoc()) {
+ NotifyUAWidgetSetupOrChange();
+ }
+ }
+
+ // Since AfterMaybeChangeAttr may call DoLoad, make sure that it is called
+ // *after* any possible changes to mSrcMediaSource.
+ if (aValue) {
+ AfterMaybeChangeAttr(aNameSpaceID, aName, aNotify);
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void HTMLMediaElement::OnAttrSetButNotChanged(int32_t aNamespaceID,
+ nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+
+ return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName,
+ aValue, aNotify);
+}
+
+void HTMLMediaElement::AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName,
+ bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::src) {
+ DoLoad();
+ }
+ }
+}
+
+nsresult HTMLMediaElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+
+ if (IsInComposedDoc()) {
+ // Construct Shadow Root so web content can be hidden in the DOM.
+ AttachAndSetUAShadowRoot();
+
+ // The preload action depends on the value of the autoplay attribute.
+ // It's value may have changed, so update it.
+ UpdatePreloadAction();
+ }
+
+ NotifyDecoderActivityChanges();
+ mMediaControlKeyListener->UpdateOwnerBrowsingContextIfNeeded();
+ return rv;
+}
+
+void HTMLMediaElement::UnbindFromTree(bool aNullParent) {
+ mVisibilityState = Visibility::Untracked;
+
+ if (IsInComposedDoc()) {
+ NotifyUAWidgetTeardown();
+ }
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+
+ MOZ_ASSERT(IsActuallyInvisible());
+ NotifyDecoderActivityChanges();
+
+ // https://html.spec.whatwg.org/#playing-the-media-resource:remove-an-element-from-a-document
+ //
+ // Dispatch a task to run once we're in a stable state which ensures we're
+ // paused if we're no longer in a document. Note that we need to dispatch this
+ // even if there are other tasks in flight for this because these can be
+ // cancelled if there's a new load.
+ //
+ // FIXME(emilio): Per that spec section, we should only do this if we used to
+ // be connected, though other browsers match our current behavior...
+ //
+ // Also, https://github.com/whatwg/html/issues/4928
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction("dom::HTMLMediaElement::UnbindFromTree",
+ [self = RefPtr<HTMLMediaElement>(this)]() {
+ if (!self->IsInComposedDoc()) {
+ self->PauseInternal();
+ self->mMediaControlKeyListener->StopIfNeeded();
+ }
+ });
+ RunInStableState(task);
+}
+
+/* static */
+CanPlayStatus HTMLMediaElement::GetCanPlay(
+ const nsAString& aType, DecoderDoctorDiagnostics* aDiagnostics) {
+ Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType);
+ if (!containerType) {
+ return CANPLAY_NO;
+ }
+ CanPlayStatus status =
+ DecoderTraits::CanHandleContainerType(*containerType, aDiagnostics);
+ if (status == CANPLAY_YES &&
+ (*containerType).ExtendedType().Codecs().IsEmpty()) {
+ // Per spec: 'Generally, a user agent should never return "probably" for a
+ // type that allows the `codecs` parameter if that parameter is not
+ // present.' As all our currently-supported types allow for `codecs`, we can
+ // do this check here.
+ // TODO: Instead, missing `codecs` should be checked in each decoder's
+ // `IsSupportedType` call from `CanHandleCodecsType()`.
+ // See bug 1399023.
+ return CANPLAY_MAYBE;
+ }
+ return status;
+}
+
+void HTMLMediaElement::CanPlayType(const nsAString& aType, nsAString& aResult) {
+ DecoderDoctorDiagnostics diagnostics;
+ CanPlayStatus canPlay = GetCanPlay(aType, &diagnostics);
+ diagnostics.StoreFormatDiagnostics(OwnerDoc(), aType, canPlay != CANPLAY_NO,
+ __func__);
+ switch (canPlay) {
+ case CANPLAY_NO:
+ aResult.Truncate();
+ break;
+ case CANPLAY_YES:
+ aResult.AssignLiteral("probably");
+ break;
+ case CANPLAY_MAYBE:
+ aResult.AssignLiteral("maybe");
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected case.");
+ break;
+ }
+
+ LOG(LogLevel::Debug,
+ ("%p CanPlayType(%s) = \"%s\"", this, NS_ConvertUTF16toUTF8(aType).get(),
+ NS_ConvertUTF16toUTF8(aResult).get()));
+}
+
+void HTMLMediaElement::AssertReadyStateIsNothing() {
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ if (mReadyState != HAVE_NOTHING) {
+ char buf[1024];
+ SprintfLiteral(buf,
+ "readyState=%d networkState=%d mLoadWaitStatus=%d "
+ "mSourceLoadCandidate=%d "
+ "mIsLoadingFromSourceChildren=%d mPreloadAction=%d "
+ "mSuspendedForPreloadNone=%d error=%d",
+ int(mReadyState), int(mNetworkState), int(mLoadWaitStatus),
+ !!mSourceLoadCandidate, mIsLoadingFromSourceChildren,
+ int(mPreloadAction), mSuspendedForPreloadNone,
+ GetError() ? GetError()->Code() : 0);
+ MOZ_CRASH_UNSAFE_PRINTF("ReadyState should be HAVE_NOTHING! %s", buf);
+ }
+#endif
+}
+
+nsresult HTMLMediaElement::InitializeDecoderAsClone(
+ ChannelMediaDecoder* aOriginal) {
+ NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
+ NS_ASSERTION(mDecoder == nullptr, "Shouldn't have a decoder");
+ AssertReadyStateIsNothing();
+
+ MediaDecoderInit decoderInit(
+ this, this, mMuted ? 0.0 : mVolume, mPreservesPitch,
+ ClampPlaybackRate(mPlaybackRate),
+ mPreloadAction == HTMLMediaElement::PRELOAD_METADATA, mHasSuspendTaint,
+ HasAttr(nsGkAtoms::loop), aOriginal->ContainerType());
+
+ RefPtr<ChannelMediaDecoder> decoder = aOriginal->Clone(decoderInit);
+ if (!decoder) return NS_ERROR_FAILURE;
+
+ LOG(LogLevel::Debug,
+ ("%p Cloned decoder %p from %p", this, decoder.get(), aOriginal));
+
+ return FinishDecoderSetup(decoder);
+}
+
+template <typename DecoderType, typename... LoadArgs>
+nsresult HTMLMediaElement::SetupDecoder(DecoderType* aDecoder,
+ LoadArgs&&... aArgs) {
+ LOG(LogLevel::Debug, ("%p Created decoder %p for type %s", this, aDecoder,
+ aDecoder->ContainerType().OriginalString().Data()));
+
+ nsresult rv = aDecoder->Load(std::forward<LoadArgs>(aArgs)...);
+ if (NS_FAILED(rv)) {
+ aDecoder->Shutdown();
+ LOG(LogLevel::Debug, ("%p Failed to load for decoder %p", this, aDecoder));
+ return rv;
+ }
+
+ rv = FinishDecoderSetup(aDecoder);
+ // Only ChannelMediaDecoder supports resource cloning.
+ if (std::is_same_v<DecoderType, ChannelMediaDecoder> && NS_SUCCEEDED(rv)) {
+ AddMediaElementToURITable();
+ NS_ASSERTION(
+ MediaElementTableCount(this, mLoadingSrc) == 1,
+ "Media element should have single table entry if decode initialized");
+ }
+
+ return rv;
+}
+
+nsresult HTMLMediaElement::InitializeDecoderForChannel(
+ nsIChannel* aChannel, nsIStreamListener** aListener) {
+ NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set");
+ AssertReadyStateIsNothing();
+
+ DecoderDoctorDiagnostics diagnostics;
+
+ nsAutoCString mimeType;
+ aChannel->GetContentType(mimeType);
+ NS_ASSERTION(!mimeType.IsEmpty(), "We should have the Content-Type.");
+ NS_ConvertUTF8toUTF16 mimeUTF16(mimeType);
+
+ RefPtr<HTMLMediaElement> self = this;
+ auto reportCanPlay = [&, self](bool aCanPlay) {
+ diagnostics.StoreFormatDiagnostics(self->OwnerDoc(), mimeUTF16, aCanPlay,
+ __func__);
+ if (!aCanPlay) {
+ nsAutoString src;
+ self->GetCurrentSrc(src);
+ AutoTArray<nsString, 2> params = {mimeUTF16, src};
+ self->ReportLoadError("MediaLoadUnsupportedMimeType", params);
+ }
+ };
+
+ auto onExit = MakeScopeExit([self] {
+ if (self->mChannelLoader) {
+ self->mChannelLoader->Done();
+ self->mChannelLoader = nullptr;
+ }
+ });
+
+ Maybe<MediaContainerType> containerType = MakeMediaContainerType(mimeType);
+ if (!containerType) {
+ reportCanPlay(false);
+ return NS_ERROR_FAILURE;
+ }
+
+ MediaDecoderInit decoderInit(
+ this, this, mMuted ? 0.0 : mVolume, mPreservesPitch,
+ ClampPlaybackRate(mPlaybackRate),
+ mPreloadAction == HTMLMediaElement::PRELOAD_METADATA, mHasSuspendTaint,
+ HasAttr(nsGkAtoms::loop), *containerType);
+
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+ if (HLSDecoder::IsSupportedType(*containerType)) {
+ RefPtr<HLSDecoder> decoder = HLSDecoder::Create(decoderInit);
+ if (!decoder) {
+ reportCanPlay(false);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ reportCanPlay(true);
+ return SetupDecoder(decoder.get(), aChannel);
+ }
+#endif
+
+ RefPtr<ChannelMediaDecoder> decoder =
+ ChannelMediaDecoder::Create(decoderInit, &diagnostics);
+ if (!decoder) {
+ reportCanPlay(false);
+ return NS_ERROR_FAILURE;
+ }
+
+ reportCanPlay(true);
+ bool isPrivateBrowsing = NodePrincipal()->GetPrivateBrowsingId() > 0;
+ return SetupDecoder(decoder.get(), aChannel, isPrivateBrowsing, aListener);
+}
+
+nsresult HTMLMediaElement::FinishDecoderSetup(MediaDecoder* aDecoder) {
+ ChangeNetworkState(NETWORK_LOADING);
+
+ // Set mDecoder now so if methods like GetCurrentSrc get called between
+ // here and Load(), they work.
+ SetDecoder(aDecoder);
+
+ // Notify the decoder of the initial activity status.
+ NotifyDecoderActivityChanges();
+
+ // Update decoder principal before we start decoding, since it
+ // can affect how we feed data to MediaStreams
+ NotifyDecoderPrincipalChanged();
+
+ // Set sink device if we have one. Otherwise the default is used.
+ if (mSink.second) {
+ mDecoder->SetSink(mSink.second);
+ }
+
+ if (mMediaKeys) {
+ if (mMediaKeys->GetCDMProxy()) {
+ mDecoder->SetCDMProxy(mMediaKeys->GetCDMProxy());
+ } else {
+ // CDM must have crashed.
+ ShutdownDecoder();
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (mChannelLoader) {
+ mChannelLoader->Done();
+ mChannelLoader = nullptr;
+ }
+
+ // We may want to suspend the new stream now.
+ // This will also do an AddRemoveSelfReference.
+ NotifyOwnerDocumentActivityChanged();
+
+ if (!mDecoder) {
+ // NotifyOwnerDocumentActivityChanged may shutdown the decoder if the
+ // owning document is inactive and we're in the EME case. We could try and
+ // handle this, but at the time of writing it's a pretty niche case, so just
+ // bail.
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mSuspendedByInactiveDocOrDocshell) {
+ mDecoder->Suspend();
+ }
+
+ if (!mPaused) {
+ SetPlayedOrSeeked(true);
+ if (!mSuspendedByInactiveDocOrDocshell) {
+ mDecoder->Play();
+ }
+ }
+
+ MaybeBeginCloningVisually();
+
+ return NS_OK;
+}
+
+void HTMLMediaElement::UpdateSrcMediaStreamPlaying(uint32_t aFlags) {
+ if (!mSrcStream) {
+ return;
+ }
+
+ bool shouldPlay = !(aFlags & REMOVING_SRC_STREAM) && !mPaused &&
+ !mSuspendedByInactiveDocOrDocshell;
+ if (shouldPlay == mSrcStreamIsPlaying) {
+ return;
+ }
+ mSrcStreamIsPlaying = shouldPlay;
+
+ LOG(LogLevel::Debug,
+ ("MediaElement %p %s playback of DOMMediaStream %p", this,
+ shouldPlay ? "Setting up" : "Removing", mSrcStream.get()));
+
+ if (shouldPlay) {
+ mSrcStreamPlaybackEnded = false;
+ mSrcStreamReportPlaybackEnded = false;
+
+ if (mMediaStreamRenderer) {
+ mMediaStreamRenderer->Start();
+ }
+ if (mSecondaryMediaStreamRenderer) {
+ mSecondaryMediaStreamRenderer->Start();
+ }
+
+ SetCapturedOutputStreamsEnabled(true); // Unmute
+ // If the input is a media stream, we don't check its data and always regard
+ // it as audible when it's playing.
+ SetAudibleState(true);
+ } else {
+ if (mMediaStreamRenderer) {
+ mMediaStreamRenderer->Stop();
+ }
+ if (mSecondaryMediaStreamRenderer) {
+ mSecondaryMediaStreamRenderer->Stop();
+ }
+ SetCapturedOutputStreamsEnabled(false); // Mute
+ }
+}
+
+void HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying() {
+ if (!mMediaStreamRenderer) {
+ // Notifications are async, the renderer could have been cleared.
+ return;
+ }
+
+ mMediaStreamRenderer->SetProgressingCurrentTime(IsPotentiallyPlaying());
+}
+
+void HTMLMediaElement::UpdateSrcStreamTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mSrcStreamPlaybackEnded) {
+ // We do a separate FireTimeUpdate() when this is set.
+ return;
+ }
+
+ FireTimeUpdate(TimeupdateType::ePeriodic);
+}
+
+void HTMLMediaElement::SetupSrcMediaStreamPlayback(DOMMediaStream* aStream) {
+ NS_ASSERTION(!mSrcStream, "Should have been ended already");
+
+ mLoadingSrc = nullptr;
+ mSrcStream = aStream;
+
+ VideoFrameContainer* container = GetVideoFrameContainer();
+ RefPtr<FirstFrameVideoOutput> firstFrameOutput =
+ container ? MakeAndAddRef<FirstFrameVideoOutput>(container,
+ AbstractMainThread())
+ : nullptr;
+ mMediaStreamRenderer = MakeAndAddRef<MediaStreamRenderer>(
+ AbstractMainThread(), container, firstFrameOutput, this);
+ mWatchManager.Watch(mPaused,
+ &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying);
+ mWatchManager.Watch(mReadyState,
+ &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying);
+ mWatchManager.Watch(mSrcStreamPlaybackEnded,
+ &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying);
+ mWatchManager.Watch(mSrcStreamPlaybackEnded,
+ &HTMLMediaElement::UpdateSrcStreamReportPlaybackEnded);
+ mWatchManager.Watch(mMediaStreamRenderer->CurrentGraphTime(),
+ &HTMLMediaElement::UpdateSrcStreamTime);
+ SetVolumeInternal();
+ if (mSink.second) {
+ mMediaStreamRenderer->SetAudioOutputDevice(mSink.second);
+ }
+
+ UpdateSrcMediaStreamPlaying();
+ UpdateSrcStreamPotentiallyPlaying();
+ mSrcStreamVideoPrincipal = NodePrincipal();
+
+ // If we pause this media element, track changes in the underlying stream
+ // will continue to fire events at this element and alter its track list.
+ // That's simpler than delaying the events, but probably confusing...
+ nsTArray<RefPtr<MediaStreamTrack>> tracks;
+ mSrcStream->GetTracks(tracks);
+ for (const RefPtr<MediaStreamTrack>& track : tracks) {
+ NotifyMediaStreamTrackAdded(track);
+ }
+
+ mMediaStreamTrackListener = MakeUnique<MediaStreamTrackListener>(this);
+ mSrcStream->RegisterTrackListener(mMediaStreamTrackListener.get());
+
+ ChangeNetworkState(NETWORK_IDLE);
+ ChangeDelayLoadStatus(false);
+
+ // FirstFrameLoaded() will be called when the stream has tracks.
+}
+
+void HTMLMediaElement::EndSrcMediaStreamPlayback() {
+ MOZ_ASSERT(mSrcStream);
+
+ UpdateSrcMediaStreamPlaying(REMOVING_SRC_STREAM);
+
+ if (mSelectedVideoStreamTrack) {
+ mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(this);
+ }
+ mSelectedVideoStreamTrack = nullptr;
+
+ MOZ_ASSERT_IF(mSecondaryMediaStreamRenderer,
+ !mMediaStreamRenderer == !mSecondaryMediaStreamRenderer);
+ if (mMediaStreamRenderer) {
+ mWatchManager.Unwatch(mPaused,
+ &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying);
+ mWatchManager.Unwatch(mReadyState,
+ &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying);
+ mWatchManager.Unwatch(mSrcStreamPlaybackEnded,
+ &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying);
+ mWatchManager.Unwatch(
+ mSrcStreamPlaybackEnded,
+ &HTMLMediaElement::UpdateSrcStreamReportPlaybackEnded);
+ mWatchManager.Unwatch(mMediaStreamRenderer->CurrentGraphTime(),
+ &HTMLMediaElement::UpdateSrcStreamTime);
+ mMediaStreamRenderer->Shutdown();
+ mMediaStreamRenderer = nullptr;
+ }
+ if (mSecondaryMediaStreamRenderer) {
+ mSecondaryMediaStreamRenderer->Shutdown();
+ mSecondaryMediaStreamRenderer = nullptr;
+ }
+
+ mSrcStream->UnregisterTrackListener(mMediaStreamTrackListener.get());
+ mMediaStreamTrackListener = nullptr;
+ mSrcStreamPlaybackEnded = false;
+ mSrcStreamReportPlaybackEnded = false;
+ mSrcStreamVideoPrincipal = nullptr;
+
+ mSrcStream = nullptr;
+}
+
+static already_AddRefed<AudioTrack> CreateAudioTrack(
+ AudioStreamTrack* aStreamTrack, nsIGlobalObject* aOwnerGlobal) {
+ nsAutoString id;
+ nsAutoString label;
+ aStreamTrack->GetId(id);
+ aStreamTrack->GetLabel(label, CallerType::System);
+
+ return MediaTrackList::CreateAudioTrack(aOwnerGlobal, id, u"main"_ns, label,
+ u""_ns, true, aStreamTrack);
+}
+
+static already_AddRefed<VideoTrack> CreateVideoTrack(
+ VideoStreamTrack* aStreamTrack, nsIGlobalObject* aOwnerGlobal) {
+ nsAutoString id;
+ nsAutoString label;
+ aStreamTrack->GetId(id);
+ aStreamTrack->GetLabel(label, CallerType::System);
+
+ return MediaTrackList::CreateVideoTrack(aOwnerGlobal, id, u"main"_ns, label,
+ u""_ns, aStreamTrack);
+}
+
+void HTMLMediaElement::NotifyMediaStreamTrackAdded(
+ const RefPtr<MediaStreamTrack>& aTrack) {
+ MOZ_ASSERT(aTrack);
+
+ if (aTrack->Ended()) {
+ return;
+ }
+
+#ifdef DEBUG
+ nsAutoString id;
+ aTrack->GetId(id);
+
+ LOG(LogLevel::Debug, ("%p, Adding %sTrack with id %s", this,
+ aTrack->AsAudioStreamTrack() ? "Audio" : "Video",
+ NS_ConvertUTF16toUTF8(id).get()));
+#endif
+
+ if (AudioStreamTrack* t = aTrack->AsAudioStreamTrack()) {
+ MOZ_DIAGNOSTIC_ASSERT(AudioTracks(), "Element can't have been unlinked");
+ RefPtr<AudioTrack> audioTrack =
+ CreateAudioTrack(t, AudioTracks()->GetOwnerGlobal());
+ AudioTracks()->AddTrack(audioTrack);
+ } else if (VideoStreamTrack* t = aTrack->AsVideoStreamTrack()) {
+ // TODO: Fix this per the spec on bug 1273443.
+ if (!IsVideo()) {
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(VideoTracks(), "Element can't have been unlinked");
+ RefPtr<VideoTrack> videoTrack =
+ CreateVideoTrack(t, VideoTracks()->GetOwnerGlobal());
+ VideoTracks()->AddTrack(videoTrack);
+ // New MediaStreamTrack added, set the new added video track as selected
+ // video track when there is no selected track.
+ if (VideoTracks()->SelectedIndex() == -1) {
+ MOZ_ASSERT(!mSelectedVideoStreamTrack);
+ videoTrack->SetEnabledInternal(true, dom::MediaTrack::FIRE_NO_EVENTS);
+ }
+ }
+
+ // The set of enabled AudioTracks and selected video track might have changed.
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal);
+ AbstractThread::DispatchDirectTask(
+ NewRunnableMethod("HTMLMediaElement::FirstFrameLoaded", this,
+ &HTMLMediaElement::FirstFrameLoaded));
+}
+
+void HTMLMediaElement::NotifyMediaStreamTrackRemoved(
+ const RefPtr<MediaStreamTrack>& aTrack) {
+ MOZ_ASSERT(aTrack);
+
+ nsAutoString id;
+ aTrack->GetId(id);
+
+ LOG(LogLevel::Debug, ("%p, Removing %sTrack with id %s", this,
+ aTrack->AsAudioStreamTrack() ? "Audio" : "Video",
+ NS_ConvertUTF16toUTF8(id).get()));
+
+ MOZ_DIAGNOSTIC_ASSERT(AudioTracks() && VideoTracks(),
+ "Element can't have been unlinked");
+ if (dom::MediaTrack* t = AudioTracks()->GetTrackById(id)) {
+ AudioTracks()->RemoveTrack(t);
+ } else if (dom::MediaTrack* t = VideoTracks()->GetTrackById(id)) {
+ VideoTracks()->RemoveTrack(t);
+ } else {
+ NS_ASSERTION(aTrack->AsVideoStreamTrack() && !IsVideo(),
+ "MediaStreamTrack ended but did not exist in track lists. "
+ "This is only allowed if a video element ends and we are an "
+ "audio element.");
+ return;
+ }
+}
+
+void HTMLMediaElement::ProcessMediaFragmentURI() {
+ if (!mLoadingSrc) {
+ mFragmentStart = mFragmentEnd = -1.0;
+ return;
+ }
+ nsMediaFragmentURIParser parser(mLoadingSrc);
+
+ if (mDecoder && parser.HasEndTime()) {
+ mFragmentEnd = parser.GetEndTime();
+ }
+
+ if (parser.HasStartTime()) {
+ SetCurrentTime(parser.GetStartTime());
+ mFragmentStart = parser.GetStartTime();
+ }
+}
+
+void HTMLMediaElement::MetadataLoaded(const MediaInfo* aInfo,
+ UniquePtr<const MetadataTags> aTags) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mDecoder) {
+ ConstructMediaTracks(aInfo);
+ }
+
+ SetMediaInfo(*aInfo);
+
+ mIsEncrypted =
+ aInfo->IsEncrypted() || mPendingEncryptedInitData.IsEncrypted();
+ mTags = std::move(aTags);
+ mLoadedDataFired = false;
+ ChangeReadyState(HAVE_METADATA);
+
+ // Add output tracks synchronously now to be sure they're available in
+ // "loadedmetadata" event handlers.
+ UpdateOutputTrackSources();
+
+ DispatchAsyncEvent(u"durationchange"_ns);
+ if (IsVideo() && HasVideo()) {
+ DispatchAsyncEvent(u"resize"_ns);
+ Invalidate(ImageSizeChanged::No, Some(mMediaInfo.mVideo.mDisplay),
+ ForceInvalidate::No);
+ }
+ NS_ASSERTION(!HasVideo() || (mMediaInfo.mVideo.mDisplay.width > 0 &&
+ mMediaInfo.mVideo.mDisplay.height > 0),
+ "Video resolution must be known on 'loadedmetadata'");
+ DispatchAsyncEvent(u"loadedmetadata"_ns);
+
+ if (mDecoder && mDecoder->IsTransportSeekable() &&
+ mDecoder->IsMediaSeekable()) {
+ ProcessMediaFragmentURI();
+ mDecoder->SetFragmentEndTime(mFragmentEnd);
+ }
+ if (mIsEncrypted) {
+ // We only support playback of encrypted content via MSE by default.
+ if (!mMediaSource && Preferences::GetBool("media.eme.mse-only", true)) {
+ DecodeError(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "Encrypted content not supported outside of MSE"));
+ return;
+ }
+
+ // Dispatch a distinct 'encrypted' event for each initData we have.
+ for (const auto& initData : mPendingEncryptedInitData.mInitDatas) {
+ DispatchEncrypted(initData.mInitData, initData.mType);
+ }
+ mPendingEncryptedInitData.Reset();
+ }
+
+ if (IsVideo() && aInfo->HasVideo()) {
+ // We are a video element playing video so update the screen wakelock
+ NotifyOwnerDocumentActivityChanged();
+ }
+
+ if (mDefaultPlaybackStartPosition != 0.0) {
+ SetCurrentTime(mDefaultPlaybackStartPosition);
+ mDefaultPlaybackStartPosition = 0.0;
+ }
+
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal);
+}
+
+void HTMLMediaElement::FirstFrameLoaded() {
+ LOG(LogLevel::Debug,
+ ("%p, FirstFrameLoaded() mFirstFrameLoaded=%d mWaitingForKey=%d", this,
+ mFirstFrameLoaded.Ref(), mWaitingForKey));
+
+ NS_ASSERTION(!mSuspendedAfterFirstFrame, "Should not have already suspended");
+
+ if (!mFirstFrameLoaded) {
+ mFirstFrameLoaded = true;
+ }
+
+ ChangeDelayLoadStatus(false);
+
+ if (mDecoder && mAllowSuspendAfterFirstFrame && mPaused &&
+ !HasAttr(nsGkAtoms::autoplay) &&
+ mPreloadAction == HTMLMediaElement::PRELOAD_METADATA) {
+ mSuspendedAfterFirstFrame = true;
+ mDecoder->Suspend();
+ }
+}
+
+void HTMLMediaElement::NetworkError(const MediaResult& aError) {
+ if (mReadyState == HAVE_NOTHING) {
+ NoSupportedMediaSourceError(aError.Description());
+ } else {
+ Error(MEDIA_ERR_NETWORK);
+ }
+}
+
+void HTMLMediaElement::DecodeError(const MediaResult& aError) {
+ nsAutoString src;
+ GetCurrentSrc(src);
+ AutoTArray<nsString, 1> params = {src};
+ ReportLoadError("MediaLoadDecodeError", params);
+
+ DecoderDoctorDiagnostics diagnostics;
+ diagnostics.StoreDecodeError(OwnerDoc(), aError, src, __func__);
+
+ if (mIsLoadingFromSourceChildren) {
+ mErrorSink->ResetError();
+ if (mSourceLoadCandidate) {
+ DispatchAsyncSourceError(mSourceLoadCandidate);
+ QueueLoadFromSourceTask();
+ } else {
+ NS_WARNING("Should know the source we were loading from!");
+ }
+ } else if (mReadyState == HAVE_NOTHING) {
+ NoSupportedMediaSourceError(aError.Description());
+ } else if (IsCORSSameOrigin()) {
+ Error(MEDIA_ERR_DECODE, aError.Description());
+ } else {
+ Error(MEDIA_ERR_DECODE, "Failed to decode media"_ns);
+ }
+}
+
+void HTMLMediaElement::DecodeWarning(const MediaResult& aError) {
+ nsAutoString src;
+ GetCurrentSrc(src);
+ DecoderDoctorDiagnostics diagnostics;
+ diagnostics.StoreDecodeWarning(OwnerDoc(), aError, src, __func__);
+}
+
+bool HTMLMediaElement::HasError() const { return GetError(); }
+
+void HTMLMediaElement::LoadAborted() { Error(MEDIA_ERR_ABORTED); }
+
+void HTMLMediaElement::Error(uint16_t aErrorCode,
+ const nsACString& aErrorDetails) {
+ mErrorSink->SetError(aErrorCode, aErrorDetails);
+ ChangeDelayLoadStatus(false);
+ UpdateAudioChannelPlayingState();
+}
+
+void HTMLMediaElement::PlaybackEnded() {
+ // We changed state which can affect AddRemoveSelfReference
+ AddRemoveSelfReference();
+
+ NS_ASSERTION(!mDecoder || mDecoder->IsEnded(),
+ "Decoder fired ended, but not in ended state");
+
+ // IsPlaybackEnded() became true.
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
+
+ if (mSrcStream) {
+ LOG(LogLevel::Debug,
+ ("%p, got duration by reaching the end of the resource", this));
+ mSrcStreamPlaybackEnded = true;
+ DispatchAsyncEvent(u"durationchange"_ns);
+ } else {
+ // mediacapture-main:
+ // Setting the loop attribute has no effect since a MediaStream has no
+ // defined end and therefore cannot be looped.
+ if (HasAttr(nsGkAtoms::loop)) {
+ SetCurrentTime(0);
+ return;
+ }
+ }
+
+ FireTimeUpdate(TimeupdateType::eMandatory);
+
+ if (!mPaused) {
+ Pause();
+ }
+
+ if (mSrcStream) {
+ // A MediaStream that goes from inactive to active shall be eligible for
+ // autoplay again according to the mediacapture-main spec.
+ mCanAutoplayFlag = true;
+ }
+
+ if (StaticPrefs::media_mediacontrol_stopcontrol_aftermediaends()) {
+ mMediaControlKeyListener->StopIfNeeded();
+ }
+ DispatchAsyncEvent(u"ended"_ns);
+}
+
+void HTMLMediaElement::UpdateSrcStreamReportPlaybackEnded() {
+ mSrcStreamReportPlaybackEnded = mSrcStreamPlaybackEnded;
+}
+
+void HTMLMediaElement::SeekStarted() { DispatchAsyncEvent(u"seeking"_ns); }
+
+void HTMLMediaElement::SeekCompleted() {
+ mPlayingBeforeSeek = false;
+ SetPlayedOrSeeked(true);
+ if (mTextTrackManager) {
+ mTextTrackManager->DidSeek();
+ }
+ // https://html.spec.whatwg.org/multipage/media.html#seeking:dom-media-seek
+ // (Step 16)
+ // TODO (bug 1688131): run these steps in a stable state.
+ FireTimeUpdate(TimeupdateType::eMandatory);
+ DispatchAsyncEvent(u"seeked"_ns);
+ // We changed whether we're seeking so we need to AddRemoveSelfReference
+ AddRemoveSelfReference();
+ if (mCurrentPlayRangeStart == -1.0) {
+ mCurrentPlayRangeStart = CurrentTime();
+ }
+
+ if (mSeekDOMPromise) {
+ AbstractMainThread()->Dispatch(NS_NewRunnableFunction(
+ __func__, [promise = std::move(mSeekDOMPromise)] {
+ promise->MaybeResolveWithUndefined();
+ }));
+ }
+ MOZ_ASSERT(!mSeekDOMPromise);
+}
+
+void HTMLMediaElement::SeekAborted() {
+ if (mSeekDOMPromise) {
+ AbstractMainThread()->Dispatch(NS_NewRunnableFunction(
+ __func__, [promise = std::move(mSeekDOMPromise)] {
+ promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ }));
+ }
+ MOZ_ASSERT(!mSeekDOMPromise);
+}
+
+void HTMLMediaElement::NotifySuspendedByCache(bool aSuspendedByCache) {
+ LOG(LogLevel::Debug,
+ ("%p, mDownloadSuspendedByCache=%d", this, aSuspendedByCache));
+ mDownloadSuspendedByCache = aSuspendedByCache;
+}
+
+void HTMLMediaElement::DownloadSuspended() {
+ if (mNetworkState == NETWORK_LOADING) {
+ DispatchAsyncEvent(u"progress"_ns);
+ }
+ ChangeNetworkState(NETWORK_IDLE);
+}
+
+void HTMLMediaElement::DownloadResumed() {
+ ChangeNetworkState(NETWORK_LOADING);
+}
+
+void HTMLMediaElement::CheckProgress(bool aHaveNewProgress) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mNetworkState == NETWORK_LOADING);
+
+ TimeStamp now = TimeStamp::NowLoRes();
+
+ if (aHaveNewProgress) {
+ mDataTime = now;
+ }
+
+ // If this is the first progress, or PROGRESS_MS has passed since the last
+ // progress event fired and more data has arrived since then, fire a
+ // progress event.
+ NS_ASSERTION(
+ (mProgressTime.IsNull() && !aHaveNewProgress) || !mDataTime.IsNull(),
+ "null TimeStamp mDataTime should not be used in comparison");
+ if (mProgressTime.IsNull()
+ ? aHaveNewProgress
+ : (now - mProgressTime >=
+ TimeDuration::FromMilliseconds(PROGRESS_MS) &&
+ mDataTime > mProgressTime)) {
+ DispatchAsyncEvent(u"progress"_ns);
+ // Resolution() ensures that future data will have now > mProgressTime,
+ // and so will trigger another event. mDataTime is not reset because it
+ // is still required to detect stalled; it is similarly offset by
+ // resolution to indicate the new data has not yet arrived.
+ mProgressTime = now - TimeDuration::Resolution();
+ if (mDataTime > mProgressTime) {
+ mDataTime = mProgressTime;
+ }
+ if (!mProgressTimer) {
+ NS_ASSERTION(aHaveNewProgress,
+ "timer dispatched when there was no timer");
+ // Were stalled. Restart timer.
+ StartProgressTimer();
+ if (!mLoadedDataFired) {
+ ChangeDelayLoadStatus(true);
+ }
+ }
+ // Download statistics may have been updated, force a recheck of the
+ // readyState.
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal);
+ }
+
+ if (now - mDataTime >= TimeDuration::FromMilliseconds(STALL_MS)) {
+ if (!mMediaSource) {
+ DispatchAsyncEvent(u"stalled"_ns);
+ } else {
+ ChangeDelayLoadStatus(false);
+ }
+
+ NS_ASSERTION(mProgressTimer, "detected stalled without timer");
+ // Stop timer events, which prevents repeated stalled events until there
+ // is more progress.
+ StopProgress();
+ }
+
+ AddRemoveSelfReference();
+}
+
+/* static */
+void HTMLMediaElement::ProgressTimerCallback(nsITimer* aTimer, void* aClosure) {
+ auto* decoder = static_cast<HTMLMediaElement*>(aClosure);
+ decoder->CheckProgress(false);
+}
+
+void HTMLMediaElement::StartProgressTimer() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mNetworkState == NETWORK_LOADING);
+ NS_ASSERTION(!mProgressTimer, "Already started progress timer.");
+
+ NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mProgressTimer), ProgressTimerCallback, this, PROGRESS_MS,
+ nsITimer::TYPE_REPEATING_SLACK, "HTMLMediaElement::ProgressTimerCallback",
+ GetMainThreadSerialEventTarget());
+}
+
+void HTMLMediaElement::StartProgress() {
+ // Record the time now for detecting stalled.
+ mDataTime = TimeStamp::NowLoRes();
+ // Reset mProgressTime so that mDataTime is not indicating bytes received
+ // after the last progress event.
+ mProgressTime = TimeStamp();
+ StartProgressTimer();
+}
+
+void HTMLMediaElement::StopProgress() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mProgressTimer) {
+ return;
+ }
+
+ mProgressTimer->Cancel();
+ mProgressTimer = nullptr;
+}
+
+void HTMLMediaElement::DownloadProgressed() {
+ if (mNetworkState != NETWORK_LOADING) {
+ return;
+ }
+ CheckProgress(true);
+}
+
+bool HTMLMediaElement::ShouldCheckAllowOrigin() {
+ return mCORSMode != CORS_NONE;
+}
+
+bool HTMLMediaElement::IsCORSSameOrigin() {
+ bool subsumes;
+ RefPtr<nsIPrincipal> principal = GetCurrentPrincipal();
+ return (NS_SUCCEEDED(NodePrincipal()->Subsumes(principal, &subsumes)) &&
+ subsumes) ||
+ ShouldCheckAllowOrigin();
+}
+
+void HTMLMediaElement::UpdateReadyStateInternal() {
+ if (!mDecoder && !mSrcStream) {
+ // Not initialized - bail out.
+ LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() "
+ "Not initialized",
+ this));
+ return;
+ }
+
+ if (mDecoder && mReadyState < HAVE_METADATA) {
+ // aNextFrame might have a next frame because the decoder can advance
+ // on its own thread before MetadataLoaded gets a chance to run.
+ // The arrival of more data can't change us out of this readyState.
+ LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() "
+ "Decoder ready state < HAVE_METADATA",
+ this));
+ return;
+ }
+
+ if (mDecoder) {
+ // IsPlaybackEnded() might have become false.
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources);
+ }
+
+ if (mSrcStream && mReadyState < HAVE_METADATA) {
+ bool hasAudioTracks = AudioTracks() && !AudioTracks()->IsEmpty();
+ bool hasVideoTracks = VideoTracks() && !VideoTracks()->IsEmpty();
+ if (!hasAudioTracks && !hasVideoTracks) {
+ LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() "
+ "Stream with no tracks",
+ this));
+ // Give it one last chance to remove the self reference if needed.
+ AddRemoveSelfReference();
+ return;
+ }
+
+ if (IsVideo() && hasVideoTracks && !HasVideo()) {
+ LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() "
+ "Stream waiting for video",
+ this));
+ return;
+ }
+
+ LOG(LogLevel::Debug,
+ ("MediaElement %p UpdateReadyStateInternal() Stream has "
+ "metadata; audioTracks=%d, videoTracks=%d, "
+ "hasVideoFrame=%d",
+ this, AudioTracks()->Length(), VideoTracks()->Length(), HasVideo()));
+
+ // We are playing a stream that has video and a video frame is now set.
+ // This means we have all metadata needed to change ready state.
+ MediaInfo mediaInfo = mMediaInfo;
+ if (hasAudioTracks) {
+ mediaInfo.EnableAudio();
+ }
+ if (hasVideoTracks) {
+ mediaInfo.EnableVideo();
+ if (mSelectedVideoStreamTrack) {
+ mediaInfo.mVideo.SetAlpha(mSelectedVideoStreamTrack->HasAlpha());
+ }
+ }
+ MetadataLoaded(&mediaInfo, nullptr);
+ }
+
+ if (mMediaSource) {
+ // readyState has changed, assuming it's following the pending mediasource
+ // operations. Notify the Mediasource that the operations have completed.
+ mMediaSource->CompletePendingTransactions();
+ }
+
+ enum NextFrameStatus nextFrameStatus = NextFrameStatus();
+ if (mWaitingForKey == NOT_WAITING_FOR_KEY) {
+ if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE && mDecoder &&
+ !mDecoder->IsEnded()) {
+ nextFrameStatus = mDecoder->NextFrameBufferedStatus();
+ }
+ } else if (mWaitingForKey == WAITING_FOR_KEY) {
+ if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE ||
+ nextFrameStatus == NEXT_FRAME_UNAVAILABLE_BUFFERING) {
+ // http://w3c.github.io/encrypted-media/#wait-for-key
+ // Continuing 7.3.4 Queue a "waitingforkey" Event
+ // 4. Queue a task to fire a simple event named waitingforkey
+ // at the media element.
+ // 5. Set the readyState of media element to HAVE_METADATA.
+ // NOTE: We'll change to HAVE_CURRENT_DATA or HAVE_METADATA
+ // depending on whether we've loaded the first frame or not
+ // below.
+ // 6. Suspend playback.
+ // Note: Playback will already be stalled, as the next frame is
+ // unavailable.
+ mWaitingForKey = WAITING_FOR_KEY_DISPATCHED;
+ DispatchAsyncEvent(u"waitingforkey"_ns);
+ }
+ } else {
+ MOZ_ASSERT(mWaitingForKey == WAITING_FOR_KEY_DISPATCHED);
+ if (nextFrameStatus == NEXT_FRAME_AVAILABLE) {
+ // We have new frames after dispatching "waitingforkey".
+ // This means we've got the key and can reset mWaitingForKey now.
+ mWaitingForKey = NOT_WAITING_FOR_KEY;
+ }
+ }
+
+ if (nextFrameStatus == MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING) {
+ LOG(LogLevel::Debug,
+ ("MediaElement %p UpdateReadyStateInternal() "
+ "NEXT_FRAME_UNAVAILABLE_SEEKING; Forcing HAVE_METADATA",
+ this));
+ ChangeReadyState(HAVE_METADATA);
+ return;
+ }
+
+ if (IsVideo() && VideoTracks() && !VideoTracks()->IsEmpty() &&
+ !IsPlaybackEnded() && GetImageContainer() &&
+ !GetImageContainer()->HasCurrentImage()) {
+ // Don't advance if we are playing video, but don't have a video frame.
+ // Also, if video became available after advancing to HAVE_CURRENT_DATA
+ // while we are still playing, we need to revert to HAVE_METADATA until
+ // a video frame is available.
+ LOG(LogLevel::Debug,
+ ("MediaElement %p UpdateReadyStateInternal() "
+ "Playing video but no video frame; Forcing HAVE_METADATA",
+ this));
+ ChangeReadyState(HAVE_METADATA);
+ return;
+ }
+
+ if (!mFirstFrameLoaded) {
+ // We haven't yet loaded the first frame, making us unable to determine
+ // if we have enough valid data at the present stage.
+ return;
+ }
+
+ if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE_BUFFERING) {
+ // Force HAVE_CURRENT_DATA when buffering.
+ ChangeReadyState(HAVE_CURRENT_DATA);
+ return;
+ }
+
+ // TextTracks must be loaded for the HAVE_ENOUGH_DATA and
+ // HAVE_FUTURE_DATA.
+ // So force HAVE_CURRENT_DATA if text tracks not loaded.
+ if (mTextTrackManager && !mTextTrackManager->IsLoaded()) {
+ ChangeReadyState(HAVE_CURRENT_DATA);
+ return;
+ }
+
+ if (mDownloadSuspendedByCache && mDecoder && !mDecoder->IsEnded()) {
+ // The decoder has signaled that the download has been suspended by the
+ // media cache. So move readyState into HAVE_ENOUGH_DATA, in case there's
+ // script waiting for a "canplaythrough" event; without this forced
+ // transition, we will never fire the "canplaythrough" event if the
+ // media cache is too small, and scripts are bound to fail. Don't force
+ // this transition if the decoder is in ended state; the readyState
+ // should remain at HAVE_CURRENT_DATA in this case.
+ // Note that this state transition includes the case where we finished
+ // downloaded the whole data stream.
+ LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() "
+ "Decoder download suspended by cache",
+ this));
+ ChangeReadyState(HAVE_ENOUGH_DATA);
+ return;
+ }
+
+ if (nextFrameStatus != MediaDecoderOwner::NEXT_FRAME_AVAILABLE) {
+ LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() "
+ "Next frame not available",
+ this));
+ ChangeReadyState(HAVE_CURRENT_DATA);
+ return;
+ }
+
+ if (mSrcStream) {
+ LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() "
+ "Stream HAVE_ENOUGH_DATA",
+ this));
+ ChangeReadyState(HAVE_ENOUGH_DATA);
+ return;
+ }
+
+ // Now see if we should set HAVE_ENOUGH_DATA.
+ // If it's something we don't know the size of, then we can't
+ // make a real estimate, so we go straight to HAVE_ENOUGH_DATA once
+ // we've downloaded enough data that our download rate is considered
+ // reliable. We have to move to HAVE_ENOUGH_DATA at some point or
+ // autoplay elements for live streams will never play. Otherwise we
+ // move to HAVE_ENOUGH_DATA if we can play through the entire media
+ // without stopping to buffer.
+ if (mDecoder->CanPlayThrough()) {
+ LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() "
+ "Decoder can play through",
+ this));
+ ChangeReadyState(HAVE_ENOUGH_DATA);
+ return;
+ }
+ LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() "
+ "Default; Decoder has future data",
+ this));
+ ChangeReadyState(HAVE_FUTURE_DATA);
+}
+
+static const char* const gReadyStateToString[] = {
+ "HAVE_NOTHING", "HAVE_METADATA", "HAVE_CURRENT_DATA", "HAVE_FUTURE_DATA",
+ "HAVE_ENOUGH_DATA"};
+
+void HTMLMediaElement::ChangeReadyState(nsMediaReadyState aState) {
+ if (mReadyState == aState) {
+ return;
+ }
+
+ nsMediaReadyState oldState = mReadyState;
+ mReadyState = aState;
+ LOG(LogLevel::Debug,
+ ("%p Ready state changed to %s", this, gReadyStateToString[aState]));
+
+ DDLOG(DDLogCategory::Property, "ready_state", gReadyStateToString[aState]);
+
+ // https://html.spec.whatwg.org/multipage/media.html#text-track-cue-active-flag
+ // The user agent must synchronously unset cues' active flag whenever the
+ // media element's readyState is changed back to HAVE_NOTHING.
+ if (mReadyState == HAVE_NOTHING && mTextTrackManager) {
+ mTextTrackManager->NotifyReset();
+ }
+
+ if (mNetworkState == NETWORK_EMPTY) {
+ return;
+ }
+
+ UpdateAudioChannelPlayingState();
+
+ // Handle raising of "waiting" event during seek (see 4.8.10.9)
+ // or
+ // 4.8.12.7 Ready states:
+ // "If the previous ready state was HAVE_FUTURE_DATA or more, and the new
+ // ready state is HAVE_CURRENT_DATA or less
+ // If the media element was potentially playing before its readyState
+ // attribute changed to a value lower than HAVE_FUTURE_DATA, and the element
+ // has not ended playback, and playback has not stopped due to errors,
+ // paused for user interaction, or paused for in-band content, the user agent
+ // must queue a task to fire a simple event named timeupdate at the element,
+ // and queue a task to fire a simple event named waiting at the element."
+ if (mPlayingBeforeSeek && mReadyState < HAVE_FUTURE_DATA) {
+ DispatchAsyncEvent(u"waiting"_ns);
+ } else if (oldState >= HAVE_FUTURE_DATA && mReadyState < HAVE_FUTURE_DATA &&
+ !Paused() && !Ended() && !mErrorSink->mError) {
+ FireTimeUpdate(TimeupdateType::eMandatory);
+ DispatchAsyncEvent(u"waiting"_ns);
+ }
+
+ if (oldState < HAVE_CURRENT_DATA && mReadyState >= HAVE_CURRENT_DATA &&
+ !mLoadedDataFired) {
+ DispatchAsyncEvent(u"loadeddata"_ns);
+ mLoadedDataFired = true;
+ }
+
+ if (oldState < HAVE_FUTURE_DATA && mReadyState >= HAVE_FUTURE_DATA) {
+ DispatchAsyncEvent(u"canplay"_ns);
+ if (!mPaused) {
+ if (mDecoder && !mSuspendedByInactiveDocOrDocshell) {
+ MOZ_ASSERT(AllowedToPlay());
+ mDecoder->Play();
+ }
+ NotifyAboutPlaying();
+ }
+ }
+
+ CheckAutoplayDataReady();
+
+ if (oldState < HAVE_ENOUGH_DATA && mReadyState >= HAVE_ENOUGH_DATA) {
+ DispatchAsyncEvent(u"canplaythrough"_ns);
+ }
+}
+
+static const char* const gNetworkStateToString[] = {"EMPTY", "IDLE", "LOADING",
+ "NO_SOURCE"};
+
+void HTMLMediaElement::ChangeNetworkState(nsMediaNetworkState aState) {
+ if (mNetworkState == aState) {
+ return;
+ }
+
+ nsMediaNetworkState oldState = mNetworkState;
+ mNetworkState = aState;
+ LOG(LogLevel::Debug,
+ ("%p Network state changed to %s", this, gNetworkStateToString[aState]));
+ DDLOG(DDLogCategory::Property, "network_state",
+ gNetworkStateToString[aState]);
+
+ if (oldState == NETWORK_LOADING) {
+ // Stop progress notification when exiting NETWORK_LOADING.
+ StopProgress();
+ }
+
+ if (mNetworkState == NETWORK_LOADING) {
+ // Start progress notification when entering NETWORK_LOADING.
+ StartProgress();
+ } else if (mNetworkState == NETWORK_IDLE && !mErrorSink->mError) {
+ // Fire 'suspend' event when entering NETWORK_IDLE and no error presented.
+ DispatchAsyncEvent(u"suspend"_ns);
+ }
+
+ // According to the resource selection (step2, step9-18), dedicated media
+ // source failure step (step4) and aborting existing load (step4), set show
+ // poster flag to true. https://html.spec.whatwg.org/multipage/media.html
+ if (mNetworkState == NETWORK_NO_SOURCE || mNetworkState == NETWORK_EMPTY) {
+ mShowPoster = true;
+ }
+
+ // Changing mNetworkState affects AddRemoveSelfReference().
+ AddRemoveSelfReference();
+}
+
+bool HTMLMediaElement::IsEligibleForAutoplay() {
+ // We also activate autoplay when playing a media source since the data
+ // download is controlled by the script and there is no way to evaluate
+ // MediaDecoder::CanPlayThrough().
+
+ if (!HasAttr(nsGkAtoms::autoplay)) {
+ return false;
+ }
+
+ if (!mCanAutoplayFlag) {
+ return false;
+ }
+
+ if (IsEditable()) {
+ return false;
+ }
+
+ if (!mPaused) {
+ return false;
+ }
+
+ if (mSuspendedByInactiveDocOrDocshell) {
+ return false;
+ }
+
+ // Static document is used for print preview and printing, should not be
+ // autoplay
+ if (OwnerDoc()->IsStaticDocument()) {
+ return false;
+ }
+
+ if (ShouldBeSuspendedByInactiveDocShell()) {
+ LOG(LogLevel::Debug, ("%p prohibiting autoplay by the docShell", this));
+ return false;
+ }
+
+ if (MediaPlaybackDelayPolicy::ShouldDelayPlayback(this)) {
+ CreateResumeDelayedMediaPlaybackAgentIfNeeded();
+ LOG(LogLevel::Debug, ("%p delay playing from autoplay", this));
+ return false;
+ }
+
+ return mReadyState >= HAVE_ENOUGH_DATA;
+}
+
+void HTMLMediaElement::CheckAutoplayDataReady() {
+ if (!IsEligibleForAutoplay()) {
+ return;
+ }
+ if (!AllowedToPlay()) {
+ DispatchEventsWhenPlayWasNotAllowed();
+ return;
+ }
+ RunAutoplay();
+}
+
+void HTMLMediaElement::RunAutoplay() {
+ mAllowedToPlayPromise.ResolveIfExists(true, __func__);
+ mPaused = false;
+ // We changed mPaused which can affect AddRemoveSelfReference
+ AddRemoveSelfReference();
+ UpdateSrcMediaStreamPlaying();
+ UpdateAudioChannelPlayingState();
+ StartMediaControlKeyListenerIfNeeded();
+
+ if (mDecoder) {
+ SetPlayedOrSeeked(true);
+ if (mCurrentPlayRangeStart == -1.0) {
+ mCurrentPlayRangeStart = CurrentTime();
+ }
+ MOZ_ASSERT(!mSuspendedByInactiveDocOrDocshell);
+ mDecoder->Play();
+ } else if (mSrcStream) {
+ SetPlayedOrSeeked(true);
+ }
+
+ // https://html.spec.whatwg.org/multipage/media.html#ready-states:show-poster-flag
+ if (mShowPoster) {
+ mShowPoster = false;
+ if (mTextTrackManager) {
+ mTextTrackManager->TimeMarchesOn();
+ }
+ }
+
+ // For blocked media, the event would be pending until it is resumed.
+ DispatchAsyncEvent(u"play"_ns);
+
+ DispatchAsyncEvent(u"playing"_ns);
+}
+
+bool HTMLMediaElement::IsActuallyInvisible() const {
+ // That means an element is not connected. It probably hasn't connected to a
+ // document tree, or connects to a disconnected DOM tree.
+ if (!IsInComposedDoc()) {
+ return true;
+ }
+
+ // An element is not in user's view port, which means it's either existing in
+ // somewhere in the page where user hasn't seen yet, or is being set
+ // `display:none`.
+ if (!IsInViewPort()) {
+ return true;
+ }
+
+ // Element being used in picture-in-picture mode would be always visible.
+ if (IsBeingUsedInPictureInPictureMode()) {
+ return false;
+ }
+
+ // That check is the page is in the background.
+ return OwnerDoc()->Hidden();
+}
+
+bool HTMLMediaElement::IsInViewPort() const {
+ return mVisibilityState == Visibility::ApproximatelyVisible;
+}
+
+VideoFrameContainer* HTMLMediaElement::GetVideoFrameContainer() {
+ if (mShuttingDown) {
+ return nullptr;
+ }
+
+ if (mVideoFrameContainer) return mVideoFrameContainer;
+
+ // Only video frames need an image container.
+ if (!IsVideo()) {
+ return nullptr;
+ }
+
+ mVideoFrameContainer = new VideoFrameContainer(
+ this, MakeAndAddRef<ImageContainer>(ImageContainer::ASYNCHRONOUS));
+
+ return mVideoFrameContainer;
+}
+
+void HTMLMediaElement::PrincipalChanged(MediaStreamTrack* aTrack) {
+ if (aTrack != mSelectedVideoStreamTrack) {
+ return;
+ }
+
+ nsContentUtils::CombineResourcePrincipals(&mSrcStreamVideoPrincipal,
+ aTrack->GetPrincipal());
+
+ LOG(LogLevel::Debug,
+ ("HTMLMediaElement %p video track principal changed to %p (combined "
+ "into %p). Waiting for it to reach VideoFrameContainer before setting.",
+ this, aTrack->GetPrincipal(), mSrcStreamVideoPrincipal.get()));
+
+ if (mVideoFrameContainer) {
+ UpdateSrcStreamVideoPrincipal(
+ mVideoFrameContainer->GetLastPrincipalHandle());
+ }
+}
+
+void HTMLMediaElement::UpdateSrcStreamVideoPrincipal(
+ const PrincipalHandle& aPrincipalHandle) {
+ nsTArray<RefPtr<VideoStreamTrack>> videoTracks;
+ mSrcStream->GetVideoTracks(videoTracks);
+
+ for (const RefPtr<VideoStreamTrack>& track : videoTracks) {
+ if (PrincipalHandleMatches(aPrincipalHandle, track->GetPrincipal()) &&
+ !track->Ended()) {
+ // When the PrincipalHandle for the VideoFrameContainer changes to that of
+ // a live track in mSrcStream we know that a removed track was displayed
+ // but is no longer so.
+ LOG(LogLevel::Debug, ("HTMLMediaElement %p VideoFrameContainer's "
+ "PrincipalHandle matches track %p. That's all we "
+ "need.",
+ this, track.get()));
+ mSrcStreamVideoPrincipal = track->GetPrincipal();
+ break;
+ }
+ }
+}
+
+void HTMLMediaElement::PrincipalHandleChangedForVideoFrameContainer(
+ VideoFrameContainer* aContainer,
+ const PrincipalHandle& aNewPrincipalHandle) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mSrcStream) {
+ return;
+ }
+
+ LOG(LogLevel::Debug, ("HTMLMediaElement %p PrincipalHandle changed in "
+ "VideoFrameContainer.",
+ this));
+
+ UpdateSrcStreamVideoPrincipal(aNewPrincipalHandle);
+}
+
+already_AddRefed<nsMediaEventRunner> HTMLMediaElement::GetEventRunner(
+ const nsAString& aName, EventFlag aFlag) {
+ RefPtr<nsMediaEventRunner> runner;
+ if (aName.EqualsLiteral("playing")) {
+ runner = new nsNotifyAboutPlayingRunner(this, TakePendingPlayPromises());
+ } else if (aName.EqualsLiteral("timeupdate")) {
+ runner = new nsTimeupdateRunner(this, aFlag == EventFlag::eMandatory);
+ } else {
+ runner = new nsAsyncEventRunner(aName, this);
+ }
+ return runner.forget();
+}
+
+nsresult HTMLMediaElement::DispatchEvent(const nsAString& aName) {
+ LOG_EVENT(LogLevel::Debug, ("%p Dispatching event %s", this,
+ NS_ConvertUTF16toUTF8(aName).get()));
+
+ if (mEventBlocker->ShouldBlockEventDelivery()) {
+ RefPtr<nsMediaEventRunner> runner = GetEventRunner(aName);
+ mEventBlocker->PostponeEvent(runner);
+ return NS_OK;
+ }
+
+ return nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, aName,
+ CanBubble::eNo, Cancelable::eNo);
+}
+
+void HTMLMediaElement::DispatchAsyncEvent(const nsAString& aName) {
+ RefPtr<nsMediaEventRunner> runner = GetEventRunner(aName);
+ DispatchAsyncEvent(std::move(runner));
+}
+
+void HTMLMediaElement::DispatchAsyncEvent(RefPtr<nsMediaEventRunner> aRunner) {
+ NS_ConvertUTF16toUTF8 eventName(aRunner->EventName());
+ LOG_EVENT(LogLevel::Debug, ("%p Queuing event %s", this, eventName.get()));
+ DDLOG(DDLogCategory::Event, "HTMLMediaElement", nsCString(eventName.get()));
+ if (mEventBlocker->ShouldBlockEventDelivery()) {
+ mEventBlocker->PostponeEvent(aRunner);
+ return;
+ }
+ GetMainThreadSerialEventTarget()->Dispatch(aRunner.forget());
+}
+
+bool HTMLMediaElement::IsPotentiallyPlaying() const {
+ // TODO:
+ // playback has not stopped due to errors,
+ // and the element has not paused for user interaction
+ return !mPaused &&
+ (mReadyState == HAVE_ENOUGH_DATA || mReadyState == HAVE_FUTURE_DATA) &&
+ !IsPlaybackEnded();
+}
+
+bool HTMLMediaElement::IsPlaybackEnded() const {
+ // TODO:
+ // the current playback position is equal to the effective end of the media
+ // resource. See bug 449157.
+ if (mDecoder) {
+ return mReadyState >= HAVE_METADATA && mDecoder->IsEnded();
+ }
+ if (mSrcStream) {
+ return mReadyState >= HAVE_METADATA && mSrcStreamPlaybackEnded;
+ }
+ return false;
+}
+
+already_AddRefed<nsIPrincipal> HTMLMediaElement::GetCurrentPrincipal() {
+ if (mDecoder) {
+ return mDecoder->GetCurrentPrincipal();
+ }
+ if (mSrcStream) {
+ nsTArray<RefPtr<MediaStreamTrack>> tracks;
+ mSrcStream->GetTracks(tracks);
+ nsCOMPtr<nsIPrincipal> principal = mSrcStream->GetPrincipal();
+ return principal.forget();
+ }
+ return nullptr;
+}
+
+bool HTMLMediaElement::HadCrossOriginRedirects() {
+ if (mDecoder) {
+ return mDecoder->HadCrossOriginRedirects();
+ }
+ return false;
+}
+
+bool HTMLMediaElement::ShouldResistFingerprinting(RFPTarget aTarget) const {
+ return OwnerDoc()->ShouldResistFingerprinting(aTarget);
+}
+
+already_AddRefed<nsIPrincipal> HTMLMediaElement::GetCurrentVideoPrincipal() {
+ if (mDecoder) {
+ return mDecoder->GetCurrentPrincipal();
+ }
+ if (mSrcStream) {
+ nsCOMPtr<nsIPrincipal> principal = mSrcStreamVideoPrincipal;
+ return principal.forget();
+ }
+ return nullptr;
+}
+
+void HTMLMediaElement::NotifyDecoderPrincipalChanged() {
+ RefPtr<nsIPrincipal> principal = GetCurrentPrincipal();
+ bool isSameOrigin = !principal || IsCORSSameOrigin();
+ mDecoder->UpdateSameOriginStatus(isSameOrigin);
+
+ if (isSameOrigin) {
+ principal = NodePrincipal();
+ }
+ for (const auto& entry : mOutputTrackSources.Values()) {
+ entry->SetPrincipal(principal);
+ }
+ mDecoder->SetOutputTracksPrincipal(principal);
+}
+
+void HTMLMediaElement::Invalidate(ImageSizeChanged aImageSizeChanged,
+ const Maybe<nsIntSize>& aNewIntrinsicSize,
+ ForceInvalidate aForceInvalidate) {
+ nsIFrame* frame = GetPrimaryFrame();
+ if (aNewIntrinsicSize) {
+ UpdateMediaSize(aNewIntrinsicSize.value());
+ if (frame) {
+ nsPresContext* presContext = frame->PresContext();
+ PresShell* presShell = presContext->PresShell();
+ presShell->FrameNeedsReflow(frame,
+ IntrinsicDirty::FrameAncestorsAndDescendants,
+ NS_FRAME_IS_DIRTY);
+ }
+ }
+
+ RefPtr<ImageContainer> imageContainer = GetImageContainer();
+ bool asyncInvalidate = imageContainer && imageContainer->IsAsync() &&
+ aForceInvalidate == ForceInvalidate::No;
+ if (frame) {
+ if (aImageSizeChanged == ImageSizeChanged::Yes) {
+ frame->InvalidateFrame();
+ } else {
+ frame->InvalidateLayer(DisplayItemType::TYPE_VIDEO, nullptr, nullptr,
+ asyncInvalidate ? nsIFrame::UPDATE_IS_ASYNC : 0);
+ }
+ }
+
+ SVGObserverUtils::InvalidateDirectRenderingObservers(this);
+}
+
+void HTMLMediaElement::UpdateMediaSize(const nsIntSize& aSize) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (IsVideo() && mReadyState != HAVE_NOTHING &&
+ mMediaInfo.mVideo.mDisplay != aSize) {
+ DispatchAsyncEvent(u"resize"_ns);
+ }
+
+ mMediaInfo.mVideo.mDisplay = aSize;
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal);
+}
+
+void HTMLMediaElement::SuspendOrResumeElement(bool aSuspendElement) {
+ LOG(LogLevel::Debug, ("%p SuspendOrResumeElement(suspend=%d) docHidden=%d",
+ this, aSuspendElement, OwnerDoc()->Hidden()));
+
+ if (aSuspendElement == mSuspendedByInactiveDocOrDocshell) {
+ return;
+ }
+
+ mSuspendedByInactiveDocOrDocshell = aSuspendElement;
+ UpdateSrcMediaStreamPlaying();
+ UpdateAudioChannelPlayingState();
+
+ if (aSuspendElement) {
+ if (mDecoder) {
+ mDecoder->Pause();
+ mDecoder->Suspend();
+ mDecoder->SetDelaySeekMode(true);
+ }
+ mEventBlocker->SetBlockEventDelivery(true);
+ // We won't want to resume media element from the bfcache.
+ ClearResumeDelayedMediaPlaybackAgentIfNeeded();
+ mMediaControlKeyListener->StopIfNeeded();
+ } else {
+ if (mDecoder) {
+ mDecoder->Resume();
+ if (!mPaused && !mDecoder->IsEnded()) {
+ mDecoder->Play();
+ }
+ mDecoder->SetDelaySeekMode(false);
+ }
+ mEventBlocker->SetBlockEventDelivery(false);
+ // If the media element has been blocked and isn't still allowed to play
+ // when it comes back from the bfcache, we would notify front end to show
+ // the blocking icon in order to inform user that the site is still being
+ // blocked.
+ if (mHasEverBeenBlockedForAutoplay && !AllowedToPlay()) {
+ MaybeNotifyAutoplayBlocked();
+ }
+ StartMediaControlKeyListenerIfNeeded();
+ }
+ if (StaticPrefs::media_testing_only_events()) {
+ auto dispatcher = MakeRefPtr<AsyncEventDispatcher>(
+ this, u"MozMediaSuspendChanged"_ns, CanBubble::eYes,
+ ChromeOnlyDispatch::eYes);
+ dispatcher->PostDOMEvent();
+ }
+}
+
+bool HTMLMediaElement::IsBeingDestroyed() {
+ nsIDocShell* docShell = OwnerDoc()->GetDocShell();
+ bool isBeingDestroyed = false;
+ if (docShell) {
+ docShell->IsBeingDestroyed(&isBeingDestroyed);
+ }
+ return isBeingDestroyed;
+}
+
+bool HTMLMediaElement::ShouldBeSuspendedByInactiveDocShell() const {
+ BrowsingContext* bc = OwnerDoc()->GetBrowsingContext();
+ return bc && !bc->IsActive() && bc->Top()->GetSuspendMediaWhenInactive();
+}
+
+void HTMLMediaElement::NotifyOwnerDocumentActivityChanged() {
+ if (mDecoder && !IsBeingDestroyed()) {
+ NotifyDecoderActivityChanges();
+ }
+
+ // We would suspend media when the document is inactive, or its docshell has
+ // been set to hidden and explicitly wants to suspend media. In those cases,
+ // the media would be not visible and we don't want them to continue playing.
+ bool shouldSuspend =
+ !OwnerDoc()->IsActive() || ShouldBeSuspendedByInactiveDocShell();
+ SuspendOrResumeElement(shouldSuspend);
+
+ // If the owning document has become inactive we should shutdown the CDM.
+ if (!OwnerDoc()->IsCurrentActiveDocument() && mMediaKeys) {
+ // We don't shutdown MediaKeys here because it also listens for document
+ // activity and will take care of shutting down itself.
+ DDUNLINKCHILD(mMediaKeys.get());
+ mMediaKeys = nullptr;
+ if (mDecoder) {
+ ShutdownDecoder();
+ }
+ }
+
+ AddRemoveSelfReference();
+}
+
+void HTMLMediaElement::NotifyFullScreenChanged() {
+ const bool isInFullScreen = IsInFullScreen();
+ if (isInFullScreen) {
+ StartMediaControlKeyListenerIfNeeded();
+ if (!mMediaControlKeyListener->IsStarted()) {
+ MEDIACONTROL_LOG("Failed to start the listener when entering fullscreen");
+ }
+ }
+ // Updating controller fullscreen state no matter the listener starts or not.
+ BrowsingContext* bc = OwnerDoc()->GetBrowsingContext();
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(bc)) {
+ updater->NotifyMediaFullScreenState(bc->Id(), isInFullScreen);
+ }
+}
+
+void HTMLMediaElement::AddRemoveSelfReference() {
+ // XXX we could release earlier here in many situations if we examined
+ // which event listeners are attached. Right now we assume there is a
+ // potential listener for every event. We would also have to keep the
+ // element alive if it was playing and producing audio output --- right now
+ // that's covered by the !mPaused check.
+ Document* ownerDoc = OwnerDoc();
+
+ // See the comment at the top of this file for the explanation of this
+ // boolean expression.
+ bool needSelfReference =
+ !mShuttingDown && ownerDoc->IsActive() &&
+ (mDelayingLoadEvent || (!mPaused && !Ended()) ||
+ (mDecoder && mDecoder->IsSeeking()) || IsEligibleForAutoplay() ||
+ (mMediaSource ? mProgressTimer : mNetworkState == NETWORK_LOADING));
+
+ if (needSelfReference != mHasSelfReference) {
+ mHasSelfReference = needSelfReference;
+ RefPtr<HTMLMediaElement> self = this;
+ if (needSelfReference) {
+ // The shutdown observer will hold a strong reference to us. This
+ // will do to keep us alive. We need to know about shutdown so that
+ // we can release our self-reference.
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "dom::HTMLMediaElement::AddSelfReference",
+ [self]() { self->mShutdownObserver->AddRefMediaElement(); }));
+ } else {
+ // Dispatch Release asynchronously so that we don't destroy this object
+ // inside a call stack of method calls on this object
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "dom::HTMLMediaElement::AddSelfReference",
+ [self]() { self->mShutdownObserver->ReleaseMediaElement(); }));
+ }
+ }
+}
+
+void HTMLMediaElement::NotifyShutdownEvent() {
+ mShuttingDown = true;
+ ResetState();
+ AddRemoveSelfReference();
+}
+
+void HTMLMediaElement::DispatchAsyncSourceError(nsIContent* aSourceElement) {
+ LOG_EVENT(LogLevel::Debug, ("%p Queuing simple source error event", this));
+
+ nsCOMPtr<nsIRunnable> event =
+ new nsSourceErrorEventRunner(this, aSourceElement);
+ GetMainThreadSerialEventTarget()->Dispatch(event.forget());
+}
+
+void HTMLMediaElement::NotifyAddedSource() {
+ // If a source element is inserted as a child of a media element
+ // that has no src attribute and whose networkState has the value
+ // NETWORK_EMPTY, the user agent must invoke the media element's
+ // resource selection algorithm.
+ if (!HasAttr(nsGkAtoms::src) && mNetworkState == NETWORK_EMPTY) {
+ AssertReadyStateIsNothing();
+ QueueSelectResourceTask();
+ }
+
+ // A load was paused in the resource selection algorithm, waiting for
+ // a new source child to be added, resume the resource selection algorithm.
+ if (mLoadWaitStatus == WAITING_FOR_SOURCE) {
+ // Rest the flag so we don't queue multiple LoadFromSourceTask() when
+ // multiple <source> are attached in an event loop.
+ mLoadWaitStatus = NOT_WAITING;
+ QueueLoadFromSourceTask();
+ }
+}
+
+HTMLSourceElement* HTMLMediaElement::GetNextSource() {
+ mSourceLoadCandidate = nullptr;
+
+ while (true) {
+ if (mSourcePointer == nsINode::GetLastChild()) {
+ return nullptr; // no more children
+ }
+
+ if (!mSourcePointer) {
+ mSourcePointer = nsINode::GetFirstChild();
+ } else {
+ mSourcePointer = mSourcePointer->GetNextSibling();
+ }
+ nsIContent* child = mSourcePointer;
+
+ // If child is a <source> element, it is the next candidate.
+ if (auto* source = HTMLSourceElement::FromNodeOrNull(child)) {
+ mSourceLoadCandidate = source;
+ return source;
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE("Execution should not reach here!");
+ return nullptr;
+}
+
+void HTMLMediaElement::ChangeDelayLoadStatus(bool aDelay) {
+ if (mDelayingLoadEvent == aDelay) return;
+
+ mDelayingLoadEvent = aDelay;
+
+ LOG(LogLevel::Debug, ("%p ChangeDelayLoadStatus(%d) doc=0x%p", this, aDelay,
+ mLoadBlockedDoc.get()));
+ if (mDecoder) {
+ mDecoder->SetLoadInBackground(!aDelay);
+ }
+ if (aDelay) {
+ mLoadBlockedDoc = OwnerDoc();
+ mLoadBlockedDoc->BlockOnload();
+ } else {
+ // mLoadBlockedDoc might be null due to GC unlinking
+ if (mLoadBlockedDoc) {
+ mLoadBlockedDoc->UnblockOnload(false);
+ mLoadBlockedDoc = nullptr;
+ }
+ }
+
+ // We changed mDelayingLoadEvent which can affect AddRemoveSelfReference
+ AddRemoveSelfReference();
+}
+
+already_AddRefed<nsILoadGroup> HTMLMediaElement::GetDocumentLoadGroup() {
+ if (!OwnerDoc()->IsActive()) {
+ NS_WARNING("Load group requested for media element in inactive document.");
+ }
+ return OwnerDoc()->GetDocumentLoadGroup();
+}
+
+nsresult HTMLMediaElement::CopyInnerTo(Element* aDest) {
+ nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (aDest->OwnerDoc()->IsStaticDocument()) {
+ HTMLMediaElement* dest = static_cast<HTMLMediaElement*>(aDest);
+ dest->SetMediaInfo(mMediaInfo);
+ }
+ return rv;
+}
+
+already_AddRefed<TimeRanges> HTMLMediaElement::Buffered() const {
+ media::TimeIntervals buffered =
+ mDecoder ? mDecoder->GetBuffered() : media::TimeIntervals();
+ RefPtr<TimeRanges> ranges = new TimeRanges(
+ ToSupports(OwnerDoc()), buffered.ToMicrosecondResolution());
+ return ranges.forget();
+}
+
+void HTMLMediaElement::SetRequestHeaders(nsIHttpChannel* aChannel) {
+ // Send Accept header for video and audio types only (Bug 489071)
+ SetAcceptHeader(aChannel);
+
+ // Apache doesn't send Content-Length when gzip transfer encoding is used,
+ // which prevents us from estimating the video length (if explicit
+ // Content-Duration and a length spec in the container are not present either)
+ // and from seeking. So, disable the standard "Accept-Encoding: gzip,deflate"
+ // that we usually send. See bug 614760.
+ DebugOnly<nsresult> rv =
+ aChannel->SetRequestHeader("Accept-Encoding"_ns, ""_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Set the Referrer header
+ //
+ // FIXME: Shouldn't this use the Element constructor? Though I guess it
+ // doesn't matter as no HTMLMediaElement supports the referrerinfo attribute.
+ auto referrerInfo = MakeRefPtr<ReferrerInfo>(*OwnerDoc());
+ rv = aChannel->SetReferrerInfoWithoutClone(referrerInfo);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+const TimeStamp& HTMLMediaElement::LastTimeupdateDispatchTime() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mLastTimeUpdateDispatchTime;
+}
+
+void HTMLMediaElement::UpdateLastTimeupdateDispatchTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mLastTimeUpdateDispatchTime = TimeStamp::Now();
+}
+
+bool HTMLMediaElement::ShouldQueueTimeupdateAsyncTask(
+ TimeupdateType aType) const {
+ NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+ // That means dispatching `timeupdate` is mandatorily required in the spec.
+ if (aType == TimeupdateType::eMandatory) {
+ return true;
+ }
+
+ // The timeupdate only occurs when the current playback position changes.
+ // https://html.spec.whatwg.org/multipage/media.html#event-media-timeupdate
+ if (mLastCurrentTime == CurrentTime()) {
+ return false;
+ }
+
+ // Number of milliseconds between timeupdate events as defined by spec.
+ if (!mQueueTimeUpdateRunnerTime.IsNull() &&
+ TimeStamp::Now() - mQueueTimeUpdateRunnerTime <
+ TimeDuration::FromMilliseconds(TIMEUPDATE_MS)) {
+ return false;
+ }
+ return true;
+}
+
+void HTMLMediaElement::FireTimeUpdate(TimeupdateType aType) {
+ NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+
+ if (ShouldQueueTimeupdateAsyncTask(aType)) {
+ RefPtr<nsMediaEventRunner> runner =
+ GetEventRunner(u"timeupdate"_ns, aType == TimeupdateType::eMandatory
+ ? EventFlag::eMandatory
+ : EventFlag::eNone);
+ DispatchAsyncEvent(std::move(runner));
+ mQueueTimeUpdateRunnerTime = TimeStamp::Now();
+ mLastCurrentTime = CurrentTime();
+ }
+ if (mFragmentEnd >= 0.0 && CurrentTime() >= mFragmentEnd) {
+ Pause();
+ mFragmentEnd = -1.0;
+ mFragmentStart = -1.0;
+ mDecoder->SetFragmentEndTime(mFragmentEnd);
+ }
+
+ // Update the cues displaying on the video.
+ // Here mTextTrackManager can be null if the cycle collector has unlinked
+ // us before our parent. In that case UnbindFromTree will call us
+ // when our parent is unlinked.
+ if (mTextTrackManager) {
+ mTextTrackManager->TimeMarchesOn();
+ }
+}
+
+MediaError* HTMLMediaElement::GetError() const { return mErrorSink->mError; }
+
+void HTMLMediaElement::GetCurrentSpec(nsCString& aString) {
+ // If playing a regular URL, an ObjectURL of a Blob/File, return that.
+ if (mLoadingSrc) {
+ mLoadingSrc->GetSpec(aString);
+ } else if (mSrcMediaSource) {
+ // If playing an ObjectURL, and it's a MediaSource, return the value of the
+ // `src` attribute.
+ nsAutoString src;
+ GetSrc(src);
+ CopyUTF16toUTF8(src, aString);
+ } else {
+ // Playing e.g. a MediaStream via an object URL - return an empty string
+ aString.Truncate();
+ }
+}
+
+double HTMLMediaElement::MozFragmentEnd() {
+ double duration = Duration();
+
+ // If there is no end fragment, or the fragment end is greater than the
+ // duration, return the duration.
+ return (mFragmentEnd < 0.0 || mFragmentEnd > duration) ? duration
+ : mFragmentEnd;
+}
+
+void HTMLMediaElement::SetDefaultPlaybackRate(double aDefaultPlaybackRate,
+ ErrorResult& aRv) {
+ if (mSrcAttrStream) {
+ return;
+ }
+
+ if (aDefaultPlaybackRate < 0) {
+ aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
+ return;
+ }
+
+ double defaultPlaybackRate = ClampPlaybackRate(aDefaultPlaybackRate);
+
+ if (mDefaultPlaybackRate == defaultPlaybackRate) {
+ return;
+ }
+
+ mDefaultPlaybackRate = defaultPlaybackRate;
+ DispatchAsyncEvent(u"ratechange"_ns);
+}
+
+void HTMLMediaElement::SetPlaybackRate(double aPlaybackRate, ErrorResult& aRv) {
+ if (mSrcAttrStream) {
+ return;
+ }
+
+ // Changing the playback rate of a media that has more than two channels is
+ // not supported.
+ if (aPlaybackRate < 0) {
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return;
+ }
+
+ if (mPlaybackRate == aPlaybackRate) {
+ return;
+ }
+
+ mPlaybackRate = aPlaybackRate;
+ // Playback rate threshold above which audio is muted.
+ uint32_t threshold = StaticPrefs::media_audio_playbackrate_muting_threshold();
+ if (mPlaybackRate != 0.0 &&
+ (mPlaybackRate > threshold || mPlaybackRate < 1. / threshold)) {
+ SetMutedInternal(mMuted | MUTED_BY_INVALID_PLAYBACK_RATE);
+ } else {
+ SetMutedInternal(mMuted & ~MUTED_BY_INVALID_PLAYBACK_RATE);
+ }
+
+ if (mDecoder) {
+ mDecoder->SetPlaybackRate(ClampPlaybackRate(mPlaybackRate));
+ }
+ DispatchAsyncEvent(u"ratechange"_ns);
+}
+
+void HTMLMediaElement::SetPreservesPitch(bool aPreservesPitch) {
+ mPreservesPitch = aPreservesPitch;
+ if (mDecoder) {
+ mDecoder->SetPreservesPitch(mPreservesPitch);
+ }
+}
+
+ImageContainer* HTMLMediaElement::GetImageContainer() {
+ VideoFrameContainer* container = GetVideoFrameContainer();
+ return container ? container->GetImageContainer() : nullptr;
+}
+
+void HTMLMediaElement::UpdateAudioChannelPlayingState() {
+ if (mAudioChannelWrapper) {
+ mAudioChannelWrapper->UpdateAudioChannelPlayingState();
+ }
+}
+
+static const char* VisibilityString(Visibility aVisibility) {
+ switch (aVisibility) {
+ case Visibility::Untracked: {
+ return "Untracked";
+ }
+ case Visibility::ApproximatelyNonVisible: {
+ return "ApproximatelyNonVisible";
+ }
+ case Visibility::ApproximatelyVisible: {
+ return "ApproximatelyVisible";
+ }
+ }
+
+ return "NAN";
+}
+
+void HTMLMediaElement::OnVisibilityChange(Visibility aNewVisibility) {
+ LOG(LogLevel::Debug,
+ ("OnVisibilityChange(): %s\n", VisibilityString(aNewVisibility)));
+
+ mVisibilityState = aNewVisibility;
+ if (StaticPrefs::media_test_video_suspend()) {
+ DispatchAsyncEvent(u"visibilitychanged"_ns);
+ }
+
+ if (!mDecoder) {
+ return;
+ }
+ NotifyDecoderActivityChanges();
+}
+
+MediaKeys* HTMLMediaElement::GetMediaKeys() const { return mMediaKeys; }
+
+bool HTMLMediaElement::ContainsRestrictedContent() const {
+ return GetMediaKeys() != nullptr;
+}
+
+void HTMLMediaElement::SetCDMProxyFailure(const MediaResult& aResult) {
+ LOG(LogLevel::Debug, ("%s", __func__));
+ MOZ_ASSERT(mSetMediaKeysDOMPromise);
+
+ ResetSetMediaKeysTempVariables();
+
+ mSetMediaKeysDOMPromise->MaybeReject(aResult.Code(), aResult.Message());
+}
+
+void HTMLMediaElement::RemoveMediaKeys() {
+ LOG(LogLevel::Debug, ("%s", __func__));
+ // 5.2.3 Stop using the CDM instance represented by the mediaKeys attribute
+ // to decrypt media data and remove the association with the media element.
+ if (mMediaKeys) {
+ mMediaKeys->Unbind();
+ }
+ mMediaKeys = nullptr;
+}
+
+bool HTMLMediaElement::TryRemoveMediaKeysAssociation() {
+ MOZ_ASSERT(mMediaKeys);
+ LOG(LogLevel::Debug, ("%s", __func__));
+ // 5.2.1 If the user agent or CDM do not support removing the association,
+ // let this object's attaching media keys value be false and reject promise
+ // with a new DOMException whose name is NotSupportedError.
+ // 5.2.2 If the association cannot currently be removed, let this object's
+ // attaching media keys value be false and reject promise with a new
+ // DOMException whose name is InvalidStateError.
+ if (mDecoder) {
+ RefPtr<HTMLMediaElement> self = this;
+ mDecoder->SetCDMProxy(nullptr)
+ ->Then(
+ AbstractMainThread(), __func__,
+ [self]() {
+ self->mSetCDMRequest.Complete();
+
+ self->RemoveMediaKeys();
+ if (self->AttachNewMediaKeys()) {
+ // No incoming MediaKeys object or MediaDecoder is not
+ // created yet.
+ self->MakeAssociationWithCDMResolved();
+ }
+ },
+ [self](const MediaResult& aResult) {
+ self->mSetCDMRequest.Complete();
+ // 5.2.4 If the preceding step failed, let this object's
+ // attaching media keys value be false and reject promise with
+ // a new DOMException whose name is the appropriate error name.
+ self->SetCDMProxyFailure(aResult);
+ })
+ ->Track(mSetCDMRequest);
+ return false;
+ }
+
+ RemoveMediaKeys();
+ return true;
+}
+
+bool HTMLMediaElement::DetachExistingMediaKeys() {
+ LOG(LogLevel::Debug, ("%s", __func__));
+ MOZ_ASSERT(mSetMediaKeysDOMPromise);
+ // 5.1 If mediaKeys is not null, CDM instance represented by mediaKeys is
+ // already in use by another media element, and the user agent is unable
+ // to use it with this element, let this object's attaching media keys
+ // value be false and reject promise with a new DOMException whose name
+ // is QuotaExceededError.
+ if (mIncomingMediaKeys && mIncomingMediaKeys->IsBoundToMediaElement()) {
+ SetCDMProxyFailure(MediaResult(
+ NS_ERROR_DOM_MEDIA_KEY_QUOTA_EXCEEDED_ERR,
+ "MediaKeys object is already bound to another HTMLMediaElement"));
+ return false;
+ }
+
+ // 5.2 If the mediaKeys attribute is not null, run the following steps:
+ if (mMediaKeys) {
+ return TryRemoveMediaKeysAssociation();
+ }
+ return true;
+}
+
+void HTMLMediaElement::MakeAssociationWithCDMResolved() {
+ LOG(LogLevel::Debug, ("%s", __func__));
+ MOZ_ASSERT(mSetMediaKeysDOMPromise);
+
+ // 5.4 Set the mediaKeys attribute to mediaKeys.
+ mMediaKeys = mIncomingMediaKeys;
+#ifdef MOZ_WMF_CDM
+ if (mMediaKeys && mMediaKeys->GetCDMProxy()) {
+ mIsUsingWMFCDM = !!mMediaKeys->GetCDMProxy()->AsWMFCDMProxy();
+ }
+#endif
+ // 5.5 Let this object's attaching media keys value be false.
+ ResetSetMediaKeysTempVariables();
+ // 5.6 Resolve promise.
+ mSetMediaKeysDOMPromise->MaybeResolveWithUndefined();
+ mSetMediaKeysDOMPromise = nullptr;
+}
+
+bool HTMLMediaElement::TryMakeAssociationWithCDM(CDMProxy* aProxy) {
+ LOG(LogLevel::Debug, ("%s", __func__));
+ MOZ_ASSERT(aProxy);
+
+ // 5.3.3 Queue a task to run the "Attempt to Resume Playback If Necessary"
+ // algorithm on the media element.
+ // Note: Setting the CDMProxy on the MediaDecoder will unblock playback.
+ if (mDecoder) {
+ // CDMProxy is set asynchronously in MediaFormatReader, once it's done,
+ // HTMLMediaElement should resolve or reject the DOM promise.
+ RefPtr<HTMLMediaElement> self = this;
+ mDecoder->SetCDMProxy(aProxy)
+ ->Then(
+ AbstractMainThread(), __func__,
+ [self]() {
+ self->mSetCDMRequest.Complete();
+ self->MakeAssociationWithCDMResolved();
+ },
+ [self](const MediaResult& aResult) {
+ self->mSetCDMRequest.Complete();
+ self->SetCDMProxyFailure(aResult);
+ })
+ ->Track(mSetCDMRequest);
+ return false;
+ }
+ return true;
+}
+
+bool HTMLMediaElement::AttachNewMediaKeys() {
+ LOG(LogLevel::Debug,
+ ("%s incoming MediaKeys(%p)", __func__, mIncomingMediaKeys.get()));
+ MOZ_ASSERT(mSetMediaKeysDOMPromise);
+
+ // 5.3. If mediaKeys is not null, run the following steps:
+ if (mIncomingMediaKeys) {
+ auto* cdmProxy = mIncomingMediaKeys->GetCDMProxy();
+ if (!cdmProxy) {
+ SetCDMProxyFailure(MediaResult(
+ NS_ERROR_DOM_INVALID_STATE_ERR,
+ "CDM crashed before binding MediaKeys object to HTMLMediaElement"));
+ return false;
+ }
+
+ // 5.3.1 Associate the CDM instance represented by mediaKeys with the
+ // media element for decrypting media data.
+ if (NS_FAILED(mIncomingMediaKeys->Bind(this))) {
+ // 5.3.2 If the preceding step failed, run the following steps:
+
+ // 5.3.2.1 Set the mediaKeys attribute to null.
+ mMediaKeys = nullptr;
+ // 5.3.2.2 Let this object's attaching media keys value be false.
+ // 5.3.2.3 Reject promise with a new DOMException whose name is
+ // the appropriate error name.
+ SetCDMProxyFailure(
+ MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR,
+ "Failed to bind MediaKeys object to HTMLMediaElement"));
+ return false;
+ }
+ return TryMakeAssociationWithCDM(cdmProxy);
+ }
+ return true;
+}
+
+void HTMLMediaElement::ResetSetMediaKeysTempVariables() {
+ mAttachingMediaKey = false;
+ mIncomingMediaKeys = nullptr;
+}
+
+already_AddRefed<Promise> HTMLMediaElement::SetMediaKeys(
+ mozilla::dom::MediaKeys* aMediaKeys, ErrorResult& aRv) {
+ LOG(LogLevel::Debug, ("%p SetMediaKeys(%p) mMediaKeys=%p mDecoder=%p", this,
+ aMediaKeys, mMediaKeys.get(), mDecoder.get()));
+
+ if (MozAudioCaptured()) {
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return nullptr;
+ }
+
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
+ if (!win) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ RefPtr<DetailedPromise> promise = DetailedPromise::Create(
+ win->AsGlobal(), aRv, "HTMLMediaElement.setMediaKeys"_ns);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // 1. If mediaKeys and the mediaKeys attribute are the same object,
+ // return a resolved promise.
+ if (mMediaKeys == aMediaKeys) {
+ promise->MaybeResolveWithUndefined();
+ return promise.forget();
+ }
+
+ // 2. If this object's attaching media keys value is true, return a
+ // promise rejected with a new DOMException whose name is InvalidStateError.
+ if (mAttachingMediaKey) {
+ promise->MaybeRejectWithInvalidStateError(
+ "A MediaKeys object is in attaching operation.");
+ return promise.forget();
+ }
+
+ // 3. Let this object's attaching media keys value be true.
+ mAttachingMediaKey = true;
+ mIncomingMediaKeys = aMediaKeys;
+
+ // 4. Let promise be a new promise.
+ mSetMediaKeysDOMPromise = promise;
+
+ // 5. Run the following steps in parallel:
+
+ // 5.1 & 5.2 & 5.3
+ if (!DetachExistingMediaKeys() || !AttachNewMediaKeys()) {
+ return promise.forget();
+ }
+
+ // 5.4, 5.5, 5.6
+ MakeAssociationWithCDMResolved();
+
+ // 6. Return promise.
+ return promise.forget();
+}
+
+EventHandlerNonNull* HTMLMediaElement::GetOnencrypted() {
+ return EventTarget::GetEventHandler(nsGkAtoms::onencrypted);
+}
+
+void HTMLMediaElement::SetOnencrypted(EventHandlerNonNull* aCallback) {
+ EventTarget::SetEventHandler(nsGkAtoms::onencrypted, aCallback);
+}
+
+EventHandlerNonNull* HTMLMediaElement::GetOnwaitingforkey() {
+ return EventTarget::GetEventHandler(nsGkAtoms::onwaitingforkey);
+}
+
+void HTMLMediaElement::SetOnwaitingforkey(EventHandlerNonNull* aCallback) {
+ EventTarget::SetEventHandler(nsGkAtoms::onwaitingforkey, aCallback);
+}
+
+void HTMLMediaElement::DispatchEncrypted(const nsTArray<uint8_t>& aInitData,
+ const nsAString& aInitDataType) {
+ LOG(LogLevel::Debug, ("%p DispatchEncrypted initDataType='%s'", this,
+ NS_ConvertUTF16toUTF8(aInitDataType).get()));
+
+ if (mReadyState == HAVE_NOTHING) {
+ // Ready state not HAVE_METADATA (yet), don't dispatch encrypted now.
+ // Queueing for later dispatch in MetadataLoaded.
+ mPendingEncryptedInitData.AddInitData(aInitDataType, aInitData);
+ return;
+ }
+
+ RefPtr<MediaEncryptedEvent> event;
+ if (IsCORSSameOrigin()) {
+ event = MediaEncryptedEvent::Constructor(this, aInitDataType, aInitData);
+ } else {
+ event = MediaEncryptedEvent::Constructor(this);
+ }
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, event.forget());
+ asyncDispatcher->PostDOMEvent();
+}
+
+bool HTMLMediaElement::IsEventAttributeNameInternal(nsAtom* aName) {
+ return aName == nsGkAtoms::onencrypted ||
+ nsGenericHTMLElement::IsEventAttributeNameInternal(aName);
+}
+
+void HTMLMediaElement::NotifyWaitingForKey() {
+ LOG(LogLevel::Debug, ("%p, NotifyWaitingForKey()", this));
+
+ // http://w3c.github.io/encrypted-media/#wait-for-key
+ // 7.3.4 Queue a "waitingforkey" Event
+ // 1. Let the media element be the specified HTMLMediaElement object.
+ // 2. If the media element's waiting for key value is true, abort these steps.
+ if (mWaitingForKey == NOT_WAITING_FOR_KEY) {
+ // 3. Set the media element's waiting for key value to true.
+ // Note: algorithm continues in UpdateReadyStateInternal() when all decoded
+ // data enqueued in the MDSM is consumed.
+ mWaitingForKey = WAITING_FOR_KEY;
+ // mWaitingForKey changed outside of UpdateReadyStateInternal. This may
+ // affect mReadyState.
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal);
+ }
+}
+
+AudioTrackList* HTMLMediaElement::AudioTracks() { return mAudioTrackList; }
+
+VideoTrackList* HTMLMediaElement::VideoTracks() { return mVideoTrackList; }
+
+TextTrackList* HTMLMediaElement::GetTextTracks() {
+ return GetOrCreateTextTrackManager()->GetTextTracks();
+}
+
+already_AddRefed<TextTrack> HTMLMediaElement::AddTextTrack(
+ TextTrackKind aKind, const nsAString& aLabel, const nsAString& aLanguage) {
+ return GetOrCreateTextTrackManager()->AddTextTrack(
+ aKind, aLabel, aLanguage, TextTrackMode::Hidden,
+ TextTrackReadyState::Loaded, TextTrackSource::AddTextTrack);
+}
+
+void HTMLMediaElement::PopulatePendingTextTrackList() {
+ if (mTextTrackManager) {
+ mTextTrackManager->PopulatePendingList();
+ }
+}
+
+TextTrackManager* HTMLMediaElement::GetOrCreateTextTrackManager() {
+ if (!mTextTrackManager) {
+ mTextTrackManager = new TextTrackManager(this);
+ mTextTrackManager->AddListeners();
+ }
+ return mTextTrackManager;
+}
+
+MediaDecoderOwner::NextFrameStatus HTMLMediaElement::NextFrameStatus() {
+ if (mDecoder) {
+ return mDecoder->NextFrameStatus();
+ }
+ if (mSrcStream) {
+ AutoTArray<RefPtr<MediaTrack>, 4> tracks;
+ GetAllEnabledMediaTracks(tracks);
+ if (!tracks.IsEmpty() && !mSrcStreamPlaybackEnded) {
+ return NEXT_FRAME_AVAILABLE;
+ }
+ return NEXT_FRAME_UNAVAILABLE;
+ }
+ return NEXT_FRAME_UNINITIALIZED;
+}
+
+void HTMLMediaElement::SetDecoder(MediaDecoder* aDecoder) {
+ MOZ_ASSERT(aDecoder); // Use ShutdownDecoder() to clear.
+ if (mDecoder) {
+ ShutdownDecoder();
+ }
+ mDecoder = aDecoder;
+ DDLINKCHILD("decoder", mDecoder.get());
+ if (mDecoder && mForcedHidden) {
+ mDecoder->SetForcedHidden(mForcedHidden);
+ }
+}
+
+float HTMLMediaElement::ComputedVolume() const {
+ return mMuted ? 0.0f
+ : mAudioChannelWrapper ? mAudioChannelWrapper->GetEffectiveVolume()
+ : static_cast<float>(mVolume);
+}
+
+bool HTMLMediaElement::ComputedMuted() const {
+ return (mMuted & MUTED_BY_AUDIO_CHANNEL);
+}
+
+bool HTMLMediaElement::IsSuspendedByInactiveDocOrDocShell() const {
+ return mSuspendedByInactiveDocOrDocshell;
+}
+
+bool HTMLMediaElement::IsCurrentlyPlaying() const {
+ // We have playable data, but we still need to check whether data is "real"
+ // current data.
+ return mReadyState >= HAVE_CURRENT_DATA && !IsPlaybackEnded();
+}
+
+void HTMLMediaElement::SetAudibleState(bool aAudible) {
+ if (mIsAudioTrackAudible != aAudible) {
+ mIsAudioTrackAudible = aAudible;
+ NotifyAudioPlaybackChanged(
+ AudioChannelService::AudibleChangedReasons::eDataAudibleChanged);
+ }
+}
+
+void HTMLMediaElement::NotifyAudioPlaybackChanged(
+ AudibleChangedReasons aReason) {
+ if (mAudioChannelWrapper) {
+ mAudioChannelWrapper->NotifyAudioPlaybackChanged(aReason);
+ }
+ // We would start the listener after media becomes audible.
+ const bool isAudible = IsAudible();
+ if (isAudible && !mMediaControlKeyListener->IsStarted()) {
+ StartMediaControlKeyListenerIfNeeded();
+ }
+ mMediaControlKeyListener->UpdateMediaAudibleState(isAudible);
+ // only request wake lock for audible media.
+ UpdateWakeLock();
+}
+
+void HTMLMediaElement::SetMediaInfo(const MediaInfo& aInfo) {
+ const bool oldHasAudio = mMediaInfo.HasAudio();
+ mMediaInfo = aInfo;
+ if ((aInfo.HasAudio() != oldHasAudio) && mResumeDelayedPlaybackAgent) {
+ mResumeDelayedPlaybackAgent->UpdateAudibleState(this, IsAudible());
+ }
+ nsILoadContext* loadContext = OwnerDoc()->GetLoadContext();
+ if (HasAudio() && loadContext && !loadContext->UsePrivateBrowsing()) {
+ mTitleChangeObserver->Subscribe();
+ UpdateStreamName();
+ } else {
+ mTitleChangeObserver->Unsubscribe();
+ }
+ if (mAudioChannelWrapper) {
+ mAudioChannelWrapper->AudioCaptureTrackChangeIfNeeded();
+ }
+ UpdateWakeLock();
+}
+
+MediaInfo HTMLMediaElement::GetMediaInfo() const { return mMediaInfo; }
+
+FrameStatistics* HTMLMediaElement::GetFrameStatistics() const {
+ return mDecoder ? &(mDecoder->GetFrameStatistics()) : nullptr;
+}
+
+void HTMLMediaElement::DispatchAsyncTestingEvent(const nsAString& aName) {
+ if (!StaticPrefs::media_testing_only_events()) {
+ return;
+ }
+ DispatchAsyncEvent(aName);
+}
+
+void HTMLMediaElement::AudioCaptureTrackChange(bool aCapture) {
+ // No need to capture a silent media element.
+ if (!HasAudio()) {
+ return;
+ }
+
+ if (aCapture && !mStreamWindowCapturer) {
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ if (!window) {
+ return;
+ }
+
+ MediaTrackGraph* mtg = MediaTrackGraph::GetInstance(
+ MediaTrackGraph::AUDIO_THREAD_DRIVER, window,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ MediaTrackGraph::DEFAULT_OUTPUT_DEVICE);
+ RefPtr<DOMMediaStream> stream =
+ CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED,
+ StreamCaptureType::CAPTURE_AUDIO, mtg);
+ mStreamWindowCapturer =
+ MakeUnique<MediaStreamWindowCapturer>(stream, window->WindowID());
+ } else if (!aCapture && mStreamWindowCapturer) {
+ for (size_t i = 0; i < mOutputStreams.Length(); i++) {
+ if (mOutputStreams[i].mStream == mStreamWindowCapturer->mStream) {
+ // We own this MediaStream, it is not exposed to JS.
+ AutoTArray<RefPtr<MediaStreamTrack>, 2> tracks;
+ mStreamWindowCapturer->mStream->GetTracks(tracks);
+ for (auto& track : tracks) {
+ track->Stop();
+ }
+ mOutputStreams.RemoveElementAt(i);
+ break;
+ }
+ }
+ mStreamWindowCapturer = nullptr;
+ if (mOutputStreams.IsEmpty()) {
+ mTracksCaptured = nullptr;
+ }
+ }
+}
+
+void HTMLMediaElement::NotifyCueDisplayStatesChanged() {
+ if (!mTextTrackManager) {
+ return;
+ }
+
+ mTextTrackManager->DispatchUpdateCueDisplay();
+}
+
+void HTMLMediaElement::LogVisibility(CallerAPI aAPI) {
+ const bool isVisible = mVisibilityState == Visibility::ApproximatelyVisible;
+
+ LOG(LogLevel::Debug, ("%p visibility = %u, API: '%d' and 'All'", this,
+ isVisible, static_cast<int>(aAPI)));
+
+ if (!isVisible) {
+ LOG(LogLevel::Debug, ("%p inTree = %u, API: '%d' and 'All'", this,
+ IsInComposedDoc(), static_cast<int>(aAPI)));
+ }
+}
+
+void HTMLMediaElement::UpdateCustomPolicyAfterPlayed() {
+ if (mAudioChannelWrapper) {
+ mAudioChannelWrapper->NotifyPlayStateChanged();
+ }
+}
+
+AbstractThread* HTMLMediaElement::AbstractMainThread() const {
+ return AbstractThread::MainThread();
+}
+
+nsTArray<RefPtr<PlayPromise>> HTMLMediaElement::TakePendingPlayPromises() {
+ return std::move(mPendingPlayPromises);
+}
+
+void HTMLMediaElement::NotifyAboutPlaying() {
+ // Stick to the DispatchAsyncEvent() call path for now because we want to
+ // trigger some telemetry-related codes in the DispatchAsyncEvent() method.
+ DispatchAsyncEvent(u"playing"_ns);
+}
+
+already_AddRefed<PlayPromise> HTMLMediaElement::CreatePlayPromise(
+ ErrorResult& aRv) const {
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
+
+ if (!win) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ RefPtr<PlayPromise> promise = PlayPromise::Create(win->AsGlobal(), aRv);
+ LOG(LogLevel::Debug, ("%p created PlayPromise %p", this, promise.get()));
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> HTMLMediaElement::CreateDOMPromise(
+ ErrorResult& aRv) const {
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
+
+ if (!win) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ return Promise::Create(win->AsGlobal(), aRv);
+}
+
+void HTMLMediaElement::AsyncResolvePendingPlayPromises() {
+ if (mShuttingDown) {
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> event = new nsResolveOrRejectPendingPlayPromisesRunner(
+ this, TakePendingPlayPromises());
+
+ GetMainThreadSerialEventTarget()->Dispatch(event.forget());
+}
+
+void HTMLMediaElement::AsyncRejectPendingPlayPromises(nsresult aError) {
+ if (!mPaused) {
+ mPaused = true;
+ DispatchAsyncEvent(u"pause"_ns);
+ }
+
+ if (mShuttingDown) {
+ return;
+ }
+
+ if (aError == NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR) {
+ DispatchEventsWhenPlayWasNotAllowed();
+ }
+
+ nsCOMPtr<nsIRunnable> event = new nsResolveOrRejectPendingPlayPromisesRunner(
+ this, TakePendingPlayPromises(), aError);
+
+ GetMainThreadSerialEventTarget()->Dispatch(event.forget());
+}
+
+void HTMLMediaElement::GetEMEInfo(dom::EMEDebugInfo& aInfo) {
+ MOZ_ASSERT(NS_IsMainThread(),
+ "MediaKeys expects to be interacted with on main thread!");
+ if (!mMediaKeys) {
+ return;
+ }
+ mMediaKeys->GetKeySystem(aInfo.mKeySystem);
+ mMediaKeys->GetSessionsInfo(aInfo.mSessionsInfo);
+}
+
+void HTMLMediaElement::NotifyDecoderActivityChanges() const {
+ if (mDecoder) {
+ mDecoder->NotifyOwnerActivityChanged(IsActuallyInvisible(),
+ IsInComposedDoc());
+ }
+}
+
+Document* HTMLMediaElement::GetDocument() const { return OwnerDoc(); }
+
+bool HTMLMediaElement::IsAudible() const {
+ // No audio track.
+ if (!HasAudio()) {
+ return false;
+ }
+
+ // Muted or the volume should not be ~0
+ if (mMuted || (std::fabs(Volume()) <= 1e-7)) {
+ return false;
+ }
+
+ return mIsAudioTrackAudible;
+}
+
+Maybe<nsAutoString> HTMLMediaElement::GetKeySystem() const {
+ if (!mMediaKeys) {
+ return Nothing();
+ }
+ nsAutoString keySystem;
+ mMediaKeys->GetKeySystem(keySystem);
+ return Some(keySystem);
+}
+
+void HTMLMediaElement::ConstructMediaTracks(const MediaInfo* aInfo) {
+ if (!aInfo) {
+ return;
+ }
+
+ AudioTrackList* audioList = AudioTracks();
+ if (audioList && aInfo->HasAudio()) {
+ const TrackInfo& info = aInfo->mAudio;
+ RefPtr<AudioTrack> track = MediaTrackList::CreateAudioTrack(
+ audioList->GetOwnerGlobal(), info.mId, info.mKind, info.mLabel,
+ info.mLanguage, info.mEnabled);
+
+ audioList->AddTrack(track);
+ }
+
+ VideoTrackList* videoList = VideoTracks();
+ if (videoList && aInfo->HasVideo()) {
+ const TrackInfo& info = aInfo->mVideo;
+ RefPtr<VideoTrack> track = MediaTrackList::CreateVideoTrack(
+ videoList->GetOwnerGlobal(), info.mId, info.mKind, info.mLabel,
+ info.mLanguage);
+
+ videoList->AddTrack(track);
+ track->SetEnabledInternal(info.mEnabled, MediaTrack::FIRE_NO_EVENTS);
+ }
+}
+
+void HTMLMediaElement::RemoveMediaTracks() {
+ if (mAudioTrackList) {
+ mAudioTrackList->RemoveTracks();
+ }
+ if (mVideoTrackList) {
+ mVideoTrackList->RemoveTracks();
+ }
+}
+
+class MediaElementGMPCrashHelper : public GMPCrashHelper {
+ public:
+ explicit MediaElementGMPCrashHelper(HTMLMediaElement* aElement)
+ : mElement(aElement) {
+ MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe.
+ }
+ already_AddRefed<nsPIDOMWindowInner> GetPluginCrashedEventTarget() override {
+ MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe.
+ if (!mElement) {
+ return nullptr;
+ }
+ return do_AddRef(mElement->OwnerDoc()->GetInnerWindow());
+ }
+
+ private:
+ WeakPtr<HTMLMediaElement> mElement;
+};
+
+already_AddRefed<GMPCrashHelper> HTMLMediaElement::CreateGMPCrashHelper() {
+ return MakeAndAddRef<MediaElementGMPCrashHelper>(this);
+}
+
+void HTMLMediaElement::MarkAsTainted() {
+ mHasSuspendTaint = true;
+
+ if (mDecoder) {
+ mDecoder->SetSuspendTaint(true);
+ }
+}
+
+bool HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj) {
+ return nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::debugger) ||
+ nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::tabs);
+}
+
+already_AddRefed<Promise> HTMLMediaElement::SetSinkId(const nsAString& aSinkId,
+ ErrorResult& aRv) {
+ LOG(LogLevel::Info,
+ ("%p, setSinkId(%s)", this, NS_ConvertUTF16toUTF8(aSinkId).get()));
+ nsCOMPtr<nsPIDOMWindowInner> win = OwnerDoc()->GetInnerWindow();
+ if (!win) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = Promise::Create(win->AsGlobal(), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!FeaturePolicyUtils::IsFeatureAllowed(win->GetExtantDoc(),
+ u"speaker-selection"_ns)) {
+ promise->MaybeRejectWithNotAllowedError(
+ "Document's Permissions Policy does not allow setSinkId()");
+ }
+
+ if (mSink.first.Equals(aSinkId)) {
+ promise->MaybeResolveWithUndefined();
+ return promise.forget();
+ }
+
+ RefPtr<MediaDevices> mediaDevices = win->Navigator()->GetMediaDevices(aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsString sinkId(aSinkId);
+ mediaDevices->GetSinkDevice(sinkId)
+ ->Then(
+ AbstractMainThread(), __func__,
+ [self = RefPtr<HTMLMediaElement>(this),
+ this](RefPtr<AudioDeviceInfo>&& aInfo) {
+ // Sink found switch output device.
+ MOZ_ASSERT(aInfo);
+ if (mDecoder) {
+ RefPtr<SinkInfoPromise> p = mDecoder->SetSink(aInfo)->Then(
+ AbstractMainThread(), __func__,
+ [aInfo](const GenericPromise::ResolveOrRejectValue& aValue) {
+ if (aValue.IsResolve()) {
+ return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
+ }
+ return SinkInfoPromise::CreateAndReject(
+ aValue.RejectValue(), __func__);
+ });
+ return p;
+ }
+ if (mSrcStream) {
+ MOZ_ASSERT(mMediaStreamRenderer);
+ RefPtr<SinkInfoPromise> p =
+ mMediaStreamRenderer->SetAudioOutputDevice(aInfo)->Then(
+ AbstractMainThread(), __func__,
+ [aInfo](
+ const GenericPromise::ResolveOrRejectValue& aValue) {
+ if (aValue.IsResolve()) {
+ return SinkInfoPromise::CreateAndResolve(aInfo,
+ __func__);
+ }
+ return SinkInfoPromise::CreateAndReject(
+ aValue.RejectValue(), __func__);
+ });
+ return p;
+ }
+ // No media attached to the element save it for later.
+ return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
+ },
+ [](nsresult res) {
+ // Promise is rejected, sink not found.
+ return SinkInfoPromise::CreateAndReject(res, __func__);
+ })
+ ->Then(AbstractMainThread(), __func__,
+ [promise, self = RefPtr<HTMLMediaElement>(this), this,
+ sinkId](const SinkInfoPromise::ResolveOrRejectValue& aValue) {
+ if (aValue.IsResolve()) {
+ LOG(LogLevel::Info, ("%p, set sinkid=%s", this,
+ NS_ConvertUTF16toUTF8(sinkId).get()));
+ mSink = std::pair(sinkId, aValue.ResolveValue());
+ promise->MaybeResolveWithUndefined();
+ } else {
+ switch (aValue.RejectValue()) {
+ case NS_ERROR_ABORT:
+ promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ break;
+ case NS_ERROR_NOT_AVAILABLE: {
+ promise->MaybeRejectWithNotFoundError(
+ "The object can not be found here.");
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid error.");
+ }
+ }
+ });
+
+ aRv = NS_OK;
+ return promise.forget();
+}
+
+void HTMLMediaElement::NotifyTextTrackModeChanged() {
+ if (mPendingTextTrackChanged) {
+ return;
+ }
+ mPendingTextTrackChanged = true;
+ AbstractMainThread()->Dispatch(
+ NS_NewRunnableFunction("HTMLMediaElement::NotifyTextTrackModeChanged",
+ [this, self = RefPtr<HTMLMediaElement>(this)]() {
+ mPendingTextTrackChanged = false;
+ if (!mTextTrackManager) {
+ return;
+ }
+ GetTextTracks()->CreateAndDispatchChangeEvent();
+ // https://html.spec.whatwg.org/multipage/media.html#text-track-model:show-poster-flag
+ if (!mShowPoster) {
+ mTextTrackManager->TimeMarchesOn();
+ }
+ }));
+}
+
+void HTMLMediaElement::CreateResumeDelayedMediaPlaybackAgentIfNeeded() {
+ if (mResumeDelayedPlaybackAgent) {
+ return;
+ }
+ mResumeDelayedPlaybackAgent =
+ MediaPlaybackDelayPolicy::CreateResumeDelayedPlaybackAgent(this,
+ IsAudible());
+ if (!mResumeDelayedPlaybackAgent) {
+ LOG(LogLevel::Debug,
+ ("%p Failed to create a delayed playback agant", this));
+ return;
+ }
+ mResumeDelayedPlaybackAgent->GetResumePromise()
+ ->Then(
+ AbstractMainThread(), __func__,
+ [self = RefPtr<HTMLMediaElement>(this)]() {
+ LOG(LogLevel::Debug, ("%p Resume delayed Play() call", self.get()));
+ self->mResumePlaybackRequest.Complete();
+ self->mResumeDelayedPlaybackAgent = nullptr;
+ IgnoredErrorResult dummy;
+ RefPtr<Promise> toBeIgnored = self->Play(dummy);
+ },
+ [self = RefPtr<HTMLMediaElement>(this)]() {
+ LOG(LogLevel::Debug,
+ ("%p Can not resume delayed Play() call", self.get()));
+ self->mResumePlaybackRequest.Complete();
+ self->mResumeDelayedPlaybackAgent = nullptr;
+ })
+ ->Track(mResumePlaybackRequest);
+}
+
+void HTMLMediaElement::ClearResumeDelayedMediaPlaybackAgentIfNeeded() {
+ if (mResumeDelayedPlaybackAgent) {
+ mResumePlaybackRequest.DisconnectIfExists();
+ mResumeDelayedPlaybackAgent = nullptr;
+ }
+}
+
+void HTMLMediaElement::NotifyMediaControlPlaybackStateChanged() {
+ if (!mMediaControlKeyListener->IsStarted()) {
+ return;
+ }
+ if (mPaused) {
+ mMediaControlKeyListener->NotifyMediaStoppedPlaying();
+ } else {
+ mMediaControlKeyListener->NotifyMediaStartedPlaying();
+ }
+}
+
+bool HTMLMediaElement::IsInFullScreen() const {
+ return State().HasState(ElementState::FULLSCREEN);
+}
+
+bool HTMLMediaElement::IsPlayable() const {
+ return (mDecoder || mSrcStream) && !HasError();
+}
+
+bool HTMLMediaElement::ShouldStartMediaControlKeyListener() const {
+ if (!IsPlayable()) {
+ MEDIACONTROL_LOG("Not start listener because media is not playable");
+ return false;
+ }
+
+ if (mSrcStream) {
+ MEDIACONTROL_LOG("Not listening because media is real-time");
+ return false;
+ }
+
+ if (IsBeingUsedInPictureInPictureMode()) {
+ MEDIACONTROL_LOG("Start listener because of being used in PiP mode");
+ return true;
+ }
+
+ if (IsInFullScreen()) {
+ MEDIACONTROL_LOG("Start listener because of being used in fullscreen");
+ return true;
+ }
+
+ // In order to filter out notification-ish sound, we use this pref to set the
+ // eligible media duration to prevent showing media control for those short
+ // sound.
+ if (Duration() <
+ StaticPrefs::media_mediacontrol_eligible_media_duration_s()) {
+ MEDIACONTROL_LOG("Not listening because media's duration %f is too short.",
+ Duration());
+ return false;
+ }
+
+ // This includes cases such like `video is muted`, `video has zero volume`,
+ // `video's audio track is still inaudible` and `tab is muted by audio channel
+ // (tab sound indicator)`, all these cases would make media inaudible.
+ // `ComputedVolume()` would return the final volume applied the affection made
+ // by audio channel, which is used to detect if the tab is muted by audio
+ // channel.
+ if (!IsAudible() || ComputedVolume() == 0.0f) {
+ MEDIACONTROL_LOG("Not listening because media is inaudible");
+ return false;
+ }
+ return true;
+}
+
+void HTMLMediaElement::StartMediaControlKeyListenerIfNeeded() {
+ if (!ShouldStartMediaControlKeyListener()) {
+ return;
+ }
+ mMediaControlKeyListener->Start();
+}
+
+void HTMLMediaElement::UpdateStreamName() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoString aTitle;
+ OwnerDoc()->GetTitle(aTitle);
+
+ if (mDecoder) {
+ mDecoder->SetStreamName(aTitle);
+ }
+}
+
+void HTMLMediaElement::SetSecondaryMediaStreamRenderer(
+ VideoFrameContainer* aContainer,
+ FirstFrameVideoOutput* aFirstFrameOutput /* = nullptr */) {
+ MOZ_ASSERT(mSrcStream);
+ MOZ_ASSERT(mMediaStreamRenderer);
+ if (mSecondaryMediaStreamRenderer) {
+ mSecondaryMediaStreamRenderer->Shutdown();
+ mSecondaryMediaStreamRenderer = nullptr;
+ }
+ if (aContainer) {
+ mSecondaryMediaStreamRenderer = MakeAndAddRef<MediaStreamRenderer>(
+ AbstractMainThread(), aContainer, aFirstFrameOutput, this);
+ if (mSrcStreamIsPlaying) {
+ mSecondaryMediaStreamRenderer->Start();
+ }
+ if (mSelectedVideoStreamTrack) {
+ mSecondaryMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack);
+ }
+ }
+}
+
+void HTMLMediaElement::UpdateMediaControlAfterPictureInPictureModeChanged() {
+ if (IsBeingUsedInPictureInPictureMode()) {
+ // When media enters PIP mode, we should ensure that the listener has been
+ // started because we always want to control PIP video.
+ StartMediaControlKeyListenerIfNeeded();
+ if (!mMediaControlKeyListener->IsStarted()) {
+ MEDIACONTROL_LOG("Failed to start listener when entering PIP mode");
+ }
+ // Updating controller PIP state no matter the listener starts or not.
+ mMediaControlKeyListener->SetPictureInPictureModeEnabled(true);
+ } else {
+ mMediaControlKeyListener->SetPictureInPictureModeEnabled(false);
+ }
+}
+
+bool HTMLMediaElement::IsBeingUsedInPictureInPictureMode() const {
+ if (!IsVideo()) {
+ return false;
+ }
+ return static_cast<const HTMLVideoElement*>(this)->IsCloningElementVisually();
+}
+
+void HTMLMediaElement::NodeInfoChanged(Document* aOldDoc) {
+ if (mMediaSource) {
+ OwnerDoc()->AddMediaElementWithMSE();
+ aOldDoc->RemoveMediaElementWithMSE();
+ }
+
+ nsGenericHTMLElement::NodeInfoChanged(aOldDoc);
+}
+
+#ifdef MOZ_WMF_CDM
+bool HTMLMediaElement::IsUsingWMFCDM() const { return mIsUsingWMFCDM; };
+#endif
+
+} // namespace mozilla::dom
+
+#undef LOG
+#undef LOG_EVENT
diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h
new file mode 100644
index 0000000000..0d35fcc85c
--- /dev/null
+++ b/dom/html/HTMLMediaElement.h
@@ -0,0 +1,1941 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLMediaElement_h
+#define mozilla_dom_HTMLMediaElement_h
+
+#include "nsGenericHTMLElement.h"
+#include "AudioChannelService.h"
+#include "MediaEventSource.h"
+#include "SeekTarget.h"
+#include "MediaDecoderOwner.h"
+#include "MediaElementEventRunners.h"
+#include "MediaPlaybackDelayPolicy.h"
+#include "MediaPromiseDefs.h"
+#include "TelemetryProbesReporter.h"
+#include "nsCycleCollectionParticipant.h"
+#include "Visibility.h"
+#include "mozilla/CORSMode.h"
+#include "DecoderTraits.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/StateWatching.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/dom/DecoderDoctorNotificationBinding.h"
+#include "mozilla/dom/HTMLMediaElementBinding.h"
+#include "mozilla/dom/MediaDebugInfoBinding.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/TextTrackManager.h"
+#include "nsGkAtoms.h"
+#include "PrincipalChangeObserver.h"
+#include "nsStubMutationObserver.h"
+#include "MediaSegment.h" // for PrincipalHandle, GraphTime
+
+#include <utility>
+
+// X.h on Linux #defines CurrentTime as 0L, so we have to #undef it here.
+#ifdef CurrentTime
+# undef CurrentTime
+#endif
+
+// Define to output information on decoding and painting framerate
+/* #define DEBUG_FRAME_RATE 1 */
+
+using nsMediaNetworkState = uint16_t;
+using nsMediaReadyState = uint16_t;
+using SuspendTypes = uint32_t;
+using AudibleChangedReasons = uint32_t;
+
+class nsIStreamListener;
+
+namespace mozilla {
+class AbstractThread;
+class ChannelMediaDecoder;
+class DecoderDoctorDiagnostics;
+class DOMMediaStream;
+class ErrorResult;
+class FirstFrameVideoOutput;
+class MediaResource;
+class MediaDecoder;
+class MediaInputPort;
+class MediaTrack;
+class MediaTrackGraph;
+class MediaStreamWindowCapturer;
+struct SharedDummyTrack;
+class VideoFrameContainer;
+class VideoOutput;
+namespace dom {
+class HTMLSourceElement;
+class MediaKeys;
+class TextTrack;
+class TimeRanges;
+class WakeLock;
+class MediaStreamTrack;
+class MediaStreamTrackSource;
+class MediaTrack;
+class VideoStreamTrack;
+} // namespace dom
+} // namespace mozilla
+
+class AudioDeviceInfo;
+class nsIChannel;
+class nsIHttpChannel;
+class nsILoadGroup;
+class nsIRunnable;
+class nsISerialEventTarget;
+class nsITimer;
+class nsRange;
+
+namespace mozilla::dom {
+
+// Number of milliseconds between timeupdate events as defined by spec
+#define TIMEUPDATE_MS 250
+
+class MediaError;
+class MediaSource;
+class PlayPromise;
+class Promise;
+class TextTrackList;
+class AudioTrackList;
+class VideoTrackList;
+
+enum class StreamCaptureType : uint8_t { CAPTURE_ALL_TRACKS, CAPTURE_AUDIO };
+
+enum class StreamCaptureBehavior : uint8_t {
+ CONTINUE_WHEN_ENDED,
+ FINISH_WHEN_ENDED
+};
+
+class HTMLMediaElement : public nsGenericHTMLElement,
+ public MediaDecoderOwner,
+ public PrincipalChangeObserver<MediaStreamTrack>,
+ public SupportsWeakPtr,
+ public nsStubMutationObserver,
+ public TelemetryProbesReporterOwner {
+ public:
+ using TimeStamp = mozilla::TimeStamp;
+ using ImageContainer = mozilla::layers::ImageContainer;
+ using VideoFrameContainer = mozilla::VideoFrameContainer;
+ using MediaResource = mozilla::MediaResource;
+ using MediaDecoderOwner = mozilla::MediaDecoderOwner;
+ using MetadataTags = mozilla::MetadataTags;
+
+ // Helper struct to keep track of the MediaStreams returned by
+ // mozCaptureStream(). For each OutputMediaStream, dom::MediaTracks get
+ // captured into MediaStreamTracks which get added to
+ // OutputMediaStream::mStream.
+ struct OutputMediaStream {
+ OutputMediaStream(RefPtr<DOMMediaStream> aStream, bool aCapturingAudioOnly,
+ bool aFinishWhenEnded);
+ ~OutputMediaStream();
+
+ RefPtr<DOMMediaStream> mStream;
+ nsTArray<RefPtr<MediaStreamTrack>> mLiveTracks;
+ const bool mCapturingAudioOnly;
+ const bool mFinishWhenEnded;
+ // If mFinishWhenEnded is true, this is the URI of the first resource
+ // mStream got tracks for.
+ nsCOMPtr<nsIURI> mFinishWhenEndedLoadingSrc;
+ // If mFinishWhenEnded is true, this is the first MediaStream mStream got
+ // tracks for.
+ RefPtr<DOMMediaStream> mFinishWhenEndedAttrStream;
+ // If mFinishWhenEnded is true, this is the MediaSource being played.
+ RefPtr<MediaSource> mFinishWhenEndedMediaSource;
+ };
+
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+
+ CORSMode GetCORSMode() { return mCORSMode; }
+
+ explicit HTMLMediaElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+ void Init();
+
+ // `eMandatory`: `timeupdate` occurs according to the spec requirement.
+ // Eg.
+ // https://html.spec.whatwg.org/multipage/media.html#seeking:event-media-timeupdate
+ // `ePeriodic` : `timeupdate` occurs regularly and should follow the rule
+ // of not dispatching that event within 250 ms. Eg.
+ // https://html.spec.whatwg.org/multipage/media.html#offsets-into-the-media-resource:event-media-timeupdate
+ enum class TimeupdateType : bool {
+ eMandatory = false,
+ ePeriodic = true,
+ };
+
+ // This is used for event runner creation. Currently only timeupdate needs
+ // that, but it can be used to extend for other events in the future if
+ // necessary.
+ enum class EventFlag : uint8_t {
+ eNone = 0,
+ eMandatory = 1,
+ };
+
+ /**
+ * This is used when the browser is constructing a video element to play
+ * a channel that we've already started loading. The src attribute and
+ * <source> children are ignored.
+ * @param aChannel the channel to use
+ * @param aListener returns a stream listener that should receive
+ * notifications for the stream
+ */
+ nsresult LoadWithChannel(nsIChannel* aChannel, nsIStreamListener** aListener);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLMediaElement,
+ nsGenericHTMLElement)
+ NS_IMPL_FROMNODE_HELPER(HTMLMediaElement,
+ IsAnyOfHTMLElements(nsGkAtoms::video,
+ nsGkAtoms::audio))
+
+ NS_DECL_ADDSIZEOFEXCLUDINGTHIS
+
+ // EventTarget
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+
+ void NodeInfoChanged(Document* aOldDoc) override;
+
+ virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ virtual nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ virtual void UnbindFromTree(bool aNullParent = true) override;
+ virtual void DoneCreatingElement() override;
+
+ virtual bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+ virtual int32_t TabIndexDefault() override;
+
+ // Called by the video decoder object, on the main thread,
+ // when it has read the metadata containing video dimensions,
+ // etc.
+ virtual void MetadataLoaded(const MediaInfo* aInfo,
+ UniquePtr<const MetadataTags> aTags) final;
+
+ // Called by the decoder object, on the main thread,
+ // when it has read the first frame of the video or audio.
+ void FirstFrameLoaded() final;
+
+ // Called by the video decoder object, on the main thread,
+ // when the resource has a network error during loading.
+ void NetworkError(const MediaResult& aError) final;
+
+ // Called by the video decoder object, on the main thread, when the
+ // resource has a decode error during metadata loading or decoding.
+ void DecodeError(const MediaResult& aError) final;
+
+ // Called by the decoder object, on the main thread, when the
+ // resource has a decode issue during metadata loading or decoding, but can
+ // continue decoding.
+ void DecodeWarning(const MediaResult& aError) final;
+
+ // Return true if error attribute is not null.
+ bool HasError() const final;
+
+ // Called by the video decoder object, on the main thread, when the
+ // resource load has been cancelled.
+ void LoadAborted() final;
+
+ // Called by the video decoder object, on the main thread,
+ // when the video playback has ended.
+ void PlaybackEnded() final;
+
+ // Called by the video decoder object, on the main thread,
+ // when the resource has started seeking.
+ void SeekStarted() final;
+
+ // Called by the video decoder object, on the main thread,
+ // when the resource has completed seeking.
+ void SeekCompleted() final;
+
+ // Called by the video decoder object, on the main thread,
+ // when the resource has aborted seeking.
+ void SeekAborted() final;
+
+ // Called by the media stream, on the main thread, when the download
+ // has been suspended by the cache or because the element itself
+ // asked the decoder to suspend the download.
+ void DownloadSuspended() final;
+
+ // Called by the media stream, on the main thread, when the download
+ // has been resumed by the cache or because the element itself
+ // asked the decoder to resumed the download.
+ void DownloadResumed();
+
+ // Called to indicate the download is progressing.
+ void DownloadProgressed() final;
+
+ // Called by the media decoder to indicate whether the media cache has
+ // suspended the channel.
+ void NotifySuspendedByCache(bool aSuspendedByCache) final;
+
+ // Return true if the media element is actually invisible to users.
+ bool IsActuallyInvisible() const override;
+
+ // Return true if the element is in the view port.
+ bool IsInViewPort() const;
+
+ // Called by the media decoder and the video frame to get the
+ // ImageContainer containing the video data.
+ VideoFrameContainer* GetVideoFrameContainer() final;
+ layers::ImageContainer* GetImageContainer();
+
+ /**
+ * Call this to reevaluate whether we should start/stop due to our owner
+ * document being active, inactive, visible or hidden.
+ */
+ void NotifyOwnerDocumentActivityChanged();
+
+ // Called when the media element enters or leaves the fullscreen.
+ void NotifyFullScreenChanged();
+
+ bool IsInFullScreen() const;
+
+ // From PrincipalChangeObserver<MediaStreamTrack>.
+ void PrincipalChanged(MediaStreamTrack* aTrack) override;
+
+ void UpdateSrcStreamVideoPrincipal(const PrincipalHandle& aPrincipalHandle);
+
+ // Called after the MediaStream we're playing rendered a frame to aContainer
+ // with a different principalHandle than the previous frame.
+ void PrincipalHandleChangedForVideoFrameContainer(
+ VideoFrameContainer* aContainer,
+ const PrincipalHandle& aNewPrincipalHandle) override;
+
+ // Dispatch events
+ void DispatchAsyncEvent(const nsAString& aName) final;
+ void DispatchAsyncEvent(RefPtr<nsMediaEventRunner> aRunner);
+
+ // Triggers a recomputation of readyState.
+ void UpdateReadyState() override {
+ mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal);
+ }
+
+ // Dispatch events that were raised while in the bfcache
+ nsresult DispatchPendingMediaEvents();
+
+ // Return true if we can activate autoplay assuming enough data has arrived.
+ // https://html.spec.whatwg.org/multipage/media.html#eligible-for-autoplay
+ bool IsEligibleForAutoplay();
+
+ // Notify that state has changed that might cause an autoplay element to
+ // start playing.
+ // If the element is 'autoplay' and is ready to play back (not paused,
+ // autoplay pref enabled, etc), it should start playing back.
+ void CheckAutoplayDataReady();
+
+ void RunAutoplay();
+
+ // Check if the media element had crossorigin set when loading started
+ bool ShouldCheckAllowOrigin();
+
+ // Returns true if the currently loaded resource is CORS same-origin with
+ // respect to the document.
+ bool IsCORSSameOrigin();
+
+ // Is the media element potentially playing as defined by the HTML 5
+ // specification.
+ // http://www.whatwg.org/specs/web-apps/current-work/#potentially-playing
+ bool IsPotentiallyPlaying() const;
+
+ // Has playback ended as defined by the HTML 5 specification.
+ // http://www.whatwg.org/specs/web-apps/current-work/#ended
+ bool IsPlaybackEnded() const;
+
+ // principal of the currently playing resource. Anything accessing the
+ // contents of this element must have a principal that subsumes this
+ // principal. Returns null if nothing is playing.
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal();
+
+ // Return true if the loading of this resource required cross-origin
+ // redirects.
+ bool HadCrossOriginRedirects();
+
+ bool ShouldResistFingerprinting(RFPTarget aTarget) const override;
+
+ // Principal of the currently playing video resource. Anything accessing the
+ // image container of this element must have a principal that subsumes this
+ // principal. If there are no live video tracks but content has been rendered
+ // to the image container, we return the last video principal we had. Should
+ // the image container be empty with no live video tracks, we return nullptr.
+ already_AddRefed<nsIPrincipal> GetCurrentVideoPrincipal();
+
+ // called to notify that the principal of the decoder's media resource has
+ // changed.
+ void NotifyDecoderPrincipalChanged() final;
+
+ void GetEMEInfo(dom::EMEDebugInfo& aInfo);
+
+ // Update the visual size of the media. Called from the decoder on the
+ // main thread when/if the size changes.
+ virtual void UpdateMediaSize(const nsIntSize& aSize);
+
+ void Invalidate(ImageSizeChanged aImageSizeChanged,
+ const Maybe<nsIntSize>& aNewIntrinsicSize,
+ ForceInvalidate aForceInvalidate) override;
+
+ // Returns the CanPlayStatus indicating if we can handle the
+ // full MIME type including the optional codecs parameter.
+ static CanPlayStatus GetCanPlay(const nsAString& aType,
+ DecoderDoctorDiagnostics* aDiagnostics);
+
+ /**
+ * Called when a child source element is added to this media element. This
+ * may queue a task to run the select resource algorithm if appropriate.
+ */
+ void NotifyAddedSource();
+
+ /**
+ * Called when there's been an error fetching the resource. This decides
+ * whether it's appropriate to fire an error event.
+ */
+ void NotifyLoadError(const nsACString& aErrorDetails = nsCString());
+
+ /**
+ * Called by one of our associated MediaTrackLists (audio/video) when a
+ * MediaTrack is added.
+ */
+ void NotifyMediaTrackAdded(dom::MediaTrack* aTrack);
+
+ /**
+ * Called by one of our associated MediaTrackLists (audio/video) when a
+ * MediaTrack is removed.
+ */
+ void NotifyMediaTrackRemoved(dom::MediaTrack* aTrack);
+
+ /**
+ * Called by one of our associated MediaTrackLists (audio/video) when an
+ * AudioTrack is enabled or a VideoTrack is selected.
+ */
+ void NotifyMediaTrackEnabled(dom::MediaTrack* aTrack);
+
+ /**
+ * Called by one of our associated MediaTrackLists (audio/video) when an
+ * AudioTrack is disabled or a VideoTrack is unselected.
+ */
+ void NotifyMediaTrackDisabled(dom::MediaTrack* aTrack);
+
+ /**
+ * Returns the current load ID. Asynchronous events store the ID that was
+ * current when they were enqueued, and if it has changed when they come to
+ * fire, they consider themselves cancelled, and don't fire.
+ */
+ uint32_t GetCurrentLoadID() const { return mCurrentLoadID; }
+
+ /**
+ * Returns the load group for this media element's owner document.
+ * XXX XBL2 issue.
+ */
+ already_AddRefed<nsILoadGroup> GetDocumentLoadGroup();
+
+ /**
+ * Returns true if the media has played or completed a seek.
+ * Used by video frame to determine whether to paint the poster.
+ */
+ bool GetPlayedOrSeeked() const { return mHasPlayedOrSeeked; }
+
+ nsresult CopyInnerTo(Element* aDest);
+
+ /**
+ * Sets the Accept header on the HTTP channel to the required
+ * video or audio MIME types.
+ */
+ virtual nsresult SetAcceptHeader(nsIHttpChannel* aChannel) = 0;
+
+ /**
+ * Sets the required request headers on the HTTP channel for
+ * video or audio requests.
+ */
+ void SetRequestHeaders(nsIHttpChannel* aChannel);
+
+ /**
+ * Asynchronously awaits a stable state, whereupon aRunnable runs on the main
+ * thread. This adds an event which run aRunnable to the appshell's list of
+ * sections synchronous the next time control returns to the event loop.
+ */
+ void RunInStableState(nsIRunnable* aRunnable);
+
+ /**
+ * Fires a timeupdate event. If aPeriodic is true, the event will only
+ * be fired if we've not fired a timeupdate event (for any reason) in the
+ * last 250ms, as required by the spec when the current time is periodically
+ * increasing during playback.
+ */
+ void FireTimeUpdate(TimeupdateType aType);
+
+ void MaybeQueueTimeupdateEvent() final {
+ FireTimeUpdate(TimeupdateType::ePeriodic);
+ }
+
+ const TimeStamp& LastTimeupdateDispatchTime() const;
+ void UpdateLastTimeupdateDispatchTime();
+
+ // WebIDL
+
+ MediaError* GetError() const;
+
+ void GetSrc(nsAString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); }
+ void SetSrc(const nsAString& aSrc, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aError);
+ }
+ void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal,
+ ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aError);
+ }
+
+ void GetCurrentSrc(nsAString& aCurrentSrc);
+
+ void GetCrossOrigin(nsAString& aResult) {
+ // Null for both missing and invalid defaults is ok, since we
+ // always parse to an enum value, so we don't need an invalid
+ // default, and we _want_ the missing default to be null.
+ GetEnumAttr(nsGkAtoms::crossorigin, nullptr, aResult);
+ }
+ void SetCrossOrigin(const nsAString& aCrossOrigin, ErrorResult& aError) {
+ SetOrRemoveNullableStringAttr(nsGkAtoms::crossorigin, aCrossOrigin, aError);
+ }
+
+ uint16_t NetworkState() const { return mNetworkState; }
+
+ void NotifyXPCOMShutdown() final;
+
+ // Called by media decoder when the audible state changed or when input is
+ // a media stream.
+ void SetAudibleState(bool aAudible) final;
+
+ // Notify agent when the MediaElement changes its audible state.
+ void NotifyAudioPlaybackChanged(AudibleChangedReasons aReason);
+
+ void GetPreload(nsAString& aValue) {
+ if (mSrcAttrStream) {
+ nsGkAtoms::none->ToString(aValue);
+ return;
+ }
+ GetEnumAttr(nsGkAtoms::preload, nullptr, aValue);
+ }
+ void SetPreload(const nsAString& aValue, ErrorResult& aRv) {
+ if (mSrcAttrStream) {
+ return;
+ }
+ SetHTMLAttr(nsGkAtoms::preload, aValue, aRv);
+ }
+
+ already_AddRefed<TimeRanges> Buffered() const;
+
+ void Load();
+
+ void CanPlayType(const nsAString& aType, nsAString& aResult);
+
+ uint16_t ReadyState() const { return mReadyState; }
+
+ bool Seeking() const;
+
+ double CurrentTime() const;
+
+ void SetCurrentTime(double aCurrentTime, ErrorResult& aRv);
+ void SetCurrentTime(double aCurrentTime) {
+ SetCurrentTime(aCurrentTime, IgnoreErrors());
+ }
+
+ void FastSeek(double aTime, ErrorResult& aRv);
+
+ already_AddRefed<Promise> SeekToNextFrame(ErrorResult& aRv);
+
+ double Duration() const;
+
+ bool HasAudio() const { return mMediaInfo.HasAudio(); }
+
+ virtual bool IsVideo() const { return false; }
+
+ bool HasVideo() const { return mMediaInfo.HasVideo(); }
+
+ bool IsEncrypted() const override { return mIsEncrypted; }
+
+#ifdef MOZ_WMF_CDM
+ bool IsUsingWMFCDM() const override;
+#endif
+
+ bool Paused() const { return mPaused; }
+
+ double DefaultPlaybackRate() const {
+ if (mSrcAttrStream) {
+ return 1.0;
+ }
+ return mDefaultPlaybackRate;
+ }
+
+ void SetDefaultPlaybackRate(double aDefaultPlaybackRate, ErrorResult& aRv);
+
+ double PlaybackRate() const {
+ if (mSrcAttrStream) {
+ return 1.0;
+ }
+ return mPlaybackRate;
+ }
+
+ void SetPlaybackRate(double aPlaybackRate, ErrorResult& aRv);
+
+ already_AddRefed<TimeRanges> Played();
+
+ already_AddRefed<TimeRanges> Seekable() const;
+
+ bool Ended();
+
+ bool Autoplay() const { return GetBoolAttr(nsGkAtoms::autoplay); }
+
+ void SetAutoplay(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::autoplay, aValue, aRv);
+ }
+
+ bool Loop() const { return GetBoolAttr(nsGkAtoms::loop); }
+
+ void SetLoop(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::loop, aValue, aRv);
+ }
+
+ already_AddRefed<Promise> Play(ErrorResult& aRv);
+ void Play() {
+ IgnoredErrorResult dummy;
+ RefPtr<Promise> toBeIgnored = Play(dummy);
+ }
+
+ void Pause(ErrorResult& aRv);
+ void Pause() { Pause(IgnoreErrors()); }
+
+ bool Controls() const { return GetBoolAttr(nsGkAtoms::controls); }
+
+ void SetControls(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::controls, aValue, aRv);
+ }
+
+ double Volume() const { return mVolume; }
+
+ void SetVolume(double aVolume, ErrorResult& aRv);
+
+ bool Muted() const { return mMuted & MUTED_BY_CONTENT; }
+ void SetMuted(bool aMuted);
+
+ bool DefaultMuted() const { return GetBoolAttr(nsGkAtoms::muted); }
+
+ void SetDefaultMuted(bool aMuted, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::muted, aMuted, aRv);
+ }
+
+ bool MozAllowCasting() const { return mAllowCasting; }
+
+ void SetMozAllowCasting(bool aShow) { mAllowCasting = aShow; }
+
+ bool MozIsCasting() const { return mIsCasting; }
+
+ void SetMozIsCasting(bool aShow) { mIsCasting = aShow; }
+
+ // Returns whether a call to Play() would be rejected with NotAllowedError.
+ // This assumes "worst case" for unknowns. So if prompting for permission is
+ // enabled and no permission is stored, this behaves as if the user would
+ // opt to block.
+ bool AllowedToPlay() const;
+
+ already_AddRefed<MediaSource> GetMozMediaSourceObject() const;
+
+ // Returns a promise which will be resolved after collecting debugging
+ // data from decoder/reader/MDSM. Used for debugging purposes.
+ already_AddRefed<Promise> MozRequestDebugInfo(ErrorResult& aRv);
+
+ // Enables DecoderDoctorLogger logging. Used for debugging purposes.
+ static void MozEnableDebugLog(const GlobalObject&);
+
+ // Returns a promise which will be resolved after collecting debugging
+ // log associated with this element. Used for debugging purposes.
+ already_AddRefed<Promise> MozRequestDebugLog(ErrorResult& aRv);
+
+ // For use by mochitests. Enabling pref "media.test.video-suspend"
+ void SetVisible(bool aVisible);
+
+ // For use by mochitests. Enabling pref "media.test.video-suspend"
+ bool HasSuspendTaint() const;
+
+ // For use by mochitests.
+ bool IsVideoDecodingSuspended() const;
+
+ // These functions return accumulated time, which are used for the telemetry
+ // usage. Return -1 for error.
+ double TotalVideoPlayTime() const;
+ double TotalVideoHDRPlayTime() const;
+ double VisiblePlayTime() const;
+ double InvisiblePlayTime() const;
+ double VideoDecodeSuspendedTime() const;
+ double TotalAudioPlayTime() const;
+ double AudiblePlayTime() const;
+ double InaudiblePlayTime() const;
+ double MutedPlayTime() const;
+
+ // Test methods for decoder doctor.
+ void SetFormatDiagnosticsReportForMimeType(const nsAString& aMimeType,
+ DecoderDoctorReportType aType);
+ void SetDecodeError(const nsAString& aError, ErrorResult& aRv);
+ void SetAudioSinkFailedStartup();
+
+ // Synchronously, return the next video frame and mark the element unable to
+ // participate in decode suspending.
+ //
+ // This function is synchronous for cases where decoding has been suspended
+ // and JS needs a frame to use in, eg., nsLayoutUtils::SurfaceFromElement()
+ // via drawImage().
+ already_AddRefed<layers::Image> GetCurrentImage();
+
+ already_AddRefed<DOMMediaStream> GetSrcObject() const;
+ void SetSrcObject(DOMMediaStream& aValue);
+ void SetSrcObject(DOMMediaStream* aValue);
+
+ bool PreservesPitch() const { return mPreservesPitch; }
+ void SetPreservesPitch(bool aPreservesPitch);
+
+ MediaKeys* GetMediaKeys() const;
+
+ already_AddRefed<Promise> SetMediaKeys(MediaKeys* mediaKeys,
+ ErrorResult& aRv);
+
+ mozilla::dom::EventHandlerNonNull* GetOnencrypted();
+ void SetOnencrypted(mozilla::dom::EventHandlerNonNull* aCallback);
+
+ mozilla::dom::EventHandlerNonNull* GetOnwaitingforkey();
+ void SetOnwaitingforkey(mozilla::dom::EventHandlerNonNull* aCallback);
+
+ void DispatchEncrypted(const nsTArray<uint8_t>& aInitData,
+ const nsAString& aInitDataType) override;
+
+ bool IsEventAttributeNameInternal(nsAtom* aName) override;
+
+ bool ContainsRestrictedContent() const;
+
+ void NotifyWaitingForKey() override;
+
+ already_AddRefed<DOMMediaStream> CaptureAudio(ErrorResult& aRv,
+ MediaTrackGraph* aGraph);
+
+ already_AddRefed<DOMMediaStream> MozCaptureStream(ErrorResult& aRv);
+
+ already_AddRefed<DOMMediaStream> MozCaptureStreamUntilEnded(ErrorResult& aRv);
+
+ bool MozAudioCaptured() const { return mAudioCaptured; }
+
+ void MozGetMetadata(JSContext* aCx, JS::MutableHandle<JSObject*> aResult,
+ ErrorResult& aRv);
+
+ double MozFragmentEnd();
+
+ AudioTrackList* AudioTracks();
+
+ VideoTrackList* VideoTracks();
+
+ TextTrackList* GetTextTracks();
+
+ already_AddRefed<TextTrack> AddTextTrack(TextTrackKind aKind,
+ const nsAString& aLabel,
+ const nsAString& aLanguage);
+
+ void AddTextTrack(TextTrack* aTextTrack) {
+ GetOrCreateTextTrackManager()->AddTextTrack(aTextTrack);
+ }
+
+ void RemoveTextTrack(TextTrack* aTextTrack, bool aPendingListOnly = false) {
+ if (mTextTrackManager) {
+ mTextTrackManager->RemoveTextTrack(aTextTrack, aPendingListOnly);
+ }
+ }
+
+ void NotifyCueAdded(TextTrackCue& aCue) {
+ if (mTextTrackManager) {
+ mTextTrackManager->NotifyCueAdded(aCue);
+ }
+ }
+ void NotifyCueRemoved(TextTrackCue& aCue) {
+ if (mTextTrackManager) {
+ mTextTrackManager->NotifyCueRemoved(aCue);
+ }
+ }
+ void NotifyCueUpdated(TextTrackCue* aCue) {
+ if (mTextTrackManager) {
+ mTextTrackManager->NotifyCueUpdated(aCue);
+ }
+ }
+
+ void NotifyCueDisplayStatesChanged();
+
+ bool IsBlessed() const { return mIsBlessed; }
+
+ // A method to check whether we are currently playing.
+ bool IsCurrentlyPlaying() const;
+
+ // Returns true if the media element is being destroyed. Used in
+ // dormancy checks to prevent dormant processing for an element
+ // that will soon be gone.
+ bool IsBeingDestroyed();
+
+ virtual void OnVisibilityChange(Visibility aNewVisibility);
+
+ // Begin testing only methods
+ float ComputedVolume() const;
+ bool ComputedMuted() const;
+
+ // Return true if the media has been suspended media due to an inactive
+ // document or prohibiting by the docshell.
+ bool IsSuspendedByInactiveDocOrDocShell() const;
+ // End testing only methods
+
+ void SetMediaInfo(const MediaInfo& aInfo);
+ MediaInfo GetMediaInfo() const override;
+
+ // Gives access to the decoder's frame statistics, if present.
+ FrameStatistics* GetFrameStatistics() const override;
+
+ void DispatchAsyncTestingEvent(const nsAString& aName) override;
+
+ AbstractThread* AbstractMainThread() const final;
+
+ // Log the usage of a {visible / invisible} video element as
+ // the source of {drawImage(), createPattern(), createImageBitmap() and
+ // captureStream()} APIs. This function can be used to collect telemetries for
+ // bug 1352007.
+ enum class CallerAPI {
+ DRAW_IMAGE,
+ CREATE_PATTERN,
+ CREATE_IMAGEBITMAP,
+ CAPTURE_STREAM,
+ CREATE_VIDEOFRAME,
+ };
+ void LogVisibility(CallerAPI aAPI);
+
+ Document* GetDocument() const override;
+
+ already_AddRefed<GMPCrashHelper> CreateGMPCrashHelper() override;
+
+ // Set the sink id (of the output device) that the audio will play. If aSinkId
+ // is empty the default device will be set.
+ already_AddRefed<Promise> SetSinkId(const nsAString& aSinkId,
+ ErrorResult& aRv);
+ // Get the sink id of the device that audio is being played. Initial value is
+ // empty and the default device is being used.
+ void GetSinkId(nsString& aSinkId) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ aSinkId = mSink.first;
+ }
+
+ // This is used to notify MediaElementAudioSourceNode that media element is
+ // allowed to play when media element is used as a source for web audio, so
+ // that we can start AudioContext if it was not allowed to start.
+ RefPtr<GenericNonExclusivePromise> GetAllowedToPlayPromise();
+
+ bool GetShowPosterFlag() const { return mShowPoster; }
+
+ bool IsAudible() const;
+
+ // Return key system in use if we have one, otherwise return nothing.
+ Maybe<nsAutoString> GetKeySystem() const override;
+
+ protected:
+ virtual ~HTMLMediaElement();
+
+ class AudioChannelAgentCallback;
+ class ChannelLoader;
+ class ErrorSink;
+ class MediaElementTrackSource;
+ class MediaLoadListener;
+ class MediaStreamRenderer;
+ class MediaStreamTrackListener;
+ class ShutdownObserver;
+ class TitleChangeObserver;
+ class MediaControlKeyListener;
+
+ MediaDecoderOwner::NextFrameStatus NextFrameStatus();
+
+ void SetDecoder(MediaDecoder* aDecoder);
+
+ void PlayInternal(bool aHandlingUserInput);
+
+ // See spec, https://html.spec.whatwg.org/#internal-pause-steps
+ void PauseInternal();
+
+ /** Use this method to change the mReadyState member, so required
+ * events can be fired.
+ */
+ void ChangeReadyState(nsMediaReadyState aState);
+
+ /**
+ * Use this method to change the mNetworkState member, so required
+ * actions will be taken during the transition.
+ */
+ void ChangeNetworkState(nsMediaNetworkState aState);
+
+ /**
+ * The MediaElement will be responsible for creating and releasing the audio
+ * wakelock depending on the playing and audible state.
+ */
+ virtual void WakeLockRelease();
+ virtual void UpdateWakeLock();
+
+ void CreateAudioWakeLockIfNeeded();
+ void ReleaseAudioWakeLockIfExists();
+ RefPtr<WakeLock> mWakeLock;
+
+ /**
+ * Logs a warning message to the web console to report various failures.
+ * aMsg is the localized message identifier, aParams is the parameters to
+ * be substituted into the localized message, and aParamCount is the number
+ * of parameters in aParams.
+ */
+ void ReportLoadError(const char* aMsg, const nsTArray<nsString>& aParams =
+ nsTArray<nsString>());
+
+ /**
+ * Log message to web console.
+ */
+ void ReportToConsole(
+ uint32_t aErrorFlags, const char* aMsg,
+ const nsTArray<nsString>& aParams = nsTArray<nsString>()) const;
+
+ /**
+ * Changes mHasPlayedOrSeeked to aValue. If mHasPlayedOrSeeked changes
+ * we'll force a reflow so that the video frame gets reflowed to reflect
+ * the poster hiding or showing immediately.
+ */
+ void SetPlayedOrSeeked(bool aValue);
+
+ /**
+ * Initialize the media element for playback of aStream
+ */
+ void SetupSrcMediaStreamPlayback(DOMMediaStream* aStream);
+ /**
+ * Stop playback on mSrcStream.
+ */
+ void EndSrcMediaStreamPlayback();
+ /**
+ * Ensure we're playing mSrcStream if and only if we're not paused.
+ */
+ enum { REMOVING_SRC_STREAM = 0x1 };
+ void UpdateSrcMediaStreamPlaying(uint32_t aFlags = 0);
+
+ /**
+ * Ensure currentTime progresses if and only if we're potentially playing
+ * mSrcStream. Called by the watch manager while we're playing mSrcStream, and
+ * one of the inputs to the potentially playing algorithm changes.
+ */
+ void UpdateSrcStreamPotentiallyPlaying();
+
+ /**
+ * mSrcStream's graph's CurrentTime() has been updated. It might be time to
+ * fire "timeupdate".
+ */
+ void UpdateSrcStreamTime();
+
+ /**
+ * Called after a tail dispatch when playback of mSrcStream ended, to comply
+ * with the spec where we must start reporting true for the ended attribute
+ * after the event loop returns to step 1. A MediaStream could otherwise be
+ * manipulated to end a HTMLMediaElement synchronously.
+ */
+ void UpdateSrcStreamReportPlaybackEnded();
+
+ /**
+ * Called by our DOMMediaStream::TrackListener when a new MediaStreamTrack has
+ * been added to the playback stream of |mSrcStream|.
+ */
+ void NotifyMediaStreamTrackAdded(const RefPtr<MediaStreamTrack>& aTrack);
+
+ /**
+ * Called by our DOMMediaStream::TrackListener when a MediaStreamTrack in
+ * |mSrcStream|'s playback stream has ended.
+ */
+ void NotifyMediaStreamTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack);
+
+ /**
+ * Convenience method to get in a single list all enabled AudioTracks and, if
+ * this is a video element, the selected VideoTrack.
+ */
+ void GetAllEnabledMediaTracks(nsTArray<RefPtr<MediaTrack>>& aTracks);
+
+ /**
+ * Enables or disables all tracks forwarded from mSrcStream to all
+ * OutputMediaStreams. We do this for muting the tracks when pausing,
+ * and unmuting when playing the media element again.
+ */
+ void SetCapturedOutputStreamsEnabled(bool aEnabled);
+
+ /**
+ * Returns true if output tracks should be muted, based on the state of this
+ * media element.
+ */
+ enum class OutputMuteState { Muted, Unmuted };
+ OutputMuteState OutputTracksMuted();
+
+ /**
+ * Sets the muted state of all output track sources. They are muted when we're
+ * paused and unmuted otherwise.
+ */
+ void UpdateOutputTracksMuting();
+
+ /**
+ * Create a new MediaStreamTrack for the TrackSource corresponding to aTrack
+ * and add it to the DOMMediaStream in aOutputStream. This automatically sets
+ * the output track to enabled or disabled depending on our current playing
+ * state.
+ */
+ enum class AddTrackMode { ASYNC, SYNC };
+ void AddOutputTrackSourceToOutputStream(
+ MediaElementTrackSource* aSource, OutputMediaStream& aOutputStream,
+ AddTrackMode aMode = AddTrackMode::ASYNC);
+
+ /**
+ * Creates output track sources when this media element is captured, tracks
+ * exist, playback is not ended and readyState is >= HAVE_METADATA.
+ */
+ void UpdateOutputTrackSources();
+
+ /**
+ * Returns an DOMMediaStream containing the played contents of this
+ * element. When aBehavior is FINISH_WHEN_ENDED, when this element ends
+ * playback we will finish the stream and not play any more into it. When
+ * aType is CONTINUE_WHEN_ENDED, ending playback does not finish the stream.
+ * The stream will never finish.
+ *
+ * When aType is CAPTURE_AUDIO, we stop playout of audio and instead route it
+ * to the DOMMediaStream. Volume and mute state will be applied to the audio
+ * reaching the stream. No video tracks will be captured in this case.
+ *
+ * aGraph may be null if the stream's tracks do not need to use a
+ * specific graph.
+ */
+ already_AddRefed<DOMMediaStream> CaptureStreamInternal(
+ StreamCaptureBehavior aFinishBehavior,
+ StreamCaptureType aStreamCaptureType, MediaTrackGraph* aGraph);
+
+ /**
+ * Initialize a decoder as a clone of an existing decoder in another
+ * element.
+ * mLoadingSrc must already be set.
+ */
+ nsresult InitializeDecoderAsClone(ChannelMediaDecoder* aOriginal);
+
+ /**
+ * Call Load() and FinishDecoderSetup() on the decoder. It also handle
+ * resource cloning if DecoderType is ChannelMediaDecoder.
+ */
+ template <typename DecoderType, typename... LoadArgs>
+ nsresult SetupDecoder(DecoderType* aDecoder, LoadArgs&&... aArgs);
+
+ /**
+ * Initialize a decoder to load the given channel. The decoder's stream
+ * listener is returned via aListener.
+ * mLoadingSrc must already be set.
+ */
+ nsresult InitializeDecoderForChannel(nsIChannel* aChannel,
+ nsIStreamListener** aListener);
+
+ /**
+ * Finish setting up the decoder after Load() has been called on it.
+ * Called by InitializeDecoderForChannel/InitializeDecoderAsClone.
+ */
+ nsresult FinishDecoderSetup(MediaDecoder* aDecoder);
+
+ /**
+ * Call this after setting up mLoadingSrc and mDecoder.
+ */
+ void AddMediaElementToURITable();
+ /**
+ * Call this before modifying mLoadingSrc.
+ */
+ void RemoveMediaElementFromURITable();
+ /**
+ * Call this to find a media element with the same NodePrincipal and
+ * mLoadingSrc set to aURI, and with a decoder on which Load() has been
+ * called.
+ */
+ HTMLMediaElement* LookupMediaElementURITable(nsIURI* aURI);
+
+ /**
+ * Shutdown and clear mDecoder and maintain associated invariants.
+ */
+ void ShutdownDecoder();
+ /**
+ * Execute the initial steps of the load algorithm that ensure existing
+ * loads are aborted, the element is emptied, and a new load ID is
+ * created.
+ */
+ void AbortExistingLoads();
+
+ /**
+ * This is the dedicated media source failure steps.
+ * Called when all potential resources are exhausted. Changes network
+ * state to NETWORK_NO_SOURCE, and sends error event with code
+ * MEDIA_ERR_SRC_NOT_SUPPORTED.
+ */
+ void NoSupportedMediaSourceError(
+ const nsACString& aErrorDetails = nsCString());
+
+ /**
+ * Per spec, Failed with elements: Queue a task, using the DOM manipulation
+ * task source, to fire a simple event named error at the candidate element.
+ * So dispatch |QueueLoadFromSourceTask| to main thread to make sure the task
+ * will be executed later than loadstart event.
+ */
+ void DealWithFailedElement(nsIContent* aSourceElement);
+
+ /**
+ * Attempts to load resources from the <source> children. This is a
+ * substep of the resource selection algorithm. Do not call this directly,
+ * call QueueLoadFromSourceTask() instead.
+ */
+ void LoadFromSourceChildren();
+
+ /**
+ * Asynchronously awaits a stable state, and then causes
+ * LoadFromSourceChildren() to be called on the main threads' event loop.
+ */
+ void QueueLoadFromSourceTask();
+
+ /**
+ * Runs the media resource selection algorithm.
+ */
+ void SelectResource();
+
+ /**
+ * A wrapper function that allows us to cleanly reset flags after a call
+ * to SelectResource()
+ */
+ void SelectResourceWrapper();
+
+ /**
+ * Asynchronously awaits a stable state, and then causes SelectResource()
+ * to be run on the main thread's event loop.
+ */
+ void QueueSelectResourceTask();
+
+ /**
+ * When loading a new source on an existing media element, make sure to reset
+ * everything that is accessible using the media element API.
+ */
+ void ResetState();
+
+ /**
+ * The resource-fetch algorithm step of the load algorithm.
+ */
+ MediaResult LoadResource();
+
+ /**
+ * Selects the next <source> child from which to load a resource. Called
+ * during the resource selection algorithm. Stores the return value in
+ * mSourceLoadCandidate before returning.
+ */
+ HTMLSourceElement* GetNextSource();
+
+ /**
+ * Changes mDelayingLoadEvent, and will call BlockOnLoad()/UnblockOnLoad()
+ * on the owning document, so it can delay the load event firing.
+ */
+ void ChangeDelayLoadStatus(bool aDelay);
+
+ /**
+ * If we suspended downloading after the first frame, unsuspend now.
+ */
+ void StopSuspendingAfterFirstFrame();
+
+ /**
+ * Called when our channel is redirected to another channel.
+ * Updates our mChannel reference to aNewChannel.
+ */
+ nsresult OnChannelRedirect(nsIChannel* aChannel, nsIChannel* aNewChannel,
+ uint32_t aFlags);
+
+ /**
+ * Call this to reevaluate whether we should be holding a self-reference.
+ */
+ void AddRemoveSelfReference();
+
+ /**
+ * Called when "xpcom-shutdown" event is received.
+ */
+ void NotifyShutdownEvent();
+
+ /**
+ * Possible values of the 'preload' attribute.
+ */
+ enum PreloadAttrValue : uint8_t {
+ PRELOAD_ATTR_EMPTY, // set to ""
+ PRELOAD_ATTR_NONE, // set to "none"
+ PRELOAD_ATTR_METADATA, // set to "metadata"
+ PRELOAD_ATTR_AUTO // set to "auto"
+ };
+
+ /**
+ * The preloading action to perform. These dictate how we react to the
+ * preload attribute. See mPreloadAction.
+ */
+ enum PreloadAction {
+ PRELOAD_UNDEFINED = 0, // not determined - used only for initialization
+ PRELOAD_NONE = 1, // do not preload
+ PRELOAD_METADATA = 2, // preload only the metadata (and first frame)
+ PRELOAD_ENOUGH = 3 // preload enough data to allow uninterrupted
+ // playback
+ };
+
+ /**
+ * The guts of Load(). Load() acts as a wrapper around this which sets
+ * mIsDoingExplicitLoad to true so that when script calls 'load()'
+ * preload-none will be automatically upgraded to preload-metadata.
+ */
+ void DoLoad();
+
+ /**
+ * Suspends the load of mLoadingSrc, so that it can be resumed later
+ * by ResumeLoad(). This is called when we have a media with a 'preload'
+ * attribute value of 'none', during the resource selection algorithm.
+ */
+ void SuspendLoad();
+
+ /**
+ * Resumes a previously suspended load (suspended by SuspendLoad(uri)).
+ * Will continue running the resource selection algorithm.
+ * Sets mPreloadAction to aAction.
+ */
+ void ResumeLoad(PreloadAction aAction);
+
+ /**
+ * Handle a change to the preload attribute. Should be called whenever the
+ * value (or presence) of the preload attribute changes. The change in
+ * attribute value may cause a change in the mPreloadAction of this
+ * element. If there is a change then this method will initiate any
+ * behaviour that is necessary to implement the action.
+ */
+ void UpdatePreloadAction();
+
+ /**
+ * Fire progress events if needed according to the time and byte constraints
+ * outlined in the specification. aHaveNewProgress is true if progress has
+ * just been detected. Otherwise the method is called as a result of the
+ * progress timer.
+ */
+ void CheckProgress(bool aHaveNewProgress);
+ static void ProgressTimerCallback(nsITimer* aTimer, void* aClosure);
+ /**
+ * Start timer to update download progress.
+ */
+ void StartProgressTimer();
+ /**
+ * Start sending progress and/or stalled events.
+ */
+ void StartProgress();
+ /**
+ * Stop progress information timer and events.
+ */
+ void StopProgress();
+
+ /**
+ * Dispatches an error event to a child source element.
+ */
+ void DispatchAsyncSourceError(nsIContent* aSourceElement);
+
+ /**
+ * Resets the media element for an error condition as per aErrorCode.
+ * aErrorCode must be one of WebIDL HTMLMediaElement error codes.
+ */
+ void Error(uint16_t aErrorCode,
+ const nsACString& aErrorDetails = nsCString());
+
+ /**
+ * Returns the URL spec of the currentSrc.
+ **/
+ void GetCurrentSpec(nsCString& aString);
+
+ /**
+ * Process any media fragment entries in the URI
+ */
+ void ProcessMediaFragmentURI();
+
+ /**
+ * Mute or unmute the audio and change the value that the |muted| map.
+ */
+ void SetMutedInternal(uint32_t aMuted);
+ /**
+ * Update the volume of the output audio stream to match the element's
+ * current mMuted/mVolume/mAudioChannelFaded state.
+ */
+ void SetVolumeInternal();
+
+ /**
+ * Suspend or resume element playback and resource download. When we suspend
+ * playback, event delivery would also be suspended (and events queued) until
+ * the element is resumed.
+ */
+ void SuspendOrResumeElement(bool aSuspendElement);
+
+ // Get the HTMLMediaElement object if the decoder is being used from an
+ // HTML media element, and null otherwise.
+ HTMLMediaElement* GetMediaElement() final { return this; }
+
+ // Return true if decoding should be paused
+ bool GetPaused() final { return Paused(); }
+
+ // Seeks to aTime seconds. aSeekType can be Exact to seek to exactly the
+ // seek target, or PrevSyncPoint if a quicker but less precise seek is
+ // desired, and we'll seek to the sync point (keyframe and/or start of the
+ // next block of audio samples) preceeding seek target.
+ void Seek(double aTime, SeekTarget::Type aSeekType, ErrorResult& aRv);
+
+ // Update the audio channel playing state
+ void UpdateAudioChannelPlayingState();
+
+ // Adds to the element's list of pending text tracks each text track
+ // in the element's list of text tracks whose text track mode is not disabled
+ // and whose text track readiness state is loading.
+ void PopulatePendingTextTrackList();
+
+ // Gets a reference to the MediaElement's TextTrackManager. If the
+ // MediaElement doesn't yet have one then it will create it.
+ TextTrackManager* GetOrCreateTextTrackManager();
+
+ // Recomputes ready state and fires events as necessary based on current
+ // state.
+ void UpdateReadyStateInternal();
+
+ // Create or destroy the captured stream.
+ void AudioCaptureTrackChange(bool aCapture);
+
+ // If the network state is empty and then we would trigger DoLoad().
+ void MaybeDoLoad();
+
+ // Anything we need to check after played success and not related with spec.
+ void UpdateCustomPolicyAfterPlayed();
+
+ // Returns a StreamCaptureType populated with the right bits, depending on the
+ // tracks this HTMLMediaElement has.
+ StreamCaptureType CaptureTypeForElement();
+
+ // True if this element can be captured, false otherwise.
+ bool CanBeCaptured(StreamCaptureType aCaptureType);
+
+ using nsGenericHTMLElement::DispatchEvent;
+ // For nsAsyncEventRunner.
+ nsresult DispatchEvent(const nsAString& aName);
+
+ already_AddRefed<nsMediaEventRunner> GetEventRunner(
+ const nsAString& aName, EventFlag aFlag = EventFlag::eNone);
+
+ // This method moves the mPendingPlayPromises into a temperate object. So the
+ // mPendingPlayPromises is cleared after this method call.
+ nsTArray<RefPtr<PlayPromise>> TakePendingPlayPromises();
+
+ // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises()
+ // and queues a task to resolve them.
+ void AsyncResolvePendingPlayPromises();
+
+ // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises()
+ // and queues a task to reject them.
+ void AsyncRejectPendingPlayPromises(nsresult aError);
+
+ // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises()
+ // and queues a task to resolve them also to dispatch a "playing" event.
+ void NotifyAboutPlaying();
+
+ already_AddRefed<Promise> CreateDOMPromise(ErrorResult& aRv) const;
+
+ // Pass information for deciding the video decode mode to decoder.
+ void NotifyDecoderActivityChanges() const;
+
+ // Constructs an AudioTrack in mAudioTrackList if aInfo reports that audio is
+ // available, and a VideoTrack in mVideoTrackList if aInfo reports that video
+ // is available.
+ void ConstructMediaTracks(const MediaInfo* aInfo);
+
+ // Removes all MediaTracks from mAudioTrackList and mVideoTrackList and fires
+ // "removetrack" on the lists accordingly.
+ // Note that by spec, this should not fire "removetrack". However, it appears
+ // other user agents do, per
+ // https://wpt.fyi/results/media-source/mediasource-avtracks.html.
+ void RemoveMediaTracks();
+
+ // Mark the decoder owned by the element as tainted so that the
+ // suspend-video-decoder is disabled.
+ void MarkAsTainted();
+
+ virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+ virtual void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) override;
+
+ bool DetachExistingMediaKeys();
+ bool TryRemoveMediaKeysAssociation();
+ void RemoveMediaKeys();
+ bool AttachNewMediaKeys();
+ bool TryMakeAssociationWithCDM(CDMProxy* aProxy);
+ void MakeAssociationWithCDMResolved();
+ void SetCDMProxyFailure(const MediaResult& aResult);
+ void ResetSetMediaKeysTempVariables();
+
+ void PauseIfShouldNotBePlaying();
+
+ WatchManager<HTMLMediaElement> mWatchManager;
+
+ // When the play is not allowed, dispatch related events which are used for
+ // testing or changing control UI.
+ void DispatchEventsWhenPlayWasNotAllowed();
+
+ // When the doc is blocked permanantly, we would dispatch event to notify
+ // front-end side to show blocking icon.
+ void MaybeNotifyAutoplayBlocked();
+
+ // Dispatch event for video control when video gets blocked in order to show
+ // the click-to-play icon.
+ void DispatchBlockEventForVideoControl();
+
+ // When playing state change, we have to notify MediaControl in the chrome
+ // process in order to keep its playing state correct.
+ void NotifyMediaControlPlaybackStateChanged();
+
+ // Clear the timer when we want to continue listening to the media control
+ // key events.
+ void ClearStopMediaControlTimerIfNeeded();
+
+ // Sets a secondary renderer for mSrcStream, so this media element can be
+ // rendered in Picture-in-Picture mode when playing a MediaStream. A null
+ // aContainer will unset the secondary renderer. aFirstFrameOutput allows
+ // for injecting a listener of the callers choice for rendering the first
+ // frame.
+ void SetSecondaryMediaStreamRenderer(
+ VideoFrameContainer* aContainer,
+ FirstFrameVideoOutput* aFirstFrameOutput = nullptr);
+
+ // This function is used to update the status of media control when the media
+ // changes its status of being used in the Picture-in-Picture mode.
+ void UpdateMediaControlAfterPictureInPictureModeChanged();
+
+ // The current decoder. Load() has been called on this decoder.
+ // At most one of mDecoder and mSrcStream can be non-null.
+ RefPtr<MediaDecoder> mDecoder;
+
+ // A reference to the VideoFrameContainer which contains the current frame
+ // of video to display.
+ RefPtr<VideoFrameContainer> mVideoFrameContainer;
+
+ // Holds a reference to the MediaStream that has been set in the src
+ // attribute.
+ RefPtr<DOMMediaStream> mSrcAttrStream;
+
+ // Holds the triggering principal for the src attribute.
+ nsCOMPtr<nsIPrincipal> mSrcAttrTriggeringPrincipal;
+
+ // Holds a reference to the MediaStream that we're actually playing.
+ // At most one of mDecoder and mSrcStream can be non-null.
+ RefPtr<DOMMediaStream> mSrcStream;
+
+ // The MediaStreamRenderer handles rendering of our selected video track, and
+ // enabled audio tracks, while mSrcStream is set.
+ RefPtr<MediaStreamRenderer> mMediaStreamRenderer;
+
+ // The secondary MediaStreamRenderer handles rendering of our selected video
+ // track to a secondary VideoFrameContainer, while mSrcStream is set.
+ RefPtr<MediaStreamRenderer> mSecondaryMediaStreamRenderer;
+
+ // True once PlaybackEnded() is called and we're playing a MediaStream.
+ // Reset to false if we start playing mSrcStream again.
+ Watchable<bool> mSrcStreamPlaybackEnded = {
+ false, "HTMLMediaElement::mSrcStreamPlaybackEnded"};
+
+ // Mirrors mSrcStreamPlaybackEnded after a tail dispatch when set to true,
+ // but may be be forced to false directly. To accomodate when an application
+ // ends playback synchronously by manipulating mSrcStream or its tracks,
+ // e.g., through MediaStream.removeTrack(), or MediaStreamTrack.stop().
+ bool mSrcStreamReportPlaybackEnded = false;
+
+ // Holds a reference to the stream connecting this stream to the window
+ // capture sink.
+ UniquePtr<MediaStreamWindowCapturer> mStreamWindowCapturer;
+
+ // Holds references to the DOM wrappers for the MediaStreams that we're
+ // writing to.
+ nsTArray<OutputMediaStream> mOutputStreams;
+
+ // Mapping for output tracks, from dom::MediaTrack ids to the
+ // MediaElementTrackSource that represents the source of all corresponding
+ // MediaStreamTracks captured from this element.
+ nsRefPtrHashtable<nsStringHashKey, MediaElementTrackSource>
+ mOutputTrackSources;
+
+ // The currently selected video stream track.
+ RefPtr<VideoStreamTrack> mSelectedVideoStreamTrack;
+
+ const RefPtr<ShutdownObserver> mShutdownObserver;
+
+ const RefPtr<TitleChangeObserver> mTitleChangeObserver;
+
+ // Holds a reference to the MediaSource, if any, referenced by the src
+ // attribute on the media element.
+ RefPtr<MediaSource> mSrcMediaSource;
+
+ // Holds a reference to the MediaSource supplying data for playback. This
+ // may either match mSrcMediaSource or come from Source element children.
+ // This is set when and only when mLoadingSrc corresponds to an object url
+ // that resolved to a MediaSource.
+ RefPtr<MediaSource> mMediaSource;
+
+ RefPtr<ChannelLoader> mChannelLoader;
+
+ // Points to the child source elements, used to iterate through the children
+ // when selecting a resource to load. This is the previous sibling of the
+ // child considered the current 'candidate' in:
+ // https://html.spec.whatwg.org/multipage/media.html#concept-media-load-algorithm
+ //
+ // mSourcePointer == nullptr, we will next try to load |GetFirstChild()|.
+ // mSourcePointer == GetLastChild(), we've exhausted all sources, waiting
+ // for new elements to be appended.
+ nsCOMPtr<nsIContent> mSourcePointer;
+
+ // Points to the document whose load we're blocking. This is the document
+ // we're bound to when loading starts.
+ nsCOMPtr<Document> mLoadBlockedDoc;
+
+ // This is used to help us block/resume the event delivery.
+ class EventBlocker;
+ RefPtr<EventBlocker> mEventBlocker;
+
+ // Media loading flags. See:
+ // http://www.whatwg.org/specs/web-apps/current-work/#video)
+ nsMediaNetworkState mNetworkState = HTMLMediaElement_Binding::NETWORK_EMPTY;
+ Watchable<nsMediaReadyState> mReadyState = {
+ HTMLMediaElement_Binding::HAVE_NOTHING, "HTMLMediaElement::mReadyState"};
+
+ enum LoadAlgorithmState {
+ // No load algorithm instance is waiting for a source to be added to the
+ // media in order to continue loading.
+ NOT_WAITING,
+ // We've run the load algorithm, and we tried all source children of the
+ // media element, and failed to load any successfully. We're waiting for
+ // another source element to be added to the media element, and will try
+ // to load any such element when its added.
+ WAITING_FOR_SOURCE
+ };
+
+ // The current media load ID. This is incremented every time we start a
+ // new load. Async events note the ID when they're first sent, and only fire
+ // if the ID is unchanged when they come to fire.
+ uint32_t mCurrentLoadID = 0;
+
+ // Denotes the waiting state of a load algorithm instance. When the load
+ // algorithm is waiting for a source element child to be added, this is set
+ // to WAITING_FOR_SOURCE, otherwise it's NOT_WAITING.
+ LoadAlgorithmState mLoadWaitStatus = NOT_WAITING;
+
+ // Current audio volume
+ double mVolume = 1.0;
+
+ // True if the audio track is not silent.
+ bool mIsAudioTrackAudible = false;
+
+ enum MutedReasons {
+ MUTED_BY_CONTENT = 0x01,
+ MUTED_BY_INVALID_PLAYBACK_RATE = 0x02,
+ MUTED_BY_AUDIO_CHANNEL = 0x04,
+ MUTED_BY_AUDIO_TRACK = 0x08
+ };
+
+ uint32_t mMuted = 0;
+
+ UniquePtr<const MetadataTags> mTags;
+
+ // URI of the resource we're attempting to load. This stores the value we
+ // return in the currentSrc attribute. Use GetCurrentSrc() to access the
+ // currentSrc attribute.
+ // This is always the original URL we're trying to load --- before
+ // redirects etc.
+ nsCOMPtr<nsIURI> mLoadingSrc;
+
+ // The triggering principal for the current source.
+ nsCOMPtr<nsIPrincipal> mLoadingSrcTriggeringPrincipal;
+
+ // Stores the current preload action for this element. Initially set to
+ // PRELOAD_UNDEFINED, its value is changed by calling
+ // UpdatePreloadAction().
+ PreloadAction mPreloadAction = PRELOAD_UNDEFINED;
+
+ // Time that the last timeupdate event was queued. Read/Write from the
+ // main thread only.
+ TimeStamp mQueueTimeUpdateRunnerTime;
+
+ // Time that the last timeupdate event was fired. Read/Write from the
+ // main thread only.
+ TimeStamp mLastTimeUpdateDispatchTime;
+
+ // Time that the last progress event was fired. Read/Write from the
+ // main thread only.
+ TimeStamp mProgressTime;
+
+ // Time that data was last read from the media resource. Used for
+ // computing if the download has stalled and to rate limit progress events
+ // when data is arriving slower than PROGRESS_MS.
+ // Read/Write from the main thread only.
+ TimeStamp mDataTime;
+
+ // Media 'currentTime' value when the last timeupdate event was queued.
+ // Read/Write from the main thread only.
+ double mLastCurrentTime = 0.0;
+
+ // Logical start time of the media resource in seconds as obtained
+ // from any media fragments. A negative value indicates that no
+ // fragment time has been set. Read/Write from the main thread only.
+ double mFragmentStart = -1.0;
+
+ // Logical end time of the media resource in seconds as obtained
+ // from any media fragments. A negative value indicates that no
+ // fragment time has been set. Read/Write from the main thread only.
+ double mFragmentEnd = -1.0;
+
+ // The defaultPlaybackRate attribute gives the desired speed at which the
+ // media resource is to play, as a multiple of its intrinsic speed.
+ double mDefaultPlaybackRate = 1.0;
+
+ // The playbackRate attribute gives the speed at which the media resource
+ // plays, as a multiple of its intrinsic speed. If it is not equal to the
+ // defaultPlaybackRate, then the implication is that the user is using a
+ // feature such as fast forward or slow motion playback.
+ double mPlaybackRate = 1.0;
+
+ // True if pitch correction is applied when playbackRate is set to a
+ // non-intrinsic value.
+ bool mPreservesPitch = true;
+
+ // Reference to the source element last returned by GetNextSource().
+ // This is the child source element which we're trying to load from.
+ nsCOMPtr<nsIContent> mSourceLoadCandidate;
+
+ // Range of time played.
+ RefPtr<TimeRanges> mPlayed;
+
+ // Timer used for updating progress events.
+ nsCOMPtr<nsITimer> mProgressTimer;
+
+ // Encrypted Media Extension media keys.
+ RefPtr<MediaKeys> mMediaKeys;
+ RefPtr<MediaKeys> mIncomingMediaKeys;
+ // The dom promise is used for HTMLMediaElement::SetMediaKeys.
+ RefPtr<DetailedPromise> mSetMediaKeysDOMPromise;
+ // Used to indicate if the MediaKeys attaching operation is on-going or not.
+ bool mAttachingMediaKey = false;
+ MozPromiseRequestHolder<SetCDMPromise> mSetCDMRequest;
+
+ // Stores the time at the start of the current 'played' range.
+ double mCurrentPlayRangeStart = 1.0;
+
+ // True if loadeddata has been fired.
+ bool mLoadedDataFired = false;
+
+ // One of the factors determines whether a media element with 'autoplay'
+ // attribute is allowed to start playing.
+ // https://html.spec.whatwg.org/multipage/media.html#can-autoplay-flag
+ bool mCanAutoplayFlag = true;
+
+ // Playback of the video is paused either due to calling the
+ // 'Pause' method, or playback not yet having started.
+ Watchable<bool> mPaused = {true, "HTMLMediaElement::mPaused"};
+
+ // The following two fields are here for the private storage of the builtin
+ // video controls, and control 'casting' of the video to external devices
+ // (TVs, projectors etc.)
+ // True if casting is currently allowed
+ bool mAllowCasting = false;
+ // True if currently casting this video
+ bool mIsCasting = false;
+
+ // Set while there are some OutputMediaStreams this media element's enabled
+ // and selected tracks are captured into. When set, all tracks are captured
+ // into the graph of this dummy track.
+ // NB: This is a SharedDummyTrack to allow non-default graphs (AudioContexts
+ // with an explicit sampleRate defined) to capture this element. When
+ // cross-graph tracks are supported, this can become a bool.
+ Watchable<RefPtr<SharedDummyTrack>> mTracksCaptured;
+
+ // True if the sound is being captured.
+ bool mAudioCaptured = false;
+
+ // If TRUE then the media element was actively playing before the currently
+ // in progress seeking. If FALSE then the media element is either not seeking
+ // or was not actively playing before the current seek. Used to decide whether
+ // to raise the 'waiting' event as per 4.7.1.8 in HTML 5 specification.
+ bool mPlayingBeforeSeek = false;
+
+ // True if this element is suspended because the document is inactive or the
+ // inactive docshell is not allowing media to play.
+ bool mSuspendedByInactiveDocOrDocshell = false;
+
+ // True if we're running the "load()" method.
+ bool mIsRunningLoadMethod = false;
+
+ // True if we're running or waiting to run queued tasks due to an explicit
+ // call to "load()".
+ bool mIsDoingExplicitLoad = false;
+
+ // True if we're loading the resource from the child source elements.
+ bool mIsLoadingFromSourceChildren = false;
+
+ // True if we're delaying the "load" event. They are delayed until either
+ // an error occurs, or the first frame is loaded.
+ bool mDelayingLoadEvent = false;
+
+ // True when we've got a task queued to call SelectResource(),
+ // or while we're running SelectResource().
+ bool mIsRunningSelectResource = false;
+
+ // True when we already have select resource call queued
+ bool mHaveQueuedSelectResource = false;
+
+ // True if we suspended the decoder because we were paused,
+ // preloading metadata is enabled, autoplay was not enabled, and we loaded
+ // the first frame.
+ bool mSuspendedAfterFirstFrame = false;
+
+ // True if we are allowed to suspend the decoder because we were paused,
+ // preloading metdata was enabled, autoplay was not enabled, and we loaded
+ // the first frame.
+ bool mAllowSuspendAfterFirstFrame = true;
+
+ // True if we've played or completed a seek. We use this to determine
+ // when the poster frame should be shown.
+ bool mHasPlayedOrSeeked = false;
+
+ // True if we've added a reference to ourselves to keep the element
+ // alive while no-one is referencing it but the element may still fire
+ // events of its own accord.
+ bool mHasSelfReference = false;
+
+ // True if we've received a notification that the engine is shutting
+ // down.
+ bool mShuttingDown = false;
+
+ // True if we've suspended a load in the resource selection algorithm
+ // due to loading a preload:none media. When true, the resource we'll
+ // load when the user initiates either playback or an explicit load is
+ // stored in mPreloadURI.
+ bool mSuspendedForPreloadNone = false;
+
+ // True if we've connected mSrcStream to the media element output.
+ bool mSrcStreamIsPlaying = false;
+
+ // True if we should set nsIClassOfService::UrgentStart to the channel to
+ // get the response ASAP for better user responsiveness.
+ bool mUseUrgentStartForChannel = false;
+
+ // The CORS mode when loading the media element
+ CORSMode mCORSMode = CORS_NONE;
+
+ // Info about the played media.
+ MediaInfo mMediaInfo;
+
+ // True if the media has encryption information.
+ bool mIsEncrypted = false;
+
+ enum WaitingForKeyState {
+ NOT_WAITING_FOR_KEY = 0,
+ WAITING_FOR_KEY = 1,
+ WAITING_FOR_KEY_DISPATCHED = 2
+ };
+
+ // True when the CDM cannot decrypt the current block due to lacking a key.
+ // Note: the "waitingforkey" event is not dispatched until all decoded data
+ // has been rendered.
+ WaitingForKeyState mWaitingForKey = NOT_WAITING_FOR_KEY;
+
+ // Listens for waitingForKey events from the owned decoder.
+ MediaEventListener mWaitingForKeyListener;
+
+ // Init Data that needs to be sent in 'encrypted' events in MetadataLoaded().
+ EncryptionInfo mPendingEncryptedInitData;
+
+ // True if the media's channel's download has been suspended.
+ Watchable<bool> mDownloadSuspendedByCache = {
+ false, "HTMLMediaElement::mDownloadSuspendedByCache"};
+
+ // Disable the video playback by track selection. This flag might not be
+ // enough if we ever expand the ability of supporting multi-tracks video
+ // playback.
+ bool mDisableVideo = false;
+
+ RefPtr<TextTrackManager> mTextTrackManager;
+
+ RefPtr<AudioTrackList> mAudioTrackList;
+
+ RefPtr<VideoTrackList> mVideoTrackList;
+
+ UniquePtr<MediaStreamTrackListener> mMediaStreamTrackListener;
+
+ // The principal guarding mVideoFrameContainer access when playing a
+ // MediaStream.
+ nsCOMPtr<nsIPrincipal> mSrcStreamVideoPrincipal;
+
+ // True if the autoplay media was blocked because it hadn't loaded metadata
+ // yet.
+ bool mBlockedAsWithoutMetadata = false;
+
+ // This promise is used to notify MediaElementAudioSourceNode that media
+ // element is allowed to play when MediaElement is used as a source for web
+ // audio.
+ MozPromiseHolder<GenericNonExclusivePromise> mAllowedToPlayPromise;
+
+ // True if media has ever been blocked for autoplay, it's used to notify front
+ // end to show the correct blocking icon when the document goes back from
+ // bfcache.
+ bool mHasEverBeenBlockedForAutoplay = false;
+
+ // True if we have dispatched a task for text track changed, will be unset
+ // when we starts processing text track changed.
+ // https://html.spec.whatwg.org/multipage/media.html#pending-text-track-change-notification-flag
+ bool mPendingTextTrackChanged = false;
+
+ public:
+ // This function will be called whenever a text track that is in a media
+ // element's list of text tracks has its text track mode change value
+ void NotifyTextTrackModeChanged();
+
+ private:
+ friend class nsMediaEventRunner;
+ friend class nsResolveOrRejectPendingPlayPromisesRunner;
+
+ already_AddRefed<PlayPromise> CreatePlayPromise(ErrorResult& aRv) const;
+
+ virtual void MaybeBeginCloningVisually(){};
+
+ uint32_t GetPreloadDefault() const;
+ uint32_t GetPreloadDefaultAuto() const;
+
+ /**
+ * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
+ * It will not be called if the value is being unset.
+ *
+ * @param aNamespaceID the namespace of the attr being set
+ * @param aName the localname of the attribute being set
+ * @param aNotify Whether we plan to notify document observers.
+ */
+ void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify);
+
+ // True if Init() has been called after construction
+ bool mInitialized = false;
+
+ // True if user has called load(), seek() or element has started playing
+ // before. It's *only* use for `click-to-play` blocking autoplay policy.
+ // In addition, we would reset this once media aborts current load.
+ bool mIsBlessed = false;
+
+ // True if the first frame has been successfully loaded.
+ Watchable<bool> mFirstFrameLoaded = {false,
+ "HTMLMediaElement::mFirstFrameLoaded"};
+
+ // Media elements also have a default playback start position, which must
+ // initially be set to zero seconds. This time is used to allow the element to
+ // be seeked even before the media is loaded.
+ double mDefaultPlaybackStartPosition = 0.0;
+
+ // True if media element has been marked as 'tainted' and can't
+ // participate in video decoder suspending.
+ bool mHasSuspendTaint = false;
+
+ // True if media element has been forced into being considered 'hidden'.
+ // For use by mochitests. Enabling pref "media.test.video-suspend"
+ bool mForcedHidden = false;
+
+ Visibility mVisibilityState = Visibility::Untracked;
+
+ UniquePtr<ErrorSink> mErrorSink;
+
+ // This wrapper will handle all audio channel related stuffs, eg. the
+ // operations of tab audio indicator, Fennec's media control. Note:
+ // mAudioChannelWrapper might be null after GC happened.
+ RefPtr<AudioChannelAgentCallback> mAudioChannelWrapper;
+
+ // A list of pending play promises. The elements are pushed during the play()
+ // method call and are resolved/rejected during further playback steps.
+ nsTArray<RefPtr<PlayPromise>> mPendingPlayPromises;
+
+ // A list of already-dispatched but not yet run
+ // nsResolveOrRejectPendingPlayPromisesRunners.
+ // Runners whose Run() method is called remove themselves from this list.
+ // We keep track of these because the load algorithm resolves/rejects all
+ // already-dispatched pending play promises.
+ nsTArray<nsResolveOrRejectPendingPlayPromisesRunner*>
+ mPendingPlayPromisesRunners;
+
+ // A pending seek promise which is created at Seek() method call and is
+ // resolved/rejected at AsyncResolveSeekDOMPromiseIfExists()/
+ // AsyncRejectSeekDOMPromiseIfExists() methods.
+ RefPtr<dom::Promise> mSeekDOMPromise;
+
+ // Return true if the docshell is inactive and explicitly wants to stop media
+ // playing in that shell.
+ bool ShouldBeSuspendedByInactiveDocShell() const;
+
+ // For debugging bug 1407148.
+ void AssertReadyStateIsNothing();
+
+ // Contains the unique id of the sink device and the device info.
+ // The initial value is ("", nullptr) and the default output device is used.
+ // It can contain an invalid id and info if the device has been
+ // unplugged. It can be set to ("", nullptr). It follows the spec attribute:
+ // https://w3c.github.io/mediacapture-output/#htmlmediaelement-extensions
+ // Read/Write from the main thread only.
+ std::pair<nsString, RefPtr<AudioDeviceInfo>> mSink;
+
+ // This flag is used to control when the user agent is to show a poster frame
+ // for a video element instead of showing the video contents.
+ // https://html.spec.whatwg.org/multipage/media.html#show-poster-flag
+ bool mShowPoster;
+
+ // We may delay starting playback of a media for an unvisited tab until it's
+ // going to foreground. We would create ResumeDelayedMediaPlaybackAgent to
+ // handle related operations at the time whenever delaying media playback is
+ // needed.
+ void CreateResumeDelayedMediaPlaybackAgentIfNeeded();
+ void ClearResumeDelayedMediaPlaybackAgentIfNeeded();
+ RefPtr<ResumeDelayedPlaybackAgent> mResumeDelayedPlaybackAgent;
+ MozPromiseRequestHolder<ResumeDelayedPlaybackAgent::ResumePromise>
+ mResumePlaybackRequest;
+
+ // Return true if we have already a decoder or a src stream and don't have any
+ // error.
+ bool IsPlayable() const;
+
+ // Return true if the media qualifies for being controlled by media control
+ // keys.
+ bool ShouldStartMediaControlKeyListener() const;
+
+ // Start the listener if media fits the requirement of being able to be
+ // controlled be media control keys.
+ void StartMediaControlKeyListenerIfNeeded();
+
+ // It's used to listen media control key, by which we would play or pause
+ // media element.
+ RefPtr<MediaControlKeyListener> mMediaControlKeyListener;
+
+ // Method to update audio stream name
+ void UpdateStreamName();
+
+ // Return true if the media element is being used in picture in picture mode.
+ bool IsBeingUsedInPictureInPictureMode() const;
+
+ // Return true if we should queue a 'timeupdate' event runner to main thread.
+ bool ShouldQueueTimeupdateAsyncTask(TimeupdateType aType) const;
+
+#ifdef MOZ_WMF_CDM
+ // It's used to record telemetry probe for WMFCDM playback.
+ bool mIsUsingWMFCDM = false;
+#endif
+};
+
+// Check if the context is chrome or has the debugger or tabs permission
+bool HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj);
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLMediaElement_h
diff --git a/dom/html/HTMLMenuElement.cpp b/dom/html/HTMLMenuElement.cpp
new file mode 100644
index 0000000000..c3f323c483
--- /dev/null
+++ b/dom/html/HTMLMenuElement.cpp
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLMenuElement.h"
+
+#include "mozilla/dom/HTMLMenuElementBinding.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Menu)
+
+namespace mozilla::dom {
+
+HTMLMenuElement::HTMLMenuElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLMenuElement::~HTMLMenuElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLMenuElement)
+
+JSObject* HTMLMenuElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLMenuElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLMenuElement.h b/dom/html/HTMLMenuElement.h
new file mode 100644
index 0000000000..4ab37ce131
--- /dev/null
+++ b/dom/html/HTMLMenuElement.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLMenuElement_h
+#define mozilla_dom_HTMLMenuElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLMenuElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLMenuElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLMenuElement, menu)
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLMenuElement, nsGenericHTMLElement)
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // WebIDL
+ bool Compact() const { return GetBoolAttr(nsGkAtoms::compact); }
+ void SetCompact(bool aCompact, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::compact, aCompact, aError);
+ }
+
+ protected:
+ virtual ~HTMLMenuElement();
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLMenuElement_h
diff --git a/dom/html/HTMLMetaElement.cpp b/dom/html/HTMLMetaElement.cpp
new file mode 100644
index 0000000000..b99ffabdaf
--- /dev/null
+++ b/dom/html/HTMLMetaElement.cpp
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/HTMLMetaElement.h"
+#include "mozilla/dom/HTMLMetaElementBinding.h"
+#include "mozilla/dom/nsCSPService.h"
+#include "mozilla/dom/nsCSPUtils.h"
+#include "mozilla/dom/ViewportMetaData.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "nsContentUtils.h"
+#include "nsSandboxFlags.h"
+#include "nsStyleConsts.h"
+#include "nsIXMLContentSink.h"
+
+static mozilla::LazyLogModule gMetaElementLog("nsMetaElement");
+#define LOG(msg) MOZ_LOG(gMetaElementLog, mozilla::LogLevel::Debug, msg)
+#define LOG_ENABLED() MOZ_LOG_TEST(gMetaElementLog, mozilla::LogLevel::Debug)
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Meta)
+
+namespace mozilla::dom {
+
+HTMLMetaElement::HTMLMetaElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLMetaElement::~HTMLMetaElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLMetaElement)
+
+void HTMLMetaElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (Document* document = GetUncomposedDoc()) {
+ if (aName == nsGkAtoms::content) {
+ if (const nsAttrValue* name = GetParsedAttr(nsGkAtoms::name)) {
+ MetaAddedOrChanged(*document, *name, ChangeKind::ContentChange);
+ }
+ CreateAndDispatchEvent(*document, u"DOMMetaChanged"_ns);
+ } else if (aName == nsGkAtoms::name) {
+ if (aOldValue) {
+ MetaRemoved(*document, *aOldValue, ChangeKind::NameChange);
+ }
+ if (aValue) {
+ MetaAddedOrChanged(*document, *aValue, ChangeKind::NameChange);
+ }
+ CreateAndDispatchEvent(*document, u"DOMMetaChanged"_ns);
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+nsresult HTMLMetaElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!IsInUncomposedDoc()) {
+ return rv;
+ }
+ Document& doc = aContext.OwnerDoc();
+
+ bool shouldProcessMeta = true;
+ // We don't want to call ProcessMETATag when we are pretty print
+ // the document
+ if (doc.IsXMLDocument()) {
+ if (nsCOMPtr<nsIXMLContentSink> xmlSink =
+ do_QueryInterface(doc.GetCurrentContentSink())) {
+ if (xmlSink->IsPrettyPrintXML() &&
+ xmlSink->IsPrettyPrintHasSpecialRoot()) {
+ shouldProcessMeta = false;
+ }
+ }
+ }
+
+ if (shouldProcessMeta) {
+ doc.ProcessMETATag(this);
+ }
+
+ if (AttrValueIs(kNameSpaceID_None, nsGkAtoms::httpEquiv, nsGkAtoms::headerCSP,
+ eIgnoreCase)) {
+ // only accept <meta http-equiv="Content-Security-Policy" content=""> if it
+ // appears in the <head> element.
+ Element* headElt = doc.GetHeadElement();
+ if (headElt && IsInclusiveDescendantOf(headElt)) {
+ nsAutoString content;
+ GetContent(content);
+
+ if (LOG_ENABLED()) {
+ nsAutoCString documentURIspec;
+ if (nsIURI* documentURI = doc.GetDocumentURI()) {
+ documentURI->GetAsciiSpec(documentURIspec);
+ }
+
+ LOG(
+ ("HTMLMetaElement %p sets CSP '%s' on document=%p, "
+ "document-uri=%s",
+ this, NS_ConvertUTF16toUTF8(content).get(), &doc,
+ documentURIspec.get()));
+ }
+ CSP_ApplyMetaCSPToDoc(doc, content);
+ }
+ }
+
+ if (const nsAttrValue* name = GetParsedAttr(nsGkAtoms::name)) {
+ MetaAddedOrChanged(doc, *name, ChangeKind::TreeChange);
+ }
+ CreateAndDispatchEvent(doc, u"DOMMetaAdded"_ns);
+ return rv;
+}
+
+void HTMLMetaElement::UnbindFromTree(bool aNullParent) {
+ if (Document* oldDoc = GetUncomposedDoc()) {
+ if (const nsAttrValue* name = GetParsedAttr(nsGkAtoms::name)) {
+ MetaRemoved(*oldDoc, *name, ChangeKind::TreeChange);
+ }
+ CreateAndDispatchEvent(*oldDoc, u"DOMMetaRemoved"_ns);
+ }
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+void HTMLMetaElement::CreateAndDispatchEvent(Document&,
+ const nsAString& aEventName) {
+ AsyncEventDispatcher::RunDOMEventWhenSafe(*this, aEventName, CanBubble::eYes,
+ ChromeOnlyDispatch::eYes);
+}
+
+JSObject* HTMLMetaElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLMetaElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void HTMLMetaElement::MetaAddedOrChanged(Document& aDoc,
+ const nsAttrValue& aName,
+ ChangeKind aChangeKind) {
+ nsAutoString content;
+ const bool hasContent = GetAttr(nsGkAtoms::content, content);
+ if (aName.Equals(nsGkAtoms::viewport, eIgnoreCase)) {
+ if (hasContent) {
+ aDoc.SetMetaViewportData(MakeUnique<ViewportMetaData>(content));
+ }
+ return;
+ }
+
+ if (aName.Equals(nsGkAtoms::referrer, eIgnoreCase)) {
+ content = nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(
+ content);
+ return aDoc.UpdateReferrerInfoFromMeta(content,
+ /* aPreload = */ false);
+ }
+ if (aName.Equals(nsGkAtoms::color_scheme, eIgnoreCase)) {
+ if (aChangeKind != ChangeKind::ContentChange) {
+ return aDoc.AddColorSchemeMeta(*this);
+ }
+ return aDoc.RecomputeColorScheme();
+ }
+}
+
+void HTMLMetaElement::MetaRemoved(Document& aDoc, const nsAttrValue& aName,
+ ChangeKind aChangeKind) {
+ MOZ_ASSERT(aChangeKind != ChangeKind::ContentChange,
+ "Content change can't trigger removal");
+ if (aName.Equals(nsGkAtoms::color_scheme, eIgnoreCase)) {
+ return aDoc.RemoveColorSchemeMeta(*this);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLMetaElement.h b/dom/html/HTMLMetaElement.h
new file mode 100644
index 0000000000..e492b49e8b
--- /dev/null
+++ b/dom/html/HTMLMetaElement.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLMetaElement_h
+#define mozilla_dom_HTMLMetaElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLMetaElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLMetaElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLMetaElement, nsGenericHTMLElement)
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ void CreateAndDispatchEvent(Document&, const nsAString& aEventName);
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ void GetName(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); }
+ void SetName(const nsAString& aName, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aRv);
+ }
+ void GetHttpEquiv(nsAString& aValue) {
+ GetHTMLAttr(nsGkAtoms::httpEquiv, aValue);
+ }
+ void SetHttpEquiv(const nsAString& aHttpEquiv, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::httpEquiv, aHttpEquiv, aRv);
+ }
+ void GetContent(nsAString& aValue) {
+ GetHTMLAttr(nsGkAtoms::content, aValue);
+ }
+ void SetContent(const nsAString& aContent, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::content, aContent, aRv);
+ }
+ void GetScheme(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::scheme, aValue); }
+ void SetScheme(const nsAString& aScheme, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::scheme, aScheme, aRv);
+ }
+ void GetMedia(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::media, aValue); }
+ void SetMedia(const nsAString& aMedia, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::media, aMedia, aRv);
+ }
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ protected:
+ virtual ~HTMLMetaElement();
+
+ private:
+ enum class ChangeKind : uint8_t { TreeChange, NameChange, ContentChange };
+ void MetaRemoved(Document& aDoc, const nsAttrValue& aName,
+ ChangeKind aChangeKind);
+ void MetaAddedOrChanged(Document& aDoc, const nsAttrValue& aName,
+ ChangeKind aChangeKind);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLMetaElement_h
diff --git a/dom/html/HTMLMeterElement.cpp b/dom/html/HTMLMeterElement.cpp
new file mode 100644
index 0000000000..3bc025de5a
--- /dev/null
+++ b/dom/html/HTMLMeterElement.cpp
@@ -0,0 +1,259 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLMeterElement.h"
+#include "mozilla/dom/HTMLMeterElementBinding.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Meter)
+
+namespace mozilla::dom {
+
+static const double kDefaultValue = 0.0;
+static const double kDefaultMin = 0.0;
+static const double kDefaultMax = 1.0;
+
+HTMLMeterElement::HTMLMeterElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLMeterElement::~HTMLMeterElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLMeterElement)
+
+static bool IsInterestingAttr(int32_t aNamespaceID, nsAtom* aAttribute) {
+ if (aNamespaceID != kNameSpaceID_None) {
+ return false;
+ }
+ return aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max ||
+ aAttribute == nsGkAtoms::min || aAttribute == nsGkAtoms::low ||
+ aAttribute == nsGkAtoms::high || aAttribute == nsGkAtoms::optimum;
+}
+
+bool HTMLMeterElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (IsInterestingAttr(aNamespaceID, aAttribute)) {
+ return aResult.ParseDoubleValue(aValue);
+ }
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLMeterElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (IsInterestingAttr(aNameSpaceID, aName)) {
+ UpdateOptimumState(aNotify);
+ }
+ nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName, aValue, aOldValue,
+ aSubjectPrincipal, aNotify);
+}
+
+void HTMLMeterElement::UpdateOptimumState(bool aNotify) {
+ AutoStateChangeNotifier notifier(*this, aNotify);
+ RemoveStatesSilently(ElementState::METER_OPTIMUM_STATES);
+ AddStatesSilently(GetOptimumState());
+}
+
+double HTMLMeterElement::Min() const {
+ /**
+ * If the attribute min is defined, the minimum is this value.
+ * Otherwise, the minimum is the default value.
+ */
+ const nsAttrValue* attrMin = mAttrs.GetAttr(nsGkAtoms::min);
+ if (attrMin && attrMin->Type() == nsAttrValue::eDoubleValue) {
+ return attrMin->GetDoubleValue();
+ }
+ return kDefaultMin;
+}
+
+double HTMLMeterElement::Max() const {
+ /**
+ * If the attribute max is defined, the maximum is this value.
+ * Otherwise, the maximum is the default value.
+ * If the maximum value is less than the minimum value,
+ * the maximum value is the same as the minimum value.
+ */
+ double max;
+
+ const nsAttrValue* attrMax = mAttrs.GetAttr(nsGkAtoms::max);
+ if (attrMax && attrMax->Type() == nsAttrValue::eDoubleValue) {
+ max = attrMax->GetDoubleValue();
+ } else {
+ max = kDefaultMax;
+ }
+
+ return std::max(max, Min());
+}
+
+double HTMLMeterElement::Value() const {
+ /**
+ * If the attribute value is defined, the actual value is this value.
+ * Otherwise, the actual value is the default value.
+ * If the actual value is less than the minimum value,
+ * the actual value is the same as the minimum value.
+ * If the actual value is greater than the maximum value,
+ * the actual value is the same as the maximum value.
+ */
+ double value;
+
+ const nsAttrValue* attrValue = mAttrs.GetAttr(nsGkAtoms::value);
+ if (attrValue && attrValue->Type() == nsAttrValue::eDoubleValue) {
+ value = attrValue->GetDoubleValue();
+ } else {
+ value = kDefaultValue;
+ }
+
+ double min = Min();
+
+ if (value <= min) {
+ return min;
+ }
+
+ return std::min(value, Max());
+}
+
+double HTMLMeterElement::Position() const {
+ const double max = Max();
+ const double min = Min();
+ const double value = Value();
+
+ double range = max - min;
+ return range != 0.0 ? (value - min) / range : 1.0;
+}
+
+double HTMLMeterElement::Low() const {
+ /**
+ * If the low value is defined, the low value is this value.
+ * Otherwise, the low value is the minimum value.
+ * If the low value is less than the minimum value,
+ * the low value is the same as the minimum value.
+ * If the low value is greater than the maximum value,
+ * the low value is the same as the maximum value.
+ */
+
+ double min = Min();
+
+ const nsAttrValue* attrLow = mAttrs.GetAttr(nsGkAtoms::low);
+ if (!attrLow || attrLow->Type() != nsAttrValue::eDoubleValue) {
+ return min;
+ }
+
+ double low = attrLow->GetDoubleValue();
+
+ if (low <= min) {
+ return min;
+ }
+
+ return std::min(low, Max());
+}
+
+double HTMLMeterElement::High() const {
+ /**
+ * If the high value is defined, the high value is this value.
+ * Otherwise, the high value is the maximum value.
+ * If the high value is less than the low value,
+ * the high value is the same as the low value.
+ * If the high value is greater than the maximum value,
+ * the high value is the same as the maximum value.
+ */
+
+ double max = Max();
+
+ const nsAttrValue* attrHigh = mAttrs.GetAttr(nsGkAtoms::high);
+ if (!attrHigh || attrHigh->Type() != nsAttrValue::eDoubleValue) {
+ return max;
+ }
+
+ double high = attrHigh->GetDoubleValue();
+
+ if (high >= max) {
+ return max;
+ }
+
+ return std::max(high, Low());
+}
+
+double HTMLMeterElement::Optimum() const {
+ /**
+ * If the optimum value is defined, the optimum value is this value.
+ * Otherwise, the optimum value is the midpoint between
+ * the minimum value and the maximum value :
+ * min + (max - min)/2 = (min + max)/2
+ * If the optimum value is less than the minimum value,
+ * the optimum value is the same as the minimum value.
+ * If the optimum value is greater than the maximum value,
+ * the optimum value is the same as the maximum value.
+ */
+
+ double max = Max();
+
+ double min = Min();
+
+ const nsAttrValue* attrOptimum = mAttrs.GetAttr(nsGkAtoms::optimum);
+ if (!attrOptimum || attrOptimum->Type() != nsAttrValue::eDoubleValue) {
+ return (min + max) / 2.0;
+ }
+
+ double optimum = attrOptimum->GetDoubleValue();
+
+ if (optimum <= min) {
+ return min;
+ }
+
+ return std::min(optimum, max);
+}
+
+ElementState HTMLMeterElement::GetOptimumState() const {
+ /*
+ * If the optimum value is in [minimum, low[,
+ * return if the value is in optimal, suboptimal or sub-suboptimal region
+ *
+ * If the optimum value is in [low, high],
+ * return if the value is in optimal or suboptimal region
+ *
+ * If the optimum value is in ]high, maximum],
+ * return if the value is in optimal, suboptimal or sub-suboptimal region
+ */
+ double value = Value();
+ double low = Low();
+ double high = High();
+ double optimum = Optimum();
+
+ if (optimum < low) {
+ if (value < low) {
+ return ElementState::OPTIMUM;
+ }
+ if (value <= high) {
+ return ElementState::SUB_OPTIMUM;
+ }
+ return ElementState::SUB_SUB_OPTIMUM;
+ }
+ if (optimum > high) {
+ if (value > high) {
+ return ElementState::OPTIMUM;
+ }
+ if (value >= low) {
+ return ElementState::SUB_OPTIMUM;
+ }
+ return ElementState::SUB_SUB_OPTIMUM;
+ }
+ // optimum in [low, high]
+ if (value >= low && value <= high) {
+ return ElementState::OPTIMUM;
+ }
+ return ElementState::SUB_OPTIMUM;
+}
+
+JSObject* HTMLMeterElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLMeterElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLMeterElement.h b/dom/html/HTMLMeterElement.h
new file mode 100644
index 0000000000..abe0521857
--- /dev/null
+++ b/dom/html/HTMLMeterElement.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLMeterElement_h
+#define mozilla_dom_HTMLMeterElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsAttrValue.h"
+#include "nsAttrValueInlines.h"
+#include "nsAlgorithm.h"
+#include <algorithm>
+
+namespace mozilla::dom {
+
+class HTMLMeterElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLMeterElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ // WebIDL
+
+ /* @return the value */
+ double Value() const;
+ /* Returns the percentage that this element should be filed based on the
+ * min/max/value */
+ double Position() const;
+ void SetValue(double aValue, ErrorResult& aRv) {
+ SetDoubleAttr(nsGkAtoms::value, aValue, aRv);
+ }
+
+ /* @return the minimum value */
+ double Min() const;
+ void SetMin(double aValue, ErrorResult& aRv) {
+ SetDoubleAttr(nsGkAtoms::min, aValue, aRv);
+ }
+
+ /* @return the maximum value */
+ double Max() const;
+ void SetMax(double aValue, ErrorResult& aRv) {
+ SetDoubleAttr(nsGkAtoms::max, aValue, aRv);
+ }
+
+ /* @return the low value */
+ double Low() const;
+ void SetLow(double aValue, ErrorResult& aRv) {
+ SetDoubleAttr(nsGkAtoms::low, aValue, aRv);
+ }
+
+ /* @return the high value */
+ double High() const;
+ void SetHigh(double aValue, ErrorResult& aRv) {
+ SetDoubleAttr(nsGkAtoms::high, aValue, aRv);
+ }
+
+ /* @return the optimum value */
+ double Optimum() const;
+ void SetOptimum(double aValue, ErrorResult& aRv) {
+ SetDoubleAttr(nsGkAtoms::optimum, aValue, aRv);
+ }
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLMeterElement, meter);
+
+ protected:
+ virtual ~HTMLMeterElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ /**
+ * Returns the optimum state of the element.
+ * ElementState::OPTIMUM if the actual value is in the optimum region.
+ * ElementState::SUB_OPTIMUM if the actual value is in the sub-optimal
+ * region.
+ * ElementState::SUB_SUB_OPTIMUM if the actual value is in the
+ * sub-sub-optimal region.
+ *
+ * @return the optimum state of the element.
+ */
+ ElementState GetOptimumState() const;
+ void UpdateOptimumState(bool aNotify);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLMeterElement_h
diff --git a/dom/html/HTMLModElement.cpp b/dom/html/HTMLModElement.cpp
new file mode 100644
index 0000000000..ddf73bfdfe
--- /dev/null
+++ b/dom/html/HTMLModElement.cpp
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLModElement.h"
+#include "mozilla/dom/HTMLModElementBinding.h"
+#include "nsStyleConsts.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Mod)
+
+namespace mozilla::dom {
+
+HTMLModElement::HTMLModElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLModElement::~HTMLModElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLModElement)
+
+JSObject* HTMLModElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLModElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLModElement.h b/dom/html/HTMLModElement.h
new file mode 100644
index 0000000000..aa1a395c91
--- /dev/null
+++ b/dom/html/HTMLModElement.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLModElement_h
+#define mozilla_dom_HTMLModElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+
+namespace mozilla::dom {
+
+class HTMLModElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLModElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ void GetCite(nsString& aCite) { GetHTMLURIAttr(nsGkAtoms::cite, aCite); }
+ void SetCite(const nsAString& aCite, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::cite, aCite, aRv);
+ }
+ void GetDateTime(DOMString& aDateTime) {
+ GetHTMLAttr(nsGkAtoms::datetime, aDateTime);
+ }
+ void SetDateTime(const nsAString& aDateTime, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::datetime, aDateTime, aRv);
+ }
+
+ protected:
+ virtual ~HTMLModElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLModElement_h
diff --git a/dom/html/HTMLObjectElement.cpp b/dom/html/HTMLObjectElement.cpp
new file mode 100644
index 0000000000..f8e5d99963
--- /dev/null
+++ b/dom/html/HTMLObjectElement.cpp
@@ -0,0 +1,273 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLObjectElement.h"
+#include "mozilla/dom/HTMLObjectElementBinding.h"
+#include "mozilla/dom/ElementInlines.h"
+#include "mozilla/dom/WindowProxyHolder.h"
+#include "nsAttrValueInlines.h"
+#include "nsGkAtoms.h"
+#include "nsError.h"
+#include "nsIContentInlines.h"
+#include "nsIWidget.h"
+#include "nsContentUtils.h"
+#ifdef XP_MACOSX
+# include "mozilla/EventDispatcher.h"
+# include "mozilla/dom/Event.h"
+# include "nsFocusManager.h"
+#endif
+
+namespace mozilla::dom {
+
+HTMLObjectElement::HTMLObjectElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : nsGenericHTMLFormControlElement(std::move(aNodeInfo),
+ FormControlType::Object),
+ mIsDoneAddingChildren(!aFromParser) {
+ SetIsNetworkCreated(aFromParser == FROM_PARSER_NETWORK);
+
+ // <object> is always barred from constraint validation.
+ SetBarredFromConstraintValidation(true);
+}
+
+HTMLObjectElement::~HTMLObjectElement() = default;
+
+bool HTMLObjectElement::IsInteractiveHTMLContent() const {
+ return HasAttr(nsGkAtoms::usemap) ||
+ nsGenericHTMLFormControlElement::IsInteractiveHTMLContent();
+}
+
+void HTMLObjectElement::DoneAddingChildren(bool aHaveNotified) {
+ mIsDoneAddingChildren = true;
+
+ // If we're already in a document, we need to trigger the load
+ // Otherwise, BindToTree takes care of that.
+ if (IsInComposedDoc()) {
+ StartObjectLoad(aHaveNotified, false);
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLObjectElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(
+ HTMLObjectElement, nsGenericHTMLFormControlElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity)
+ nsObjectLoadingContent::Traverse(tmp, cb);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLObjectElement,
+ nsGenericHTMLFormControlElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity)
+ nsObjectLoadingContent::Unlink(tmp);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(
+ HTMLObjectElement, nsGenericHTMLFormControlElement, nsIRequestObserver,
+ nsIStreamListener, nsFrameLoaderOwner, nsIObjectLoadingContent,
+ nsIChannelEventSink, nsIConstraintValidation)
+
+NS_IMPL_ELEMENT_CLONE(HTMLObjectElement)
+
+nsresult HTMLObjectElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLFormControlElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we already have all the children, start the load.
+ if (IsInComposedDoc() && mIsDoneAddingChildren) {
+ void (HTMLObjectElement::*start)() = &HTMLObjectElement::StartObjectLoad;
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod("dom::HTMLObjectElement::BindToTree", this, start));
+ }
+
+ return NS_OK;
+}
+
+void HTMLObjectElement::UnbindFromTree(bool aNullParent) {
+ nsObjectLoadingContent::UnbindFromTree(aNullParent);
+ nsGenericHTMLFormControlElement::UnbindFromTree(aNullParent);
+}
+
+void HTMLObjectElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+ return nsGenericHTMLFormControlElement::AfterSetAttr(
+ aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLObjectElement::OnAttrSetButNotChanged(
+ int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, aNotify);
+ return nsGenericHTMLFormControlElement::OnAttrSetButNotChanged(
+ aNamespaceID, aName, aValue, aNotify);
+}
+
+void HTMLObjectElement::AfterMaybeChangeAttr(int32_t aNamespaceID,
+ nsAtom* aName, bool aNotify) {
+ // if aNotify is false, we are coming from the parser or some such place;
+ // we'll get bound after all the attributes have been set, so we'll do the
+ // object load from BindToTree/DoneAddingChildren.
+ // Skip the LoadObject call in that case.
+ // We also don't want to start loading the object when we're not yet in
+ // a document, just in case that the caller wants to set additional
+ // attributes before inserting the node into the document.
+ if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::data ||
+ !aNotify || !IsInComposedDoc() || !mIsDoneAddingChildren ||
+ BlockEmbedOrObjectContentLoading()) {
+ return;
+ }
+ nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+ "HTMLObjectElement::LoadObject",
+ [self = RefPtr<HTMLObjectElement>(this), aNotify]() {
+ if (self->IsInComposedDoc()) {
+ self->LoadObject(aNotify, true);
+ }
+ }));
+}
+
+bool HTMLObjectElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ // TODO: this should probably be managed directly by IsHTMLFocusable.
+ // See bug 597242.
+ Document* doc = GetComposedDoc();
+ if (!doc || IsInDesignMode()) {
+ if (aTabIndex) {
+ *aTabIndex = -1;
+ }
+
+ *aIsFocusable = false;
+ return false;
+ }
+
+ const nsAttrValue* attrVal = mAttrs.GetAttr(nsGkAtoms::tabindex);
+ bool isFocusable = attrVal && attrVal->Type() == nsAttrValue::eInteger;
+
+ // This method doesn't call nsGenericHTMLFormControlElement intentionally.
+ // TODO: It should probably be changed when bug 597242 will be fixed.
+ if (IsEditingHost() || Type() == ObjectType::Document) {
+ if (aTabIndex) {
+ *aTabIndex = isFocusable ? attrVal->GetIntegerValue() : 0;
+ }
+
+ *aIsFocusable = true;
+ return false;
+ }
+
+ // TODO: this should probably be managed directly by IsHTMLFocusable.
+ // See bug 597242.
+ if (aTabIndex && isFocusable) {
+ *aTabIndex = attrVal->GetIntegerValue();
+ *aIsFocusable = true;
+ }
+
+ return false;
+}
+
+int32_t HTMLObjectElement::TabIndexDefault() { return 0; }
+
+Nullable<WindowProxyHolder> HTMLObjectElement::GetContentWindow(
+ nsIPrincipal& aSubjectPrincipal) {
+ Document* doc = GetContentDocument(aSubjectPrincipal);
+ if (doc) {
+ nsPIDOMWindowOuter* win = doc->GetWindow();
+ if (win) {
+ return WindowProxyHolder(win->GetBrowsingContext());
+ }
+ }
+
+ return nullptr;
+}
+
+bool HTMLObjectElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseAlignValue(aValue, aResult);
+ }
+ if (ParseImageAttribute(aAttribute, aValue, aResult)) {
+ return true;
+ }
+ }
+
+ return nsGenericHTMLFormControlElement::ParseAttribute(
+ aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLObjectElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapImageAlignAttributeInto(aBuilder);
+ MapImageBorderAttributeInto(aBuilder);
+ MapImageMarginAttributeInto(aBuilder);
+ MapImageSizeAttributesInto(aBuilder);
+ MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLObjectElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry* const map[] = {
+ sCommonAttributeMap,
+ sImageMarginSizeAttributeMap,
+ sImageBorderAttributeMap,
+ sImageAlignAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLObjectElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+void HTMLObjectElement::StartObjectLoad(bool aNotify, bool aForce) {
+ // BindToTree can call us asynchronously, and we may be removed from the tree
+ // in the interim
+ if (!IsInComposedDoc() || !OwnerDoc()->IsActive() ||
+ BlockEmbedOrObjectContentLoading()) {
+ return;
+ }
+
+ LoadObject(aNotify, aForce);
+ SetIsNetworkCreated(false);
+}
+
+uint32_t HTMLObjectElement::GetCapabilities() const {
+ return nsObjectLoadingContent::GetCapabilities() | eFallbackIfClassIDPresent;
+}
+
+void HTMLObjectElement::DestroyContent() {
+ nsObjectLoadingContent::Destroy();
+ nsGenericHTMLFormControlElement::DestroyContent();
+}
+
+nsresult HTMLObjectElement::CopyInnerTo(Element* aDest) {
+ nsresult rv = nsGenericHTMLFormControlElement::CopyInnerTo(aDest);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aDest->OwnerDoc()->IsStaticDocument()) {
+ CreateStaticClone(static_cast<HTMLObjectElement*>(aDest));
+ }
+
+ return rv;
+}
+
+JSObject* HTMLObjectElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLObjectElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Object)
diff --git a/dom/html/HTMLObjectElement.h b/dom/html/HTMLObjectElement.h
new file mode 100644
index 0000000000..c627511b5c
--- /dev/null
+++ b/dom/html/HTMLObjectElement.h
@@ -0,0 +1,205 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLObjectElement_h
+#define mozilla_dom_HTMLObjectElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/ConstraintValidation.h"
+#include "nsGenericHTMLElement.h"
+#include "nsObjectLoadingContent.h"
+
+namespace mozilla::dom {
+
+class FormData;
+template <typename T>
+struct Nullable;
+class WindowProxyHolder;
+
+class HTMLObjectElement final : public nsGenericHTMLFormControlElement,
+ public nsObjectLoadingContent,
+ public ConstraintValidation {
+ public:
+ explicit HTMLObjectElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser = NOT_FROM_PARSER);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLObjectElement, object)
+ int32_t TabIndexDefault() override;
+
+ // nsObjectLoadingContent
+ const Element* AsElement() const final { return this; }
+
+ // Element
+ bool IsInteractiveHTMLContent() const override;
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+
+ // Overriden nsIFormControl methods
+ NS_IMETHOD Reset() override { return NS_OK; }
+
+ NS_IMETHOD SubmitNamesValues(FormData* aFormData) override { return NS_OK; }
+
+ void DoneAddingChildren(bool aHaveNotified) override;
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ void DestroyContent() override;
+
+ // nsObjectLoadingContent
+ uint32_t GetCapabilities() const override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ nsresult CopyInnerTo(Element* aDest);
+
+ void StartObjectLoad() { StartObjectLoad(true, false); }
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLObjectElement,
+ nsGenericHTMLFormControlElement)
+
+ // Web IDL binding methods
+ void GetData(DOMString& aValue) {
+ GetURIAttr(nsGkAtoms::data, nsGkAtoms::codebase, aValue);
+ }
+ void SetData(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::data, aValue, aRv);
+ }
+ void GetType(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::type, aValue); }
+ void SetType(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::type, aValue, aRv);
+ }
+ void GetName(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); }
+ void SetName(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aValue, aRv);
+ }
+ void GetUseMap(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::usemap, aValue); }
+ void SetUseMap(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::usemap, aValue, aRv);
+ }
+ void GetWidth(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::width, aValue); }
+ void SetWidth(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::width, aValue, aRv);
+ }
+ void GetHeight(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::height, aValue); }
+ void SetHeight(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::height, aValue, aRv);
+ }
+ using nsObjectLoadingContent::GetContentDocument;
+
+ Nullable<WindowProxyHolder> GetContentWindow(nsIPrincipal& aSubjectPrincipal);
+
+ using ConstraintValidation::GetValidationMessage;
+ using ConstraintValidation::SetCustomValidity;
+ void GetAlign(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::align, aValue); }
+ void SetAlign(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::align, aValue, aRv);
+ }
+ void GetArchive(DOMString& aValue) {
+ GetHTMLAttr(nsGkAtoms::archive, aValue);
+ }
+ void SetArchive(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::archive, aValue, aRv);
+ }
+ void GetCode(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::code, aValue); }
+ void SetCode(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::code, aValue, aRv);
+ }
+ bool Declare() { return GetBoolAttr(nsGkAtoms::declare); }
+ void SetDeclare(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::declare, aValue, aRv);
+ }
+ uint32_t Hspace() {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::hspace, 0);
+ }
+ void SetHspace(uint32_t aValue, ErrorResult& aRv) {
+ SetUnsignedIntAttr(nsGkAtoms::hspace, aValue, 0, aRv);
+ }
+ void GetStandby(DOMString& aValue) {
+ GetHTMLAttr(nsGkAtoms::standby, aValue);
+ }
+ void SetStandby(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::standby, aValue, aRv);
+ }
+ uint32_t Vspace() {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::vspace, 0);
+ }
+ void SetVspace(uint32_t aValue, ErrorResult& aRv) {
+ SetUnsignedIntAttr(nsGkAtoms::vspace, aValue, 0, aRv);
+ }
+ void GetCodeBase(DOMString& aValue) {
+ GetURIAttr(nsGkAtoms::codebase, nullptr, aValue);
+ }
+ void SetCodeBase(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::codebase, aValue, aRv);
+ }
+ void GetCodeType(DOMString& aValue) {
+ GetHTMLAttr(nsGkAtoms::codetype, aValue);
+ }
+ void SetCodeType(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::codetype, aValue, aRv);
+ }
+ void GetBorder(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::border, aValue); }
+ void SetBorder(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::border, aValue, aRv);
+ }
+
+ Document* GetSVGDocument(nsIPrincipal& aSubjectPrincipal) {
+ return GetContentDocument(aSubjectPrincipal);
+ }
+
+ /**
+ * Calls LoadObject with the correct arguments to start the plugin load.
+ */
+ void StartObjectLoad(bool aNotify, bool aForceLoad);
+
+ protected:
+ void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+ void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) override;
+
+ private:
+ nsContentPolicyType GetContentPolicyType() const override {
+ return nsIContentPolicy::TYPE_INTERNAL_OBJECT;
+ }
+
+ virtual ~HTMLObjectElement();
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+
+ /**
+ * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
+ * This function will be called by AfterSetAttr whether the attribute is being
+ * set or unset.
+ *
+ * @param aNamespaceID the namespace of the attr being set
+ * @param aName the localname of the attribute being set
+ * @param aNotify Whether we plan to notify document observers.
+ */
+ void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify);
+
+ bool mIsDoneAddingChildren;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLObjectElement_h
diff --git a/dom/html/HTMLOptGroupElement.cpp b/dom/html/HTMLOptGroupElement.cpp
new file mode 100644
index 0000000000..9a160b430c
--- /dev/null
+++ b/dom/html/HTMLOptGroupElement.cpp
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/HTMLOptGroupElement.h"
+#include "mozilla/dom/HTMLOptGroupElementBinding.h"
+#include "mozilla/dom/HTMLSelectElement.h" // SafeOptionListMutation
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsIFrame.h"
+#include "nsIFormControlFrame.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(OptGroup)
+
+namespace mozilla::dom {
+
+/**
+ * The implementation of &lt;optgroup&gt;
+ */
+
+HTMLOptGroupElement::HTMLOptGroupElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ // We start off enabled
+ AddStatesSilently(ElementState::ENABLED);
+}
+
+HTMLOptGroupElement::~HTMLOptGroupElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLOptGroupElement)
+
+void HTMLOptGroupElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ aVisitor.mCanHandle = false;
+
+ if (nsIFrame* frame = GetPrimaryFrame()) {
+ // FIXME(emilio): This poking at the style of the frame is broken unless we
+ // flush before every event handling, which we don't really want to.
+ if (frame->StyleUI()->UserInput() == StyleUserInput::None) {
+ return;
+ }
+ }
+
+ nsGenericHTMLElement::GetEventTargetParent(aVisitor);
+}
+
+Element* HTMLOptGroupElement::GetSelect() {
+ Element* parent = nsINode::GetParentElement();
+ if (!parent || !parent->IsHTMLElement(nsGkAtoms::select)) {
+ return nullptr;
+ }
+ return parent;
+}
+
+void HTMLOptGroupElement::InsertChildBefore(nsIContent* aKid,
+ nsIContent* aBeforeThis,
+ bool aNotify, ErrorResult& aRv) {
+ const uint32_t index =
+ aBeforeThis ? *ComputeIndexOf(aBeforeThis) : GetChildCount();
+ SafeOptionListMutation safeMutation(GetSelect(), this, aKid, index, aNotify);
+ nsGenericHTMLElement::InsertChildBefore(aKid, aBeforeThis, aNotify, aRv);
+ if (aRv.Failed()) {
+ safeMutation.MutationFailed();
+ }
+}
+
+void HTMLOptGroupElement::RemoveChildNode(nsIContent* aKid, bool aNotify) {
+ SafeOptionListMutation safeMutation(GetSelect(), this, nullptr,
+ *ComputeIndexOf(aKid), aNotify);
+ nsGenericHTMLElement::RemoveChildNode(aKid, aNotify);
+}
+
+void HTMLOptGroupElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::disabled) {
+ ElementState disabledStates;
+ if (aValue) {
+ disabledStates |= ElementState::DISABLED;
+ } else {
+ disabledStates |= ElementState::ENABLED;
+ }
+
+ ElementState oldDisabledStates = State() & ElementState::DISABLED_STATES;
+ ElementState changedStates = disabledStates ^ oldDisabledStates;
+
+ if (!changedStates.IsEmpty()) {
+ ToggleStates(changedStates, aNotify);
+
+ // All our children <option> have their :disabled state depending on our
+ // disabled attribute. We should make sure their state is updated.
+ for (nsIContent* child = nsINode::GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (auto optElement = HTMLOptionElement::FromNode(child)) {
+ optElement->OptGroupDisabledChanged(true);
+ }
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+JSObject* HTMLOptGroupElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLOptGroupElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLOptGroupElement.h b/dom/html/HTMLOptGroupElement.h
new file mode 100644
index 0000000000..47596e520e
--- /dev/null
+++ b/dom/html/HTMLOptGroupElement.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLOptGroupElement_h
+#define mozilla_dom_HTMLOptGroupElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla {
+class ErrorResult;
+class EventChainPreVisitor;
+namespace dom {
+
+class HTMLOptGroupElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLOptGroupElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLOptGroupElement, optgroup)
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLOptGroupElement,
+ nsGenericHTMLElement)
+
+ // nsINode
+ virtual void InsertChildBefore(nsIContent* aKid, nsIContent* aBeforeThis,
+ bool aNotify, ErrorResult& aRv) override;
+ virtual void RemoveChildNode(nsIContent* aKid, bool aNotify) override;
+
+ // nsIContent
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) override;
+
+ bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); }
+ void SetDisabled(bool aValue, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::disabled, aValue, aError);
+ }
+
+ void GetLabel(nsAString& aValue) const {
+ GetHTMLAttr(nsGkAtoms::label, aValue);
+ }
+ void SetLabel(const nsAString& aLabel, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::label, aLabel, aError);
+ }
+
+ protected:
+ virtual ~HTMLOptGroupElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ protected:
+ /**
+ * Get the select content element that contains this option
+ * @param aSelectElement the select element [OUT]
+ */
+ Element* GetSelect();
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_HTMLOptGroupElement_h */
diff --git a/dom/html/HTMLOptionElement.cpp b/dom/html/HTMLOptionElement.cpp
new file mode 100644
index 0000000000..733e15c609
--- /dev/null
+++ b/dom/html/HTMLOptionElement.cpp
@@ -0,0 +1,348 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLOptionElement.h"
+
+#include "HTMLOptGroupElement.h"
+#include "mozilla/dom/HTMLOptionElementBinding.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsIFormControl.h"
+#include "nsISelectControlFrame.h"
+
+// Notify/query select frame for selected state
+#include "nsIFormControlFrame.h"
+#include "mozilla/dom/Document.h"
+#include "nsNodeInfoManager.h"
+#include "nsCOMPtr.h"
+#include "nsContentCreatorFunctions.h"
+#include "mozAutoDocUpdate.h"
+#include "nsTextNode.h"
+
+/**
+ * Implementation of &lt;option&gt;
+ */
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Option)
+
+namespace mozilla::dom {
+
+HTMLOptionElement::HTMLOptionElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ // We start off enabled
+ AddStatesSilently(ElementState::ENABLED);
+}
+
+HTMLOptionElement::~HTMLOptionElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLOptionElement)
+
+mozilla::dom::HTMLFormElement* HTMLOptionElement::GetForm() {
+ HTMLSelectElement* selectControl = GetSelect();
+ return selectControl ? selectControl->GetForm() : nullptr;
+}
+
+void HTMLOptionElement::SetSelectedInternal(bool aValue, bool aNotify) {
+ mSelectedChanged = true;
+ SetStates(ElementState::CHECKED, aValue, aNotify);
+}
+
+void HTMLOptionElement::OptGroupDisabledChanged(bool aNotify) {
+ UpdateDisabledState(aNotify);
+}
+
+void HTMLOptionElement::UpdateDisabledState(bool aNotify) {
+ bool isDisabled = HasAttr(nsGkAtoms::disabled);
+
+ if (!isDisabled) {
+ nsIContent* parent = GetParent();
+ if (auto optGroupElement = HTMLOptGroupElement::FromNodeOrNull(parent)) {
+ isDisabled = optGroupElement->IsDisabled();
+ }
+ }
+
+ ElementState disabledStates;
+ if (isDisabled) {
+ disabledStates |= ElementState::DISABLED;
+ } else {
+ disabledStates |= ElementState::ENABLED;
+ }
+
+ ElementState oldDisabledStates = State() & ElementState::DISABLED_STATES;
+ ElementState changedStates = disabledStates ^ oldDisabledStates;
+
+ if (!changedStates.IsEmpty()) {
+ ToggleStates(changedStates, aNotify);
+ }
+}
+
+void HTMLOptionElement::SetSelected(bool aValue) {
+ // Note: The select content obj maintains all the PresState
+ // so defer to it to get the answer
+ HTMLSelectElement* selectInt = GetSelect();
+ if (selectInt) {
+ int32_t index = Index();
+ HTMLSelectElement::OptionFlags mask{
+ HTMLSelectElement::OptionFlag::SetDisabled,
+ HTMLSelectElement::OptionFlag::Notify};
+ if (aValue) {
+ mask += HTMLSelectElement::OptionFlag::IsSelected;
+ }
+
+ // This should end up calling SetSelectedInternal
+ selectInt->SetOptionsSelectedByIndex(index, index, mask);
+ } else {
+ SetSelectedInternal(aValue, true);
+ }
+}
+
+int32_t HTMLOptionElement::Index() {
+ static int32_t defaultIndex = 0;
+
+ // Only select elements can contain a list of options.
+ HTMLSelectElement* selectElement = GetSelect();
+ if (!selectElement) {
+ return defaultIndex;
+ }
+
+ HTMLOptionsCollection* options = selectElement->GetOptions();
+ if (!options) {
+ return defaultIndex;
+ }
+
+ int32_t index = defaultIndex;
+ MOZ_ALWAYS_SUCCEEDS(options->GetOptionIndex(this, 0, true, &index));
+ return index;
+}
+
+nsChangeHint HTMLOptionElement::GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const {
+ nsChangeHint retval =
+ nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType);
+
+ if (aAttribute == nsGkAtoms::label) {
+ retval |= nsChangeHint_ReconstructFrame;
+ } else if (aAttribute == nsGkAtoms::text) {
+ retval |= NS_STYLE_HINT_REFLOW;
+ }
+ return retval;
+}
+
+void HTMLOptionElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue, aNotify);
+
+ if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::selected ||
+ mSelectedChanged) {
+ return;
+ }
+
+ // We just changed out selected state (since we look at the "selected"
+ // attribute when mSelectedChanged is false). Let's tell our select about
+ // it.
+ HTMLSelectElement* selectInt = GetSelect();
+ if (!selectInt) {
+ // If option is a child of select, SetOptionsSelectedByIndex will set the
+ // selected state if needed.
+ SetStates(ElementState::CHECKED, !!aValue, aNotify);
+ return;
+ }
+
+ NS_ASSERTION(!mSelectedChanged, "Shouldn't be here");
+
+ bool inSetDefaultSelected = mIsInSetDefaultSelected;
+ mIsInSetDefaultSelected = true;
+
+ int32_t index = Index();
+ HTMLSelectElement::OptionFlags mask =
+ HTMLSelectElement::OptionFlag::SetDisabled;
+ if (aValue) {
+ mask += HTMLSelectElement::OptionFlag::IsSelected;
+ }
+
+ if (aNotify) {
+ mask += HTMLSelectElement::OptionFlag::Notify;
+ }
+
+ // This can end up calling SetSelectedInternal if our selected state needs to
+ // change, which we will allow to take effect so that parts of
+ // SetOptionsSelectedByIndex that might depend on it working don't get
+ // confused.
+ selectInt->SetOptionsSelectedByIndex(index, index, mask);
+
+ // Now reset our members; when we finish the attr set we'll end up with the
+ // rigt selected state.
+ mIsInSetDefaultSelected = inSetDefaultSelected;
+ // the selected state might have been changed by SetOptionsSelectedByIndex,
+ // possibly more than once; make sure our mSelectedChanged state is set back
+ // correctly.
+ mSelectedChanged = false;
+}
+
+void HTMLOptionElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::disabled) {
+ UpdateDisabledState(aNotify);
+ }
+
+ if (aName == nsGkAtoms::value && Selected()) {
+ // Since this option is selected, changing value may have changed missing
+ // validity state of the select element
+ if (HTMLSelectElement* select = GetSelect()) {
+ select->UpdateValueMissingValidityState();
+ }
+ }
+
+ if (aName == nsGkAtoms::selected) {
+ SetStates(ElementState::DEFAULT, !!aValue, aNotify);
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLOptionElement::GetText(nsAString& aText) {
+ nsAutoString text;
+
+ nsIContent* child = nsINode::GetFirstChild();
+ while (child) {
+ if (Text* textChild = child->GetAsText()) {
+ textChild->AppendTextTo(text);
+ }
+ if (child->IsHTMLElement(nsGkAtoms::script) ||
+ child->IsSVGElement(nsGkAtoms::script)) {
+ child = child->GetNextNonChildNode(this);
+ } else {
+ child = child->GetNextNode(this);
+ }
+ }
+
+ // XXX No CompressWhitespace for nsAString. Sad.
+ text.CompressWhitespace(true, true);
+ aText = text;
+}
+
+void HTMLOptionElement::SetText(const nsAString& aText, ErrorResult& aRv) {
+ aRv = nsContentUtils::SetNodeTextContent(this, aText, false);
+}
+
+nsresult HTMLOptionElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Our new parent might change :disabled/:enabled state.
+ UpdateDisabledState(false);
+
+ return NS_OK;
+}
+
+void HTMLOptionElement::UnbindFromTree(bool aNullParent) {
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+
+ // Our previous parent could have been involved in :disabled/:enabled state.
+ UpdateDisabledState(false);
+}
+
+// Get the select content element that contains this option
+HTMLSelectElement* HTMLOptionElement::GetSelect() {
+ nsIContent* parent = GetParent();
+ if (!parent) {
+ return nullptr;
+ }
+
+ HTMLSelectElement* select = HTMLSelectElement::FromNode(parent);
+ if (select) {
+ return select;
+ }
+
+ if (!parent->IsHTMLElement(nsGkAtoms::optgroup)) {
+ return nullptr;
+ }
+
+ return HTMLSelectElement::FromNodeOrNull(parent->GetParent());
+}
+
+already_AddRefed<HTMLOptionElement> HTMLOptionElement::Option(
+ const GlobalObject& aGlobal, const nsAString& aText,
+ const Optional<nsAString>& aValue, bool aDefaultSelected, bool aSelected,
+ ErrorResult& aError) {
+ nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
+ Document* doc;
+ if (!win || !(doc = win->GetExtantDoc())) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo = doc->NodeInfoManager()->GetNodeInfo(
+ nsGkAtoms::option, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE);
+
+ auto* nim = nodeInfo->NodeInfoManager();
+ RefPtr<HTMLOptionElement> option =
+ new (nim) HTMLOptionElement(nodeInfo.forget());
+
+ if (!aText.IsEmpty()) {
+ // Create a new text node and append it to the option
+ RefPtr<nsTextNode> textContent = new (option->NodeInfo()->NodeInfoManager())
+ nsTextNode(option->NodeInfo()->NodeInfoManager());
+
+ textContent->SetText(aText, false);
+
+ option->AppendChildTo(textContent, false, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ }
+
+ if (aValue.WasPassed()) {
+ // Set the value attribute for this element. We're calling SetAttr
+ // directly because we want to pass aNotify == false.
+ aError = option->SetAttr(kNameSpaceID_None, nsGkAtoms::value,
+ aValue.Value(), false);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ }
+
+ if (aDefaultSelected) {
+ // We're calling SetAttr directly because we want to pass
+ // aNotify == false.
+ aError =
+ option->SetAttr(kNameSpaceID_None, nsGkAtoms::selected, u""_ns, false);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ }
+
+ option->SetSelected(aSelected);
+ option->SetSelectedChanged(false);
+
+ return option.forget();
+}
+
+nsresult HTMLOptionElement::CopyInnerTo(Element* aDest) {
+ nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aDest->OwnerDoc()->IsStaticDocument()) {
+ static_cast<HTMLOptionElement*>(aDest)->SetSelected(Selected());
+ }
+ return NS_OK;
+}
+
+JSObject* HTMLOptionElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLOptionElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLOptionElement.h b/dom/html/HTMLOptionElement.h
new file mode 100644
index 0000000000..4d8e920a52
--- /dev/null
+++ b/dom/html/HTMLOptionElement.h
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLOptionElement_h__
+#define mozilla_dom_HTMLOptionElement_h__
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "mozilla/dom/HTMLFormElement.h"
+
+namespace mozilla::dom {
+
+class HTMLSelectElement;
+
+class HTMLOptionElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLOptionElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ static already_AddRefed<HTMLOptionElement> Option(
+ const GlobalObject& aGlobal, const nsAString& aText,
+ const Optional<nsAString>& aValue, bool aDefaultSelected, bool aSelected,
+ ErrorResult& aError);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLOptionElement, option)
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLOptionElement, nsGenericHTMLElement)
+
+ using mozilla::dom::Element::GetText;
+
+ bool Selected() const { return State().HasState(ElementState::CHECKED); }
+ void SetSelected(bool aValue);
+
+ void SetSelectedChanged(bool aValue) { mSelectedChanged = aValue; }
+
+ nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const override;
+
+ void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ void SetSelectedInternal(bool aValue, bool aNotify);
+
+ /**
+ * This callback is called by an optgroup on all its option elements whenever
+ * its disabled state is changed so that option elements can know their
+ * disabled state might have changed.
+ */
+ void OptGroupDisabledChanged(bool aNotify);
+
+ /**
+ * Check our disabled content attribute and optgroup's (if it exists) disabled
+ * state to decide whether our disabled flag should be toggled.
+ */
+ void UpdateDisabledState(bool aNotify);
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ nsresult CopyInnerTo(mozilla::dom::Element* aDest);
+
+ bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); }
+
+ void SetDisabled(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::disabled, aValue, aRv);
+ }
+
+ HTMLFormElement* GetForm();
+
+ void GetRenderedLabel(nsAString& aLabel) {
+ if (!GetAttr(nsGkAtoms::label, aLabel) || aLabel.IsEmpty()) {
+ GetText(aLabel);
+ }
+ }
+
+ void GetLabel(nsAString& aLabel) {
+ if (!GetAttr(nsGkAtoms::label, aLabel)) {
+ GetText(aLabel);
+ }
+ }
+ void SetLabel(const nsAString& aLabel, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::label, aLabel, aError);
+ }
+
+ bool DefaultSelected() const { return HasAttr(nsGkAtoms::selected); }
+ void SetDefaultSelected(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::selected, aValue, aRv);
+ }
+
+ void GetValue(nsAString& aValue) {
+ if (!GetAttr(nsGkAtoms::value, aValue)) {
+ GetText(aValue);
+ }
+ }
+ void SetValue(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::value, aValue, aRv);
+ }
+
+ void GetText(nsAString& aText);
+ void SetText(const nsAString& aText, ErrorResult& aRv);
+
+ int32_t Index();
+
+ protected:
+ virtual ~HTMLOptionElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ /**
+ * Get the select content element that contains this option, this
+ * intentionally does not return nsresult, all we care about is if
+ * there's a select associated with this option or not.
+ */
+ HTMLSelectElement* GetSelect();
+
+ bool mSelectedChanged = false;
+
+ // True only while we're under the SetOptionsSelectedByIndex call when our
+ // "selected" attribute is changing and mSelectedChanged is false.
+ bool mIsInSetDefaultSelected = false;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLOptionElement_h__
diff --git a/dom/html/HTMLOptionsCollection.cpp b/dom/html/HTMLOptionsCollection.cpp
new file mode 100644
index 0000000000..be483641d9
--- /dev/null
+++ b/dom/html/HTMLOptionsCollection.cpp
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLOptionsCollection.h"
+#include "mozilla/dom/HTMLOptionsCollectionBinding.h"
+
+#include "mozilla/dom/HTMLOptionElement.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+
+namespace mozilla::dom {
+
+HTMLOptionsCollection::HTMLOptionsCollection(HTMLSelectElement* aSelect)
+ : mSelect(aSelect) {}
+
+nsresult HTMLOptionsCollection::GetOptionIndex(Element* aOption,
+ int32_t aStartIndex,
+ bool aForward, int32_t* aIndex) {
+ // NOTE: aIndex shouldn't be set if the returned value isn't NS_OK.
+
+ int32_t index;
+
+ // Make the common case fast
+ if (aStartIndex == 0 && aForward) {
+ index = mElements.IndexOf(aOption);
+ if (index == -1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aIndex = index;
+ return NS_OK;
+ }
+
+ int32_t high = mElements.Length();
+ int32_t step = aForward ? 1 : -1;
+
+ for (index = aStartIndex; index < high && index > -1; index += step) {
+ if (mElements[index] == aOption) {
+ *aIndex = index;
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HTMLOptionsCollection, mElements, mSelect)
+
+// nsISupports
+
+// QueryInterface implementation for HTMLOptionsCollection
+NS_INTERFACE_TABLE_HEAD(HTMLOptionsCollection)
+ NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY
+ NS_INTERFACE_TABLE(HTMLOptionsCollection, nsIHTMLCollection)
+ NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(HTMLOptionsCollection)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLOptionsCollection)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLOptionsCollection)
+
+JSObject* HTMLOptionsCollection::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLOptionsCollection_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+uint32_t HTMLOptionsCollection::Length() { return mElements.Length(); }
+
+void HTMLOptionsCollection::SetLength(uint32_t aLength, ErrorResult& aError) {
+ mSelect->SetLength(aLength, aError);
+}
+
+void HTMLOptionsCollection::IndexedSetter(uint32_t aIndex,
+ HTMLOptionElement* aOption,
+ ErrorResult& aError) {
+ // if the new option is null, just remove this option. Note that it's safe
+ // to pass a too-large aIndex in here.
+ if (!aOption) {
+ mSelect->Remove(aIndex);
+
+ // We're done.
+ return;
+ }
+
+ // Now we're going to be setting an option in our collection
+ if (aIndex > mElements.Length()) {
+ // Fill our array with blank options up to (but not including, since we're
+ // about to change it) aIndex, for compat with other browsers.
+ SetLength(aIndex, aError);
+ ENSURE_SUCCESS_VOID(aError);
+ }
+
+ NS_ASSERTION(aIndex <= mElements.Length(), "SetLength lied");
+
+ if (aIndex == mElements.Length()) {
+ mSelect->AppendChild(*aOption, aError);
+ return;
+ }
+
+ // Find the option they're talking about and replace it
+ // hold a strong reference to follow COM rules.
+ RefPtr<HTMLOptionElement> refChild = ItemAsOption(aIndex);
+ if (!refChild) {
+ aError.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ nsCOMPtr<nsINode> parent = refChild->GetParent();
+ if (!parent) {
+ return;
+ }
+
+ parent->ReplaceChild(*aOption, *refChild, aError);
+}
+
+int32_t HTMLOptionsCollection::SelectedIndex() {
+ return mSelect->SelectedIndex();
+}
+
+void HTMLOptionsCollection::SetSelectedIndex(int32_t aSelectedIndex) {
+ mSelect->SetSelectedIndex(aSelectedIndex);
+}
+
+Element* HTMLOptionsCollection::GetElementAt(uint32_t aIndex) {
+ return ItemAsOption(aIndex);
+}
+
+HTMLOptionElement* HTMLOptionsCollection::NamedGetter(const nsAString& aName,
+ bool& aFound) {
+ uint32_t count = mElements.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ HTMLOptionElement* content = mElements.ElementAt(i);
+ if (content && (content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
+ aName, eCaseMatters) ||
+ content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id,
+ aName, eCaseMatters))) {
+ aFound = true;
+ return content;
+ }
+ }
+
+ aFound = false;
+ return nullptr;
+}
+
+nsINode* HTMLOptionsCollection::GetParentObject() { return mSelect; }
+
+DocGroup* HTMLOptionsCollection::GetDocGroup() const {
+ return mSelect ? mSelect->GetDocGroup() : nullptr;
+}
+
+void HTMLOptionsCollection::GetSupportedNames(nsTArray<nsString>& aNames) {
+ AutoTArray<nsAtom*, 8> atoms;
+ for (uint32_t i = 0; i < mElements.Length(); ++i) {
+ HTMLOptionElement* content = mElements.ElementAt(i);
+ if (content) {
+ // Note: HasName means the names is exposed on the document,
+ // which is false for options, so we don't check it here.
+ const nsAttrValue* val = content->GetParsedAttr(nsGkAtoms::name);
+ if (val && val->Type() == nsAttrValue::eAtom) {
+ nsAtom* name = val->GetAtomValue();
+ if (!atoms.Contains(name)) {
+ atoms.AppendElement(name);
+ }
+ }
+ if (content->HasID()) {
+ nsAtom* id = content->GetID();
+ if (!atoms.Contains(id)) {
+ atoms.AppendElement(id);
+ }
+ }
+ }
+ }
+
+ uint32_t atomsLen = atoms.Length();
+ nsString* names = aNames.AppendElements(atomsLen);
+ for (uint32_t i = 0; i < atomsLen; ++i) {
+ atoms[i]->ToString(names[i]);
+ }
+}
+
+void HTMLOptionsCollection::Add(const HTMLOptionOrOptGroupElement& aElement,
+ const Nullable<HTMLElementOrLong>& aBefore,
+ ErrorResult& aError) {
+ mSelect->Add(aElement, aBefore, aError);
+}
+
+void HTMLOptionsCollection::Remove(int32_t aIndex) { mSelect->Remove(aIndex); }
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLOptionsCollection.h b/dom/html/HTMLOptionsCollection.h
new file mode 100644
index 0000000000..e4300c876d
--- /dev/null
+++ b/dom/html/HTMLOptionsCollection.h
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLOptionsCollection_h
+#define mozilla_dom_HTMLOptionsCollection_h
+
+#include "mozilla/Attributes.h"
+#include "nsIHTMLCollection.h"
+#include "nsWrapperCache.h"
+
+#include "mozilla/dom/HTMLOptionElement.h"
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "nsGenericHTMLElement.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class DocGroup;
+class HTMLElementOrLong;
+class HTMLOptionElementOrHTMLOptGroupElement;
+class HTMLSelectElement;
+
+/**
+ * The collection of options in the select (what you get back when you do
+ * select.options in DOM)
+ */
+class HTMLOptionsCollection final : public nsIHTMLCollection,
+ public nsWrapperCache {
+ typedef HTMLOptionElementOrHTMLOptGroupElement HTMLOptionOrOptGroupElement;
+
+ public:
+ explicit HTMLOptionsCollection(HTMLSelectElement* aSelect);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+
+ // nsWrapperCache
+ using nsWrapperCache::GetWrapper;
+ using nsWrapperCache::GetWrapperPreserveColor;
+ using nsWrapperCache::PreserveWrapper;
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ protected:
+ virtual ~HTMLOptionsCollection() = default;
+
+ virtual JSObject* GetWrapperPreserveColorInternal() override {
+ return nsWrapperCache::GetWrapperPreserveColor();
+ }
+ virtual void PreserveWrapperInternal(
+ nsISupports* aScriptObjectHolder) override {
+ nsWrapperCache::PreserveWrapper(aScriptObjectHolder);
+ }
+
+ public:
+ virtual uint32_t Length() override;
+ virtual Element* GetElementAt(uint32_t aIndex) override;
+ virtual nsINode* GetParentObject() override;
+ DocGroup* GetDocGroup() const;
+
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(HTMLOptionsCollection,
+ nsIHTMLCollection)
+
+ // Helpers for HTMLSelectElement
+ /**
+ * Insert an option
+ * @param aOption the option to insert
+ * @param aIndex the index to insert at
+ */
+ void InsertOptionAt(mozilla::dom::HTMLOptionElement* aOption,
+ uint32_t aIndex) {
+ mElements.InsertElementAt(aIndex, aOption);
+ }
+
+ /**
+ * Remove an option
+ * @param aIndex the index of the option to remove
+ */
+ void RemoveOptionAt(uint32_t aIndex) { mElements.RemoveElementAt(aIndex); }
+
+ /**
+ * Get the option at the index
+ * @param aIndex the index
+ * @param aReturn the option returned [OUT]
+ */
+ mozilla::dom::HTMLOptionElement* ItemAsOption(uint32_t aIndex) {
+ return mElements.SafeElementAt(aIndex, nullptr);
+ }
+
+ /**
+ * Clears out all options
+ */
+ void Clear() { mElements.Clear(); }
+
+ /**
+ * Append an option to end of array
+ */
+ void AppendOption(mozilla::dom::HTMLOptionElement* aOption) {
+ mElements.AppendElement(aOption);
+ }
+
+ /**
+ * Finds the index of a given option element.
+ * If the option isn't part of the collection, return NS_ERROR_FAILURE
+ * without setting aIndex.
+ *
+ * @param aOption the option to get the index of
+ * @param aStartIndex the index to start looking at
+ * @param aForward TRUE to look forward, FALSE to look backward
+ * @return the option index
+ */
+ nsresult GetOptionIndex(Element* aOption, int32_t aStartIndex, bool aForward,
+ int32_t* aIndex);
+
+ HTMLOptionElement* GetNamedItem(const nsAString& aName) {
+ bool dummy;
+ return NamedGetter(aName, dummy);
+ }
+ HTMLOptionElement* NamedGetter(const nsAString& aName, bool& aFound);
+ virtual Element* GetFirstNamedElement(const nsAString& aName,
+ bool& aFound) override {
+ return NamedGetter(aName, aFound);
+ }
+ void Add(const HTMLOptionOrOptGroupElement& aElement,
+ const Nullable<HTMLElementOrLong>& aBefore, ErrorResult& aError);
+ void Remove(int32_t aIndex);
+ int32_t SelectedIndex();
+ void SetSelectedIndex(int32_t aSelectedIndex);
+ void IndexedSetter(uint32_t aIndex, HTMLOptionElement* aOption,
+ ErrorResult& aError);
+ virtual void GetSupportedNames(nsTArray<nsString>& aNames) override;
+ void SetLength(uint32_t aLength, ErrorResult& aError);
+
+ private:
+ /** The list of options (holds strong references). This is infallible, so
+ * various members such as InsertOptionAt are also infallible. */
+ nsTArray<RefPtr<mozilla::dom::HTMLOptionElement> > mElements;
+ /** The select element that contains this array */
+ RefPtr<HTMLSelectElement> mSelect;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_HTMLOptionsCollection_h
diff --git a/dom/html/HTMLOutputElement.cpp b/dom/html/HTMLOutputElement.cpp
new file mode 100644
index 0000000000..8a45a552f2
--- /dev/null
+++ b/dom/html/HTMLOutputElement.cpp
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLOutputElement.h"
+
+#include "mozAutoDocUpdate.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "mozilla/dom/HTMLOutputElementBinding.h"
+#include "nsContentUtils.h"
+#include "nsDOMTokenList.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Output)
+
+namespace mozilla::dom {
+
+HTMLOutputElement::HTMLOutputElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : nsGenericHTMLFormControlElement(std::move(aNodeInfo),
+ FormControlType::Output),
+ mValueModeFlag(eModeDefault),
+ mIsDoneAddingChildren(!aFromParser) {
+ AddMutationObserver(this);
+
+ // <output> is always barred from constraint validation since it is not a
+ // submittable element.
+ SetBarredFromConstraintValidation(true);
+}
+
+HTMLOutputElement::~HTMLOutputElement() = default;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLOutputElement,
+ nsGenericHTMLFormControlElement, mValidity,
+ mTokenList)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLOutputElement,
+ nsGenericHTMLFormControlElement,
+ nsIMutationObserver,
+ nsIConstraintValidation)
+
+NS_IMPL_ELEMENT_CLONE(HTMLOutputElement)
+
+void HTMLOutputElement::SetCustomValidity(const nsAString& aError) {
+ ConstraintValidation::SetCustomValidity(aError);
+}
+
+NS_IMETHODIMP
+HTMLOutputElement::Reset() {
+ mValueModeFlag = eModeDefault;
+ // We can't pass mDefaultValue, because it'll be truncated when
+ // the element's descendants are changed, so pass a copy instead.
+ const nsAutoString currentDefaultValue(mDefaultValue);
+ return nsContentUtils::SetNodeTextContent(this, currentDefaultValue, true);
+}
+
+bool HTMLOutputElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::_for) {
+ aResult.ParseAtomArray(aValue);
+ return true;
+ }
+ }
+
+ return nsGenericHTMLFormControlElement::ParseAttribute(
+ aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLOutputElement::DoneAddingChildren(bool aHaveNotified) {
+ mIsDoneAddingChildren = true;
+ // We should update DefaultValue, after parsing is done.
+ DescendantsChanged();
+}
+
+void HTMLOutputElement::GetValue(nsAString& aValue) const {
+ nsContentUtils::GetNodeTextContent(this, true, aValue);
+}
+
+void HTMLOutputElement::SetValue(const nsAString& aValue, ErrorResult& aRv) {
+ mValueModeFlag = eModeValue;
+ aRv = nsContentUtils::SetNodeTextContent(this, aValue, true);
+}
+
+void HTMLOutputElement::SetDefaultValue(const nsAString& aDefaultValue,
+ ErrorResult& aRv) {
+ mDefaultValue = aDefaultValue;
+ if (mValueModeFlag == eModeDefault) {
+ // We can't pass mDefaultValue, because it'll be truncated when
+ // the element's descendants are changed.
+ aRv = nsContentUtils::SetNodeTextContent(this, aDefaultValue, true);
+ }
+}
+
+nsDOMTokenList* HTMLOutputElement::HtmlFor() {
+ if (!mTokenList) {
+ mTokenList = new nsDOMTokenList(this, nsGkAtoms::_for);
+ }
+ return mTokenList;
+}
+
+void HTMLOutputElement::DescendantsChanged() {
+ if (mIsDoneAddingChildren && mValueModeFlag == eModeDefault) {
+ nsContentUtils::GetNodeTextContent(this, true, mDefaultValue);
+ }
+}
+
+// nsIMutationObserver
+
+void HTMLOutputElement::CharacterDataChanged(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {
+ DescendantsChanged();
+}
+
+void HTMLOutputElement::ContentAppended(nsIContent* aFirstNewContent) {
+ DescendantsChanged();
+}
+
+void HTMLOutputElement::ContentInserted(nsIContent* aChild) {
+ DescendantsChanged();
+}
+
+void HTMLOutputElement::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ DescendantsChanged();
+}
+
+JSObject* HTMLOutputElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLOutputElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLOutputElement.h b/dom/html/HTMLOutputElement.h
new file mode 100644
index 0000000000..8a658a1594
--- /dev/null
+++ b/dom/html/HTMLOutputElement.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLOutputElement_h
+#define mozilla_dom_HTMLOutputElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/ConstraintValidation.h"
+#include "nsGenericHTMLElement.h"
+#include "nsStubMutationObserver.h"
+
+namespace mozilla::dom {
+
+class FormData;
+
+class HTMLOutputElement final : public nsGenericHTMLFormControlElement,
+ public nsStubMutationObserver,
+ public ConstraintValidation {
+ public:
+ using ConstraintValidation::GetValidationMessage;
+
+ explicit HTMLOutputElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser = NOT_FROM_PARSER);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIFormControl
+ NS_IMETHOD Reset() override;
+ // The output element is not submittable.
+ NS_IMETHOD SubmitNamesValues(FormData* aFormData) override { return NS_OK; }
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ void DoneAddingChildren(bool aHaveNotified) override;
+
+ // This function is called when a callback function from nsIMutationObserver
+ // has to be used to update the defaultValue attribute.
+ void DescendantsChanged();
+
+ // nsIMutationObserver
+ NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLOutputElement,
+ nsGenericHTMLFormControlElement)
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL
+ nsDOMTokenList* HtmlFor();
+
+ void GetName(nsAString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); }
+
+ void SetName(const nsAString& aName, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aRv);
+ }
+
+ void GetType(nsAString& aType) { aType.AssignLiteral("output"); }
+
+ void GetDefaultValue(nsAString& aDefaultValue) {
+ aDefaultValue = mDefaultValue;
+ }
+
+ void SetDefaultValue(const nsAString& aDefaultValue, ErrorResult& aRv);
+
+ void GetValue(nsAString& aValue) const;
+ void SetValue(const nsAString& aValue, ErrorResult& aRv);
+
+ // nsIConstraintValidation::WillValidate is fine.
+ // nsIConstraintValidation::Validity() is fine.
+ // nsIConstraintValidation::GetValidationMessage() is fine.
+ // nsIConstraintValidation::CheckValidity() is fine.
+ void SetCustomValidity(const nsAString& aError);
+
+ protected:
+ virtual ~HTMLOutputElement();
+
+ enum ValueModeFlag { eModeDefault, eModeValue };
+
+ ValueModeFlag mValueModeFlag;
+ bool mIsDoneAddingChildren;
+ nsString mDefaultValue;
+ RefPtr<nsDOMTokenList> mTokenList;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLOutputElement_h
diff --git a/dom/html/HTMLParagraphElement.cpp b/dom/html/HTMLParagraphElement.cpp
new file mode 100644
index 0000000000..19f732446d
--- /dev/null
+++ b/dom/html/HTMLParagraphElement.cpp
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLParagraphElement.h"
+#include "mozilla/dom/HTMLParagraphElementBinding.h"
+
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsStyleConsts.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Paragraph)
+
+namespace mozilla::dom {
+
+HTMLParagraphElement::~HTMLParagraphElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLParagraphElement)
+
+bool HTMLParagraphElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aAttribute == nsGkAtoms::align && aNamespaceID == kNameSpaceID_None) {
+ return ParseDivAlignValue(aValue, aResult);
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLParagraphElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLParagraphElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry* const map[] = {
+ sDivAlignAttributeMap,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLParagraphElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+JSObject* HTMLParagraphElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLParagraphElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLParagraphElement.h b/dom/html/HTMLParagraphElement.h
new file mode 100644
index 0000000000..e768465760
--- /dev/null
+++ b/dom/html/HTMLParagraphElement.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLParagraphElement_h
+#define mozilla_dom_HTMLParagraphElement_h
+
+#include "mozilla/Attributes.h"
+
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLParagraphElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLParagraphElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLParagraphElement,
+ nsGenericHTMLElement)
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // WebIDL API
+ void GetAlign(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::align, aValue); }
+ void SetAlign(const nsAString& aValue, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::align, aValue, rv);
+ }
+
+ protected:
+ virtual ~HTMLParagraphElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLParagraphElement_h
diff --git a/dom/html/HTMLPictureElement.cpp b/dom/html/HTMLPictureElement.cpp
new file mode 100644
index 0000000000..45b1e4e3e3
--- /dev/null
+++ b/dom/html/HTMLPictureElement.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLPictureElement.h"
+#include "mozilla/dom/HTMLPictureElementBinding.h"
+#include "mozilla/dom/HTMLImageElement.h"
+#include "mozilla/dom/HTMLSourceElement.h"
+
+// Expand NS_IMPL_NS_NEW_HTML_ELEMENT(Picture) to add pref check.
+nsGenericHTMLElement* NS_NewHTMLPictureElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
+ auto* nim = nodeInfo->NodeInfoManager();
+ return new (nim) mozilla::dom::HTMLPictureElement(nodeInfo.forget());
+}
+
+namespace mozilla::dom {
+
+HTMLPictureElement::HTMLPictureElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLPictureElement::~HTMLPictureElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLPictureElement)
+
+void HTMLPictureElement::RemoveChildNode(nsIContent* aKid, bool aNotify) {
+ MOZ_ASSERT(aKid);
+
+ if (auto* img = HTMLImageElement::FromNode(aKid)) {
+ img->PictureSourceRemoved();
+ } else if (auto* source = HTMLSourceElement::FromNode(aKid)) {
+ // Find all img siblings after this <source> to notify them of its demise
+ nsCOMPtr<nsIContent> nextSibling = source->GetNextSibling();
+ if (nextSibling && nextSibling->GetParentNode() == this) {
+ do {
+ if (auto* img = HTMLImageElement::FromNode(nextSibling)) {
+ img->PictureSourceRemoved(source);
+ }
+ } while ((nextSibling = nextSibling->GetNextSibling()));
+ }
+ }
+
+ nsGenericHTMLElement::RemoveChildNode(aKid, aNotify);
+}
+
+void HTMLPictureElement::InsertChildBefore(nsIContent* aKid,
+ nsIContent* aBeforeThis,
+ bool aNotify, ErrorResult& aRv) {
+ nsGenericHTMLElement::InsertChildBefore(aKid, aBeforeThis, aNotify, aRv);
+ if (aRv.Failed() || !aKid) {
+ return;
+ }
+
+ if (auto* img = HTMLImageElement::FromNode(aKid)) {
+ img->PictureSourceAdded();
+ } else if (auto* source = HTMLSourceElement::FromNode(aKid)) {
+ // Find all img siblings after this <source> to notify them of its insertion
+ nsCOMPtr<nsIContent> nextSibling = source->GetNextSibling();
+ if (nextSibling && nextSibling->GetParentNode() == this) {
+ do {
+ if (auto* img = HTMLImageElement::FromNode(nextSibling)) {
+ img->PictureSourceAdded(source);
+ }
+ } while ((nextSibling = nextSibling->GetNextSibling()));
+ }
+ }
+}
+
+JSObject* HTMLPictureElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLPictureElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLPictureElement.h b/dom/html/HTMLPictureElement.h
new file mode 100644
index 0000000000..5ec2a3abd0
--- /dev/null
+++ b/dom/html/HTMLPictureElement.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLPictureElement_h
+#define mozilla_dom_HTMLPictureElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla {
+class ErrorResult;
+namespace dom {
+
+class HTMLPictureElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLPictureElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLPictureElement, nsGenericHTMLElement)
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+ virtual void RemoveChildNode(nsIContent* aKid, bool aNotify) override;
+ virtual void InsertChildBefore(nsIContent* aKid, nsIContent* aBeforeThis,
+ bool aNotify, ErrorResult& aRv) override;
+
+ protected:
+ virtual ~HTMLPictureElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_HTMLPictureElement_h
diff --git a/dom/html/HTMLPreElement.cpp b/dom/html/HTMLPreElement.cpp
new file mode 100644
index 0000000000..13628400d0
--- /dev/null
+++ b/dom/html/HTMLPreElement.cpp
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLPreElement.h"
+#include "mozilla/dom/HTMLPreElementBinding.h"
+
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsAttrValueInlines.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Pre)
+
+namespace mozilla::dom {
+
+HTMLPreElement::~HTMLPreElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLPreElement)
+
+bool HTMLPreElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::width) {
+ return aResult.ParseIntValue(aValue);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLPreElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ // wrap: empty
+ if (aBuilder.GetAttr(nsGkAtoms::wrap)) {
+ // Equivalent to expanding `white-space: pre-wrap`
+ aBuilder.SetKeywordValue(eCSSProperty_white_space_collapse,
+ StyleWhiteSpaceCollapse::Preserve);
+ aBuilder.SetKeywordValue(eCSSProperty_text_wrap_mode,
+ StyleTextWrapMode::Wrap);
+ }
+
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLPreElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ if (!mNodeInfo->Equals(nsGkAtoms::pre)) {
+ return nsGenericHTMLElement::IsAttributeMapped(aAttribute);
+ }
+
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::wrap},
+ {nullptr},
+ };
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLPreElement::GetAttributeMappingFunction() const {
+ if (!mNodeInfo->Equals(nsGkAtoms::pre)) {
+ return nsGenericHTMLElement::GetAttributeMappingFunction();
+ }
+
+ return &MapAttributesIntoRule;
+}
+
+JSObject* HTMLPreElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLPreElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLPreElement.h b/dom/html/HTMLPreElement.h
new file mode 100644
index 0000000000..4841f2ff15
--- /dev/null
+++ b/dom/html/HTMLPreElement.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLPreElement_h
+#define mozilla_dom_HTMLPreElement_h
+
+#include "mozilla/Attributes.h"
+
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLPreElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLPreElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLPreElement, nsGenericHTMLElement)
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // WebIDL API
+ int32_t Width() const { return GetIntAttr(nsGkAtoms::width, 0); }
+ void SetWidth(int32_t aWidth, mozilla::ErrorResult& rv) {
+ rv = SetIntAttr(nsGkAtoms::width, aWidth);
+ }
+
+ protected:
+ virtual ~HTMLPreElement();
+
+ JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*>) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLPreElement_h
diff --git a/dom/html/HTMLProgressElement.cpp b/dom/html/HTMLProgressElement.cpp
new file mode 100644
index 0000000000..6ad70c1f39
--- /dev/null
+++ b/dom/html/HTMLProgressElement.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLProgressElement.h"
+#include "mozilla/dom/HTMLProgressElementBinding.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Progress)
+
+namespace mozilla::dom {
+
+HTMLProgressElement::HTMLProgressElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ // We start out indeterminate
+ AddStatesSilently(ElementState::INDETERMINATE);
+}
+
+HTMLProgressElement::~HTMLProgressElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLProgressElement)
+
+bool HTMLProgressElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max) {
+ return aResult.ParseDoubleValue(aValue);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLProgressElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::value) {
+ const bool indeterminate =
+ !aValue || aValue->Type() != nsAttrValue::eDoubleValue;
+ SetStates(ElementState::INDETERMINATE, indeterminate, aNotify);
+ }
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+double HTMLProgressElement::Value() const {
+ const nsAttrValue* attrValue = mAttrs.GetAttr(nsGkAtoms::value);
+ if (!attrValue || attrValue->Type() != nsAttrValue::eDoubleValue ||
+ attrValue->GetDoubleValue() < 0.0) {
+ return 0.0;
+ }
+
+ return std::min(attrValue->GetDoubleValue(), Max());
+}
+
+double HTMLProgressElement::Max() const {
+ const nsAttrValue* attrMax = mAttrs.GetAttr(nsGkAtoms::max);
+ if (!attrMax || attrMax->Type() != nsAttrValue::eDoubleValue ||
+ attrMax->GetDoubleValue() <= 0.0) {
+ return 1.0;
+ }
+
+ return attrMax->GetDoubleValue();
+}
+
+double HTMLProgressElement::Position() const {
+ if (State().HasState(ElementState::INDETERMINATE)) {
+ return -1.0;
+ }
+
+ return Value() / Max();
+}
+
+JSObject* HTMLProgressElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLProgressElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLProgressElement.h b/dom/html/HTMLProgressElement.h
new file mode 100644
index 0000000000..e0de536282
--- /dev/null
+++ b/dom/html/HTMLProgressElement.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLProgressElement_h
+#define mozilla_dom_HTMLProgressElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsAttrValue.h"
+#include "nsAttrValueInlines.h"
+#include <algorithm>
+
+namespace mozilla::dom {
+
+class HTMLProgressElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLProgressElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ // WebIDL
+ double Value() const;
+ void SetValue(double aValue, ErrorResult& aRv) {
+ SetDoubleAttr(nsGkAtoms::value, aValue, aRv);
+ }
+ double Max() const;
+ void SetMax(double aValue, ErrorResult& aRv) {
+ // https://html.spec.whatwg.org/multipage/form-elements.html#dom-progress-max
+ // The max IDL attribute must reflect the content attribute of the same
+ // name, limited to only positive numbers.
+ SetDoubleAttr<Reflection::OnlyPositive>(nsGkAtoms::max, aValue, aRv);
+ }
+ double Position() const;
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLProgressElement, progress);
+
+ protected:
+ virtual ~HTMLProgressElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLProgressElement_h
diff --git a/dom/html/HTMLScriptElement.cpp b/dom/html/HTMLScriptElement.cpp
new file mode 100644
index 0000000000..006bff23a7
--- /dev/null
+++ b/dom/html/HTMLScriptElement.cpp
@@ -0,0 +1,254 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsAttrValue.h"
+#include "nsAttrValueOrString.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "mozilla/dom/Document.h"
+#include "nsNetUtil.h"
+#include "nsContentUtils.h"
+#include "nsUnicharUtils.h" // for nsCaseInsensitiveStringComparator()
+#include "nsIScriptContext.h"
+#include "nsIScriptGlobalObject.h"
+#include "nsServiceManagerUtils.h"
+#include "nsError.h"
+#include "nsTArray.h"
+#include "nsDOMJSUtils.h"
+#include "nsIScriptError.h"
+#include "nsISupportsImpl.h"
+#include "nsDOMTokenList.h"
+#include "mozilla/dom/FetchPriority.h"
+#include "mozilla/dom/HTMLScriptElement.h"
+#include "mozilla/dom/HTMLScriptElementBinding.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/StaticPrefs_dom.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Script)
+
+using JS::loader::ScriptKind;
+
+namespace mozilla::dom {
+
+JSObject* HTMLScriptElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLScriptElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+HTMLScriptElement::HTMLScriptElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : nsGenericHTMLElement(std::move(aNodeInfo)), ScriptElement(aFromParser) {
+ AddMutationObserver(this);
+}
+
+HTMLScriptElement::~HTMLScriptElement() = default;
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLScriptElement,
+ nsGenericHTMLElement,
+ nsIScriptLoaderObserver,
+ nsIScriptElement,
+ nsIMutationObserver)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLScriptElement, nsGenericHTMLElement,
+ mBlocking)
+
+nsresult HTMLScriptElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsInComposedDoc()) {
+ MaybeProcessScript();
+ }
+
+ return NS_OK;
+}
+
+bool HTMLScriptElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::crossorigin) {
+ ParseCORSValue(aValue, aResult);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::integrity) {
+ aResult.ParseStringOrAtom(aValue);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::fetchpriority) {
+ ParseFetchPriority(aValue, aResult);
+ return true;
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+nsresult HTMLScriptElement::Clone(dom::NodeInfo* aNodeInfo,
+ nsINode** aResult) const {
+ *aResult = nullptr;
+
+ HTMLScriptElement* it = new (aNodeInfo->NodeInfoManager())
+ HTMLScriptElement(do_AddRef(aNodeInfo), NOT_FROM_PARSER);
+
+ nsCOMPtr<nsINode> kungFuDeathGrip = it;
+ nsresult rv = const_cast<HTMLScriptElement*>(this)->CopyInnerTo(it);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The clone should be marked evaluated if we are.
+ it->mAlreadyStarted = mAlreadyStarted;
+ it->mLineNumber = mLineNumber;
+ it->mMalformed = mMalformed;
+
+ kungFuDeathGrip.swap(*aResult);
+
+ return NS_OK;
+}
+
+void HTMLScriptElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (nsGkAtoms::async == aName && kNameSpaceID_None == aNamespaceID) {
+ mForceAsync = false;
+ }
+ if (nsGkAtoms::src == aName && kNameSpaceID_None == aNamespaceID) {
+ mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue ? aValue->GetStringValue() : EmptyString(),
+ aMaybeScriptedPrincipal);
+ }
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNamespaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void HTMLScriptElement::GetInnerHTML(nsAString& aInnerHTML,
+ OOMReporter& aError) {
+ if (!nsContentUtils::GetNodeTextContent(this, false, aInnerHTML, fallible)) {
+ aError.ReportOOM();
+ }
+}
+
+void HTMLScriptElement::SetInnerHTML(const nsAString& aInnerHTML,
+ nsIPrincipal* aScriptedPrincipal,
+ ErrorResult& aError) {
+ aError = nsContentUtils::SetNodeTextContent(this, aInnerHTML, true);
+}
+
+void HTMLScriptElement::GetText(nsAString& aValue, ErrorResult& aRv) const {
+ if (!nsContentUtils::GetNodeTextContent(this, false, aValue, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ }
+}
+
+void HTMLScriptElement::SetText(const nsAString& aValue, ErrorResult& aRv) {
+ aRv = nsContentUtils::SetNodeTextContent(this, aValue, true);
+}
+
+// variation of this code in SVGScriptElement - check if changes
+// need to be transfered when modifying
+
+void HTMLScriptElement::GetScriptText(nsAString& text) const {
+ GetText(text, IgnoreErrors());
+}
+
+void HTMLScriptElement::GetScriptCharset(nsAString& charset) {
+ GetCharset(charset);
+}
+
+void HTMLScriptElement::FreezeExecutionAttrs(const Document* aOwnerDoc) {
+ if (mFrozen) {
+ return;
+ }
+
+ // Determine whether this is a(n) classic/module/importmap script.
+ DetermineKindFromType(aOwnerDoc);
+
+ // variation of this code in SVGScriptElement - check if changes
+ // need to be transfered when modifying. Note that we don't use GetSrc here
+ // because it will return the base URL when the attr value is "".
+ nsAutoString src;
+ if (GetAttr(nsGkAtoms::src, src)) {
+ // Empty src should be treated as invalid URL.
+ if (!src.IsEmpty()) {
+ nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(mUri), src,
+ OwnerDoc(), GetBaseURI());
+
+ if (!mUri) {
+ AutoTArray<nsString, 2> params = {u"src"_ns, src};
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "HTML"_ns, OwnerDoc(),
+ nsContentUtils::eDOM_PROPERTIES, "ScriptSourceInvalidUri", params,
+ nullptr, u""_ns, GetScriptLineNumber(),
+ GetScriptColumnNumber().oneOriginValue());
+ }
+ } else {
+ AutoTArray<nsString, 1> params = {u"src"_ns};
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "HTML"_ns, OwnerDoc(),
+ nsContentUtils::eDOM_PROPERTIES, "ScriptSourceEmpty", params, nullptr,
+ u""_ns, GetScriptLineNumber(),
+ GetScriptColumnNumber().oneOriginValue());
+ }
+
+ // At this point mUri will be null for invalid URLs.
+ mExternal = true;
+ }
+
+ bool async = (mExternal || mKind == ScriptKind::eModule) && Async();
+ bool defer = mExternal && Defer();
+
+ mDefer = !async && defer;
+ mAsync = async;
+
+ mFrozen = true;
+}
+
+CORSMode HTMLScriptElement::GetCORSMode() const {
+ return AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin));
+}
+
+FetchPriority HTMLScriptElement::GetFetchPriority() const {
+ return nsGenericHTMLElement::GetFetchPriority();
+}
+
+mozilla::dom::ReferrerPolicy HTMLScriptElement::GetReferrerPolicy() {
+ return GetReferrerPolicyAsEnum();
+}
+
+bool HTMLScriptElement::HasScriptContent() {
+ return (mFrozen ? mExternal : HasAttr(nsGkAtoms::src)) ||
+ nsContentUtils::HasNonEmptyTextContent(this);
+}
+
+// https://html.spec.whatwg.org/multipage/scripting.html#dom-script-supports
+/* static */
+bool HTMLScriptElement::Supports(const GlobalObject& aGlobal,
+ const nsAString& aType) {
+ nsAutoString type(aType);
+ return aType.EqualsLiteral("classic") || aType.EqualsLiteral("module") ||
+
+ aType.EqualsLiteral("importmap");
+}
+
+nsDOMTokenList* HTMLScriptElement::Blocking() {
+ if (!mBlocking) {
+ mBlocking =
+ new nsDOMTokenList(this, nsGkAtoms::blocking, sSupportedBlockingValues);
+ }
+ return mBlocking;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLScriptElement.h b/dom/html/HTMLScriptElement.h
new file mode 100644
index 0000000000..db09e247bc
--- /dev/null
+++ b/dom/html/HTMLScriptElement.h
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLScriptElement_h
+#define mozilla_dom_HTMLScriptElement_h
+
+#include "mozilla/dom/FetchPriority.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/ScriptElement.h"
+#include "nsGenericHTMLElement.h"
+#include "nsStringFwd.h"
+
+class nsDOMTokenList;
+
+namespace mozilla::dom {
+
+class HTMLScriptElement final : public nsGenericHTMLElement,
+ public ScriptElement {
+ public:
+ using Element::GetText;
+
+ HTMLScriptElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // CC
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLScriptElement,
+ nsGenericHTMLElement)
+
+ void GetInnerHTML(nsAString& aInnerHTML, OOMReporter& aError) override;
+ virtual void SetInnerHTML(const nsAString& aInnerHTML,
+ nsIPrincipal* aSubjectPrincipal,
+ mozilla::ErrorResult& aError) override;
+
+ // nsIScriptElement
+ virtual void GetScriptText(nsAString& text) const override;
+ virtual void GetScriptCharset(nsAString& charset) override;
+ virtual void FreezeExecutionAttrs(const Document* aOwnerDoc) override;
+ virtual CORSMode GetCORSMode() const override;
+ virtual FetchPriority GetFetchPriority() const override;
+ virtual mozilla::dom::ReferrerPolicy GetReferrerPolicy() override;
+
+ // nsIContent
+ virtual nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // Element
+ virtual void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+
+ // WebIDL
+ void GetText(nsAString& aValue, ErrorResult& aRv) const;
+
+ void SetText(const nsAString& aValue, ErrorResult& aRv);
+
+ void GetCharset(nsAString& aCharset) {
+ GetHTMLAttr(nsGkAtoms::charset, aCharset);
+ }
+ void SetCharset(const nsAString& aCharset, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::charset, aCharset, aRv);
+ }
+
+ bool Defer() { return GetBoolAttr(nsGkAtoms::defer); }
+ void SetDefer(bool aDefer, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::defer, aDefer, aRv);
+ }
+
+ void GetSrc(nsAString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); }
+ void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal,
+ ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aRv);
+ }
+
+ void GetType(nsAString& aType) { GetHTMLAttr(nsGkAtoms::type, aType); }
+ void SetType(const nsAString& aType, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::type, aType, aRv);
+ }
+
+ void GetHtmlFor(nsAString& aHtmlFor) {
+ GetHTMLAttr(nsGkAtoms::_for, aHtmlFor);
+ }
+ void SetHtmlFor(const nsAString& aHtmlFor, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::_for, aHtmlFor, aRv);
+ }
+
+ void GetEvent(nsAString& aEvent) { GetHTMLAttr(nsGkAtoms::event, aEvent); }
+ void SetEvent(const nsAString& aEvent, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::event, aEvent, aRv);
+ }
+
+ bool Async() { return mForceAsync || GetBoolAttr(nsGkAtoms::async); }
+
+ void SetAsync(bool aValue, ErrorResult& aRv) {
+ mForceAsync = false;
+ SetHTMLBoolAttr(nsGkAtoms::async, aValue, aRv);
+ }
+
+ bool NoModule() { return GetBoolAttr(nsGkAtoms::nomodule); }
+
+ void SetNoModule(bool aValue, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::nomodule, aValue, aRv);
+ }
+
+ void GetCrossOrigin(nsAString& aResult) {
+ // Null for both missing and invalid defaults is ok, since we
+ // always parse to an enum value, so we don't need an invalid
+ // default, and we _want_ the missing default to be null.
+ GetEnumAttr(nsGkAtoms::crossorigin, nullptr, aResult);
+ }
+ void SetCrossOrigin(const nsAString& aCrossOrigin, ErrorResult& aError) {
+ SetOrRemoveNullableStringAttr(nsGkAtoms::crossorigin, aCrossOrigin, aError);
+ }
+ void GetIntegrity(nsAString& aIntegrity) {
+ GetHTMLAttr(nsGkAtoms::integrity, aIntegrity);
+ }
+ void SetIntegrity(const nsAString& aIntegrity, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::integrity, aIntegrity, aRv);
+ }
+ void SetReferrerPolicy(const nsAString& aReferrerPolicy,
+ ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::referrerpolicy, aReferrerPolicy, aError);
+ }
+ void GetReferrerPolicy(nsAString& aReferrerPolicy) {
+ GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrerPolicy);
+ }
+
+ nsDOMTokenList* Blocking();
+
+ // Required for the webidl-binding because `GetFetchPriority` is overloaded.
+ using nsGenericHTMLElement::GetFetchPriority;
+
+ [[nodiscard]] static bool Supports(const GlobalObject& aGlobal,
+ const nsAString& aType);
+
+ protected:
+ virtual ~HTMLScriptElement();
+
+ virtual bool GetAsyncState() override { return Async(); }
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // nsIScriptElement
+ nsIContent* GetAsContent() override { return this; }
+
+ // ScriptElement
+ virtual bool HasScriptContent() override;
+
+ RefPtr<nsDOMTokenList> mBlocking;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLScriptElement_h
diff --git a/dom/html/HTMLSelectElement.cpp b/dom/html/HTMLSelectElement.cpp
new file mode 100644
index 0000000000..18bf2b79b2
--- /dev/null
+++ b/dom/html/HTMLSelectElement.cpp
@@ -0,0 +1,1645 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLSelectElement.h"
+
+#include "mozAutoDocUpdate.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/FormData.h"
+#include "mozilla/dom/HTMLOptGroupElement.h"
+#include "mozilla/dom/HTMLOptionElement.h"
+#include "mozilla/dom/HTMLSelectElementBinding.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Unused.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsContentList.h"
+#include "nsContentUtils.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsComboboxControlFrame.h"
+#include "mozilla/dom/Document.h"
+#include "nsIFormControlFrame.h"
+#include "nsIFrame.h"
+#include "nsListControlFrame.h"
+#include "nsISelectControlFrame.h"
+#include "nsLayoutUtils.h"
+#include "mozilla/PresState.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStyleConsts.h"
+#include "nsTextNode.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Select)
+
+namespace mozilla::dom {
+
+//----------------------------------------------------------------------
+//
+// SafeOptionListMutation
+//
+
+SafeOptionListMutation::SafeOptionListMutation(nsIContent* aSelect,
+ nsIContent* aParent,
+ nsIContent* aKid,
+ uint32_t aIndex, bool aNotify)
+ : mSelect(HTMLSelectElement::FromNodeOrNull(aSelect)),
+ mTopLevelMutation(false),
+ mNeedsRebuild(false),
+ mNotify(aNotify) {
+ if (mSelect) {
+ mInitialSelectedOption = mSelect->Item(mSelect->SelectedIndex());
+ mTopLevelMutation = !mSelect->mMutating;
+ if (mTopLevelMutation) {
+ mSelect->mMutating = true;
+ } else {
+ // This is very unfortunate, but to handle mutation events properly,
+ // option list must be up-to-date before inserting or removing options.
+ // Fortunately this is called only if mutation event listener
+ // adds or removes options.
+ mSelect->RebuildOptionsArray(mNotify);
+ }
+ nsresult rv;
+ if (aKid) {
+ rv = mSelect->WillAddOptions(aKid, aParent, aIndex, mNotify);
+ } else {
+ rv = mSelect->WillRemoveOptions(aParent, aIndex, mNotify);
+ }
+ mNeedsRebuild = NS_FAILED(rv);
+ }
+}
+
+SafeOptionListMutation::~SafeOptionListMutation() {
+ if (mSelect) {
+ if (mNeedsRebuild || (mTopLevelMutation && mGuard.Mutated(1))) {
+ mSelect->RebuildOptionsArray(true);
+ }
+ if (mTopLevelMutation) {
+ mSelect->mMutating = false;
+ }
+ if (mSelect->Item(mSelect->SelectedIndex()) != mInitialSelectedOption) {
+ // We must have triggered the SelectSomething() codepath, which can cause
+ // our validity to change. Unfortunately, our attempt to update validity
+ // in that case may not have worked correctly, because we actually call it
+ // before we have inserted the new <option>s into the DOM! Go ahead and
+ // update validity here as needed, because by now we know our <option>s
+ // are where they should be.
+ mSelect->UpdateValueMissingValidityState();
+ mSelect->UpdateValidityElementStates(mNotify);
+ }
+#ifdef DEBUG
+ mSelect->VerifyOptionsArray();
+#endif
+ }
+}
+
+//----------------------------------------------------------------------
+//
+// HTMLSelectElement
+//
+
+// construction, destruction
+
+HTMLSelectElement::HTMLSelectElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : nsGenericHTMLFormControlElementWithState(
+ std::move(aNodeInfo), aFromParser, FormControlType::Select),
+ mOptions(new HTMLOptionsCollection(this)),
+ mAutocompleteAttrState(nsContentUtils::eAutocompleteAttrState_Unknown),
+ mAutocompleteInfoState(nsContentUtils::eAutocompleteAttrState_Unknown),
+ mIsDoneAddingChildren(!aFromParser),
+ mDisabledChanged(false),
+ mMutating(false),
+ mInhibitStateRestoration(!!(aFromParser & FROM_PARSER_FRAGMENT)),
+ mUserInteracted(false),
+ mDefaultSelectionSet(false),
+ mIsOpenInParentProcess(false),
+ mNonOptionChildren(0),
+ mOptGroupCount(0),
+ mSelectedIndex(-1) {
+ SetHasWeirdParserInsertionMode();
+
+ // DoneAddingChildren() will be called later if it's from the parser,
+ // otherwise it is
+
+ // Set up our default state: enabled, optional, and valid.
+ AddStatesSilently(ElementState::ENABLED | ElementState::OPTIONAL_ |
+ ElementState::VALID);
+}
+
+// ISupports
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLSelectElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(
+ HTMLSelectElement, nsGenericHTMLFormControlElementWithState)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOptions)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedOptions)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(
+ HTMLSelectElement, nsGenericHTMLFormControlElementWithState)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedOptions)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(
+ HTMLSelectElement, nsGenericHTMLFormControlElementWithState,
+ nsIConstraintValidation)
+
+// nsIDOMHTMLSelectElement
+
+NS_IMPL_ELEMENT_CLONE(HTMLSelectElement)
+
+void HTMLSelectElement::SetCustomValidity(const nsAString& aError) {
+ ConstraintValidation::SetCustomValidity(aError);
+ UpdateValidityElementStates(true);
+}
+
+// https://html.spec.whatwg.org/multipage/input.html#dom-input-showpicker
+void HTMLSelectElement::ShowPicker(ErrorResult& aRv) {
+ // Step 1. If this is not mutable, then throw an "InvalidStateError"
+ // DOMException.
+ if (IsDisabled()) {
+ return aRv.ThrowInvalidStateError("This select is disabled.");
+ }
+
+ // Step 2. If this's relevant settings object's origin is not same origin with
+ // this's relevant settings object's top-level origin, and this is a select
+ // element, [...], then throw a "SecurityError" DOMException.
+ nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
+ WindowGlobalChild* windowGlobalChild =
+ window ? window->GetWindowGlobalChild() : nullptr;
+ if (!windowGlobalChild || !windowGlobalChild->SameOriginWithTop()) {
+ return aRv.ThrowSecurityError(
+ "Call was blocked because the current origin isn't same-origin with "
+ "top.");
+ }
+
+ // Step 3. If this's relevant global object does not have transient
+ // activation, then throw a "NotAllowedError" DOMException.
+ if (!OwnerDoc()->HasValidTransientUserGestureActivation()) {
+ return aRv.ThrowNotAllowedError(
+ "Call was blocked due to lack of user activation.");
+ }
+
+ // Step 4. If this is a select element, and this is not being rendered, then
+ // throw a "NotSupportedError" DOMException.
+
+ // Flush frames so that IsRendered returns up-to-date results.
+ Unused << GetPrimaryFrame(FlushType::Frames);
+ if (!IsRendered()) {
+ return aRv.ThrowNotSupportedError("This select isn't being rendered.");
+ }
+
+ // Step 5. Show the picker, if applicable, for this.
+#if !defined(ANDROID)
+ if (!IsCombobox()) {
+ return;
+ }
+#endif
+ if (!OpenInParentProcess()) {
+ RefPtr<Document> doc = OwnerDoc();
+ nsContentUtils::DispatchChromeEvent(doc, this, u"mozshowdropdown"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+ }
+}
+
+void HTMLSelectElement::GetAutocomplete(DOMString& aValue) {
+ const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete);
+
+ mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute(
+ attributeVal, aValue, mAutocompleteAttrState);
+}
+
+void HTMLSelectElement::GetAutocompleteInfo(AutocompleteInfo& aInfo) {
+ const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete);
+ mAutocompleteInfoState = nsContentUtils::SerializeAutocompleteAttribute(
+ attributeVal, aInfo, mAutocompleteInfoState, true);
+}
+
+void HTMLSelectElement::InsertChildBefore(nsIContent* aKid,
+ nsIContent* aBeforeThis, bool aNotify,
+ ErrorResult& aRv) {
+ const uint32_t index =
+ aBeforeThis ? *ComputeIndexOf(aBeforeThis) : GetChildCount();
+ SafeOptionListMutation safeMutation(this, this, aKid, index, aNotify);
+ nsGenericHTMLFormControlElementWithState::InsertChildBefore(aKid, aBeforeThis,
+ aNotify, aRv);
+ if (aRv.Failed()) {
+ safeMutation.MutationFailed();
+ }
+}
+
+void HTMLSelectElement::RemoveChildNode(nsIContent* aKid, bool aNotify) {
+ SafeOptionListMutation safeMutation(this, this, nullptr,
+ *ComputeIndexOf(aKid), aNotify);
+ nsGenericHTMLFormControlElementWithState::RemoveChildNode(aKid, aNotify);
+}
+
+void HTMLSelectElement::InsertOptionsIntoList(nsIContent* aOptions,
+ int32_t aListIndex,
+ int32_t aDepth, bool aNotify) {
+ MOZ_ASSERT(aDepth == 0 || aDepth == 1);
+ int32_t insertIndex = aListIndex;
+
+ HTMLOptionElement* optElement = HTMLOptionElement::FromNode(aOptions);
+ if (optElement) {
+ mOptions->InsertOptionAt(optElement, insertIndex);
+ insertIndex++;
+ } else if (aDepth == 0) {
+ // If it's at the top level, then we just found out there are non-options
+ // at the top level, which will throw off the insert count
+ mNonOptionChildren++;
+
+ // Deal with optgroups
+ if (aOptions->IsHTMLElement(nsGkAtoms::optgroup)) {
+ mOptGroupCount++;
+
+ for (nsIContent* child = aOptions->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ optElement = HTMLOptionElement::FromNode(child);
+ if (optElement) {
+ mOptions->InsertOptionAt(optElement, insertIndex);
+ insertIndex++;
+ }
+ }
+ }
+ } // else ignore even if optgroup; we want to ignore nested optgroups.
+
+ // Deal with the selected list
+ if (insertIndex - aListIndex) {
+ // Fix the currently selected index
+ if (aListIndex <= mSelectedIndex) {
+ mSelectedIndex += (insertIndex - aListIndex);
+ OnSelectionChanged();
+ }
+
+ // Get the frame stuff for notification. No need to flush here
+ // since if there's no frame for the select yet the select will
+ // get into the right state once it's created.
+ nsISelectControlFrame* selectFrame = nullptr;
+ AutoWeakFrame weakSelectFrame;
+ bool didGetFrame = false;
+
+ // Actually select the options if the added options warrant it
+ for (int32_t i = aListIndex; i < insertIndex; i++) {
+ // Notify the frame that the option is added
+ if (!didGetFrame || (selectFrame && !weakSelectFrame.IsAlive())) {
+ selectFrame = GetSelectFrame();
+ weakSelectFrame = do_QueryFrame(selectFrame);
+ didGetFrame = true;
+ }
+
+ if (selectFrame) {
+ selectFrame->AddOption(i);
+ }
+
+ RefPtr<HTMLOptionElement> option = Item(i);
+ if (option && option->Selected()) {
+ // Clear all other options
+ if (!HasAttr(nsGkAtoms::multiple)) {
+ OptionFlags mask{OptionFlag::IsSelected, OptionFlag::ClearAll,
+ OptionFlag::SetDisabled, OptionFlag::Notify,
+ OptionFlag::InsertingOptions};
+ SetOptionsSelectedByIndex(i, i, mask);
+ }
+
+ // This is sort of a hack ... we need to notify that the option was
+ // set and change selectedIndex even though we didn't really change
+ // its value.
+ OnOptionSelected(selectFrame, i, true, false, aNotify);
+ }
+ }
+
+ CheckSelectSomething(aNotify);
+ }
+}
+
+nsresult HTMLSelectElement::RemoveOptionsFromList(nsIContent* aOptions,
+ int32_t aListIndex,
+ int32_t aDepth,
+ bool aNotify) {
+ MOZ_ASSERT(aDepth == 0 || aDepth == 1);
+ int32_t numRemoved = 0;
+
+ HTMLOptionElement* optElement = HTMLOptionElement::FromNode(aOptions);
+ if (optElement) {
+ if (mOptions->ItemAsOption(aListIndex) != optElement) {
+ NS_ERROR("wrong option at index");
+ return NS_ERROR_UNEXPECTED;
+ }
+ mOptions->RemoveOptionAt(aListIndex);
+ numRemoved++;
+ } else if (aDepth == 0) {
+ // Yay, one less artifact at the top level.
+ mNonOptionChildren--;
+
+ // Recurse down deeper for options
+ if (mOptGroupCount && aOptions->IsHTMLElement(nsGkAtoms::optgroup)) {
+ mOptGroupCount--;
+
+ for (nsIContent* child = aOptions->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ optElement = HTMLOptionElement::FromNode(child);
+ if (optElement) {
+ if (mOptions->ItemAsOption(aListIndex) != optElement) {
+ NS_ERROR("wrong option at index");
+ return NS_ERROR_UNEXPECTED;
+ }
+ mOptions->RemoveOptionAt(aListIndex);
+ numRemoved++;
+ }
+ }
+ }
+ } // else don't check for an optgroup; we want to ignore nested optgroups
+
+ if (numRemoved) {
+ // Tell the widget we removed the options
+ nsISelectControlFrame* selectFrame = GetSelectFrame();
+ if (selectFrame) {
+ nsAutoScriptBlocker scriptBlocker;
+ for (int32_t i = aListIndex; i < aListIndex + numRemoved; ++i) {
+ selectFrame->RemoveOption(i);
+ }
+ }
+
+ // Fix the selected index
+ if (aListIndex <= mSelectedIndex) {
+ if (mSelectedIndex < (aListIndex + numRemoved)) {
+ // aListIndex <= mSelectedIndex < aListIndex+numRemoved
+ // Find a new selected index if it was one of the ones removed.
+ // If this is a Combobox, no other Item will be selected.
+ if (IsCombobox()) {
+ mSelectedIndex = -1;
+ OnSelectionChanged();
+ } else {
+ FindSelectedIndex(aListIndex, aNotify);
+ }
+ } else {
+ // Shift the selected index if something in front of it was removed
+ // aListIndex+numRemoved <= mSelectedIndex
+ mSelectedIndex -= numRemoved;
+ OnSelectionChanged();
+ }
+ }
+
+ // Select something in case we removed the selected option on a
+ // single select
+ if (!CheckSelectSomething(aNotify) && mSelectedIndex == -1) {
+ // Update the validity state in case of we've just removed the last
+ // option.
+ UpdateValueMissingValidityState();
+ UpdateValidityElementStates(aNotify);
+ }
+ }
+
+ return NS_OK;
+}
+
+// XXXldb Doing the processing before the content nodes have been added
+// to the document (as the name of this function seems to require, and
+// as the callers do), is highly unusual. Passing around unparented
+// content to other parts of the app can make those things think the
+// options are the root content node.
+NS_IMETHODIMP
+HTMLSelectElement::WillAddOptions(nsIContent* aOptions, nsIContent* aParent,
+ int32_t aContentIndex, bool aNotify) {
+ if (this != aParent && this != aParent->GetParent()) {
+ return NS_OK;
+ }
+ int32_t level = aParent == this ? 0 : 1;
+
+ // Get the index where the options will be inserted
+ int32_t ind = -1;
+ if (!mNonOptionChildren) {
+ // If there are no artifacts, aContentIndex == ind
+ ind = aContentIndex;
+ } else {
+ // If there are artifacts, we have to get the index of the option the
+ // hard way
+ int32_t children = aParent->GetChildCount();
+
+ if (aContentIndex >= children) {
+ // If the content insert is after the end of the parent, then we want to
+ // get the next index *after* the parent and insert there.
+ ind = GetOptionIndexAfter(aParent);
+ } else {
+ // If the content insert is somewhere in the middle of the container, then
+ // we want to get the option currently at the index and insert in front of
+ // that.
+ nsIContent* currentKid = aParent->GetChildAt_Deprecated(aContentIndex);
+ NS_ASSERTION(currentKid, "Child not found!");
+ if (currentKid) {
+ ind = GetOptionIndexAt(currentKid);
+ } else {
+ ind = -1;
+ }
+ }
+ }
+
+ InsertOptionsIntoList(aOptions, ind, level, aNotify);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLSelectElement::WillRemoveOptions(nsIContent* aParent, int32_t aContentIndex,
+ bool aNotify) {
+ if (this != aParent && this != aParent->GetParent()) {
+ return NS_OK;
+ }
+ int32_t level = this == aParent ? 0 : 1;
+
+ // Get the index where the options will be removed
+ nsIContent* currentKid = aParent->GetChildAt_Deprecated(aContentIndex);
+ if (currentKid) {
+ int32_t ind;
+ if (!mNonOptionChildren) {
+ // If there are no artifacts, aContentIndex == ind
+ ind = aContentIndex;
+ } else {
+ // If there are artifacts, we have to get the index of the option the
+ // hard way
+ ind = GetFirstOptionIndex(currentKid);
+ }
+ if (ind != -1) {
+ nsresult rv = RemoveOptionsFromList(currentKid, ind, level, aNotify);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+int32_t HTMLSelectElement::GetOptionIndexAt(nsIContent* aOptions) {
+ // Search this node and below.
+ // If not found, find the first one *after* this node.
+ int32_t retval = GetFirstOptionIndex(aOptions);
+ if (retval == -1) {
+ retval = GetOptionIndexAfter(aOptions);
+ }
+
+ return retval;
+}
+
+int32_t HTMLSelectElement::GetOptionIndexAfter(nsIContent* aOptions) {
+ // - If this is the select, the next option is the last.
+ // - If not, search all the options after aOptions and up to the last option
+ // in the parent.
+ // - If it's not there, search for the first option after the parent.
+ if (aOptions == this) {
+ return Length();
+ }
+
+ int32_t retval = -1;
+
+ nsCOMPtr<nsIContent> parent = aOptions->GetParent();
+
+ if (parent) {
+ const int32_t index = parent->ComputeIndexOf_Deprecated(aOptions);
+ const int32_t count = static_cast<int32_t>(parent->GetChildCount());
+
+ retval = GetFirstChildOptionIndex(parent, index + 1, count);
+
+ if (retval == -1) {
+ retval = GetOptionIndexAfter(parent);
+ }
+ }
+
+ return retval;
+}
+
+int32_t HTMLSelectElement::GetFirstOptionIndex(nsIContent* aOptions) {
+ int32_t listIndex = -1;
+ HTMLOptionElement* optElement = HTMLOptionElement::FromNode(aOptions);
+ if (optElement) {
+ mOptions->GetOptionIndex(optElement, 0, true, &listIndex);
+ return listIndex;
+ }
+
+ listIndex = GetFirstChildOptionIndex(aOptions, 0, aOptions->GetChildCount());
+
+ return listIndex;
+}
+
+int32_t HTMLSelectElement::GetFirstChildOptionIndex(nsIContent* aOptions,
+ int32_t aStartIndex,
+ int32_t aEndIndex) {
+ int32_t retval = -1;
+
+ for (int32_t i = aStartIndex; i < aEndIndex; ++i) {
+ retval = GetFirstOptionIndex(aOptions->GetChildAt_Deprecated(i));
+ if (retval != -1) {
+ break;
+ }
+ }
+
+ return retval;
+}
+
+nsISelectControlFrame* HTMLSelectElement::GetSelectFrame() {
+ nsIFormControlFrame* form_control_frame = GetFormControlFrame(false);
+
+ nsISelectControlFrame* select_frame = nullptr;
+
+ if (form_control_frame) {
+ select_frame = do_QueryFrame(form_control_frame);
+ }
+
+ return select_frame;
+}
+
+void HTMLSelectElement::Add(
+ const HTMLOptionElementOrHTMLOptGroupElement& aElement,
+ const Nullable<HTMLElementOrLong>& aBefore, ErrorResult& aRv) {
+ nsGenericHTMLElement& element =
+ aElement.IsHTMLOptionElement() ? static_cast<nsGenericHTMLElement&>(
+ aElement.GetAsHTMLOptionElement())
+ : static_cast<nsGenericHTMLElement&>(
+ aElement.GetAsHTMLOptGroupElement());
+
+ if (aBefore.IsNull()) {
+ Add(element, static_cast<nsGenericHTMLElement*>(nullptr), aRv);
+ } else if (aBefore.Value().IsHTMLElement()) {
+ Add(element, &aBefore.Value().GetAsHTMLElement(), aRv);
+ } else {
+ Add(element, aBefore.Value().GetAsLong(), aRv);
+ }
+}
+
+void HTMLSelectElement::Add(nsGenericHTMLElement& aElement,
+ nsGenericHTMLElement* aBefore,
+ ErrorResult& aError) {
+ if (!aBefore) {
+ Element::AppendChild(aElement, aError);
+ return;
+ }
+
+ // Just in case we're not the parent, get the parent of the reference
+ // element
+ nsCOMPtr<nsINode> parent = aBefore->Element::GetParentNode();
+ if (!parent || !parent->IsInclusiveDescendantOf(this)) {
+ // NOT_FOUND_ERR: Raised if before is not a descendant of the SELECT
+ // element.
+ aError.Throw(NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ // If the before parameter is not null, we are equivalent to the
+ // insertBefore method on the parent of before.
+ nsCOMPtr<nsINode> refNode = aBefore;
+ parent->InsertBefore(aElement, refNode, aError);
+}
+
+void HTMLSelectElement::Remove(int32_t aIndex) const {
+ if (aIndex < 0) {
+ return;
+ }
+
+ nsCOMPtr<nsINode> option = Item(static_cast<uint32_t>(aIndex));
+ if (!option) {
+ return;
+ }
+
+ option->Remove();
+}
+
+void HTMLSelectElement::GetType(nsAString& aType) {
+ if (HasAttr(nsGkAtoms::multiple)) {
+ aType.AssignLiteral("select-multiple");
+ } else {
+ aType.AssignLiteral("select-one");
+ }
+}
+
+void HTMLSelectElement::SetLength(uint32_t aLength, ErrorResult& aRv) {
+ constexpr uint32_t kMaxDynamicSelectLength = 100000;
+
+ uint32_t curlen = Length();
+
+ if (curlen > aLength) { // Remove extra options
+ for (uint32_t i = curlen; i > aLength; --i) {
+ Remove(i - 1);
+ }
+ } else if (aLength > curlen) {
+ if (aLength > kMaxDynamicSelectLength) {
+ nsAutoString strOptionsLength;
+ strOptionsLength.AppendInt(aLength);
+
+ nsAutoString strLimit;
+ strLimit.AppendInt(kMaxDynamicSelectLength);
+
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::warningFlag, "DOM"_ns, OwnerDoc(),
+ nsContentUtils::eDOM_PROPERTIES,
+ "SelectOptionsLengthAssignmentWarning", {strOptionsLength, strLimit});
+ return;
+ }
+
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+
+ nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::option,
+ getter_AddRefs(nodeInfo));
+
+ nsCOMPtr<nsINode> node = NS_NewHTMLOptionElement(nodeInfo.forget());
+ for (uint32_t i = curlen; i < aLength; i++) {
+ nsINode::AppendChild(*node, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (i + 1 < aLength) {
+ node = node->CloneNode(true, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ MOZ_ASSERT(node);
+ }
+ }
+ }
+}
+
+/* static */
+bool HTMLSelectElement::MatchSelectedOptions(Element* aElement,
+ int32_t /* unused */,
+ nsAtom* /* unused */,
+ void* /* unused*/) {
+ HTMLOptionElement* option = HTMLOptionElement::FromNode(aElement);
+ return option && option->Selected();
+}
+
+nsIHTMLCollection* HTMLSelectElement::SelectedOptions() {
+ if (!mSelectedOptions) {
+ mSelectedOptions = new nsContentList(this, MatchSelectedOptions, nullptr,
+ nullptr, /* deep */ true);
+ }
+ return mSelectedOptions;
+}
+
+void HTMLSelectElement::SetSelectedIndexInternal(int32_t aIndex, bool aNotify) {
+ int32_t oldSelectedIndex = mSelectedIndex;
+ OptionFlags mask{OptionFlag::IsSelected, OptionFlag::ClearAll,
+ OptionFlag::SetDisabled};
+ if (aNotify) {
+ mask += OptionFlag::Notify;
+ }
+
+ SetOptionsSelectedByIndex(aIndex, aIndex, mask);
+
+ nsISelectControlFrame* selectFrame = GetSelectFrame();
+ if (selectFrame) {
+ selectFrame->OnSetSelectedIndex(oldSelectedIndex, mSelectedIndex);
+ }
+
+ OnSelectionChanged();
+}
+
+bool HTMLSelectElement::IsOptionSelectedByIndex(int32_t aIndex) const {
+ HTMLOptionElement* option = Item(static_cast<uint32_t>(aIndex));
+ return option && option->Selected();
+}
+
+void HTMLSelectElement::OnOptionSelected(nsISelectControlFrame* aSelectFrame,
+ int32_t aIndex, bool aSelected,
+ bool aChangeOptionState,
+ bool aNotify) {
+ // Set the selected index
+ if (aSelected && (aIndex < mSelectedIndex || mSelectedIndex < 0)) {
+ mSelectedIndex = aIndex;
+ OnSelectionChanged();
+ } else if (!aSelected && aIndex == mSelectedIndex) {
+ FindSelectedIndex(aIndex + 1, aNotify);
+ }
+
+ if (aChangeOptionState) {
+ // Tell the option to get its bad self selected
+ RefPtr<HTMLOptionElement> option = Item(static_cast<uint32_t>(aIndex));
+ if (option) {
+ option->SetSelectedInternal(aSelected, aNotify);
+ }
+ }
+
+ // Let the frame know too
+ if (aSelectFrame) {
+ aSelectFrame->OnOptionSelected(aIndex, aSelected);
+ }
+
+ UpdateSelectedOptions();
+ UpdateValueMissingValidityState();
+ UpdateValidityElementStates(aNotify);
+}
+
+void HTMLSelectElement::FindSelectedIndex(int32_t aStartIndex, bool aNotify) {
+ mSelectedIndex = -1;
+ uint32_t len = Length();
+ for (int32_t i = aStartIndex; i < int32_t(len); i++) {
+ if (IsOptionSelectedByIndex(i)) {
+ mSelectedIndex = i;
+ break;
+ }
+ }
+ OnSelectionChanged();
+}
+
+// XXX Consider splitting this into two functions for ease of reading:
+// SelectOptionsByIndex(startIndex, endIndex, clearAll, checkDisabled)
+// startIndex, endIndex - the range of options to turn on
+// (-1, -1) will clear all indices no matter what.
+// clearAll - will clear all other options unless checkDisabled is on
+// and all the options attempted to be set are disabled
+// (note that if it is not multiple, and an option is selected,
+// everything else will be cleared regardless).
+// checkDisabled - if this is TRUE, and an option is disabled, it will not be
+// changed regardless of whether it is selected or not.
+// Generally the UI passes TRUE and JS passes FALSE.
+// (setDisabled currently is the opposite)
+// DeselectOptionsByIndex(startIndex, endIndex, checkDisabled)
+// startIndex, endIndex - the range of options to turn on
+// (-1, -1) will clear all indices no matter what.
+// checkDisabled - if this is TRUE, and an option is disabled, it will not be
+// changed regardless of whether it is selected or not.
+// Generally the UI passes TRUE and JS passes FALSE.
+// (setDisabled currently is the opposite)
+//
+// XXXbz the above comment is pretty confusing. Maybe we should actually
+// document the args to this function too, in addition to documenting what
+// things might end up looking like? In particular, pay attention to the
+// setDisabled vs checkDisabled business.
+bool HTMLSelectElement::SetOptionsSelectedByIndex(int32_t aStartIndex,
+ int32_t aEndIndex,
+ OptionFlags aOptionsMask) {
+#if 0
+ printf("SetOption(%d-%d, %c, ClearAll=%c)\n", aStartIndex, aEndIndex,
+ (aOptionsMask.contains(OptionFlag::IsSelected) ? 'Y' : 'N'),
+ (aOptionsMask.contains(OptionFlag::ClearAll) ? 'Y' : 'N'));
+#endif
+ // Don't bother if the select is disabled
+ if (!aOptionsMask.contains(OptionFlag::SetDisabled) && IsDisabled()) {
+ return false;
+ }
+
+ // Don't bother if there are no options
+ uint32_t numItems = Length();
+ if (numItems == 0) {
+ return false;
+ }
+
+ // First, find out whether multiple items can be selected
+ bool isMultiple = Multiple();
+
+ // These variables tell us whether any options were selected
+ // or deselected.
+ bool optionsSelected = false;
+ bool optionsDeselected = false;
+
+ nsISelectControlFrame* selectFrame = nullptr;
+ bool didGetFrame = false;
+ AutoWeakFrame weakSelectFrame;
+
+ if (aOptionsMask.contains(OptionFlag::IsSelected)) {
+ // Setting selectedIndex to an out-of-bounds index means -1. (HTML5)
+ if (aStartIndex < 0 || AssertedCast<uint32_t>(aStartIndex) >= numItems ||
+ aEndIndex < 0 || AssertedCast<uint32_t>(aEndIndex) >= numItems) {
+ aStartIndex = -1;
+ aEndIndex = -1;
+ }
+
+ // Only select the first value if it's not multiple
+ if (!isMultiple) {
+ aEndIndex = aStartIndex;
+ }
+
+ // This variable tells whether or not all of the options we attempted to
+ // select are disabled. If ClearAll is passed in as true, and we do not
+ // select anything because the options are disabled, we will not clear the
+ // other options. (This is to make the UI work the way one might expect.)
+ bool allDisabled = !aOptionsMask.contains(OptionFlag::SetDisabled);
+
+ //
+ // Save a little time when clearing other options
+ //
+ int32_t previousSelectedIndex = mSelectedIndex;
+
+ //
+ // Select the requested indices
+ //
+ // If index is -1, everything will be deselected (bug 28143)
+ if (aStartIndex != -1) {
+ MOZ_ASSERT(aStartIndex >= 0);
+ MOZ_ASSERT(aEndIndex >= 0);
+ // Loop through the options and select them (if they are not disabled and
+ // if they are not already selected).
+ for (uint32_t optIndex = AssertedCast<uint32_t>(aStartIndex);
+ optIndex <= AssertedCast<uint32_t>(aEndIndex); optIndex++) {
+ RefPtr<HTMLOptionElement> option = Item(optIndex);
+
+ // Ignore disabled options.
+ if (!aOptionsMask.contains(OptionFlag::SetDisabled)) {
+ if (option && IsOptionDisabled(option)) {
+ continue;
+ }
+ allDisabled = false;
+ }
+
+ // If the index is already selected, ignore it. On the other hand when
+ // the option has just been inserted we have to get in sync with it.
+ if (option && (aOptionsMask.contains(OptionFlag::InsertingOptions) ||
+ !option->Selected())) {
+ // To notify the frame if anything gets changed. No need
+ // to flush here, if there's no frame yet we don't need to
+ // force it to be created just to notify it about a change
+ // in the select.
+ selectFrame = GetSelectFrame();
+ weakSelectFrame = do_QueryFrame(selectFrame);
+ didGetFrame = true;
+
+ OnOptionSelected(selectFrame, optIndex, true, !option->Selected(),
+ aOptionsMask.contains(OptionFlag::Notify));
+ optionsSelected = true;
+ }
+ }
+ }
+
+ // Next remove all other options if single select or all is clear
+ // If index is -1, everything will be deselected (bug 28143)
+ if (((!isMultiple && optionsSelected) ||
+ (aOptionsMask.contains(OptionFlag::ClearAll) && !allDisabled) ||
+ aStartIndex == -1) &&
+ previousSelectedIndex != -1) {
+ for (uint32_t optIndex = AssertedCast<uint32_t>(previousSelectedIndex);
+ optIndex < numItems; optIndex++) {
+ if (static_cast<int32_t>(optIndex) < aStartIndex ||
+ static_cast<int32_t>(optIndex) > aEndIndex) {
+ HTMLOptionElement* option = Item(optIndex);
+ // If the index is already deselected, ignore it.
+ if (option && option->Selected()) {
+ if (!didGetFrame || (selectFrame && !weakSelectFrame.IsAlive())) {
+ // To notify the frame if anything gets changed, don't
+ // flush, if the frame doesn't exist we don't need to
+ // create it just to tell it about this change.
+ selectFrame = GetSelectFrame();
+ weakSelectFrame = do_QueryFrame(selectFrame);
+
+ didGetFrame = true;
+ }
+
+ OnOptionSelected(selectFrame, optIndex, false, true,
+ aOptionsMask.contains(OptionFlag::Notify));
+ optionsDeselected = true;
+
+ // Only need to deselect one option if not multiple
+ if (!isMultiple) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ // If we're deselecting, loop through all selected items and deselect
+ // any that are in the specified range.
+ for (int32_t optIndex = aStartIndex; optIndex <= aEndIndex; optIndex++) {
+ HTMLOptionElement* option = Item(optIndex);
+ if (!aOptionsMask.contains(OptionFlag::SetDisabled) &&
+ IsOptionDisabled(option)) {
+ continue;
+ }
+
+ // If the index is already selected, ignore it.
+ if (option && option->Selected()) {
+ if (!didGetFrame || (selectFrame && !weakSelectFrame.IsAlive())) {
+ // To notify the frame if anything gets changed, don't
+ // flush, if the frame doesn't exist we don't need to
+ // create it just to tell it about this change.
+ selectFrame = GetSelectFrame();
+ weakSelectFrame = do_QueryFrame(selectFrame);
+
+ didGetFrame = true;
+ }
+
+ OnOptionSelected(selectFrame, optIndex, false, true,
+ aOptionsMask.contains(OptionFlag::Notify));
+ optionsDeselected = true;
+ }
+ }
+ }
+
+ // Make sure something is selected unless we were set to -1 (none)
+ if (optionsDeselected && aStartIndex != -1 &&
+ !aOptionsMask.contains(OptionFlag::NoReselect)) {
+ optionsSelected =
+ CheckSelectSomething(aOptionsMask.contains(OptionFlag::Notify)) ||
+ optionsSelected;
+ }
+
+ // Let the caller know whether anything was changed
+ return optionsSelected || optionsDeselected;
+}
+
+NS_IMETHODIMP
+HTMLSelectElement::IsOptionDisabled(int32_t aIndex, bool* aIsDisabled) {
+ *aIsDisabled = false;
+ RefPtr<HTMLOptionElement> option = Item(aIndex);
+ NS_ENSURE_TRUE(option, NS_ERROR_FAILURE);
+
+ *aIsDisabled = IsOptionDisabled(option);
+ return NS_OK;
+}
+
+bool HTMLSelectElement::IsOptionDisabled(HTMLOptionElement* aOption) const {
+ MOZ_ASSERT(aOption);
+ if (aOption->Disabled()) {
+ return true;
+ }
+
+ // Check for disabled optgroups
+ // If there are no artifacts, there are no optgroups
+ if (mNonOptionChildren) {
+ for (nsCOMPtr<Element> node =
+ static_cast<nsINode*>(aOption)->GetParentElement();
+ node; node = node->GetParentElement()) {
+ // If we reached the select element, we're done
+ if (node->IsHTMLElement(nsGkAtoms::select)) {
+ return false;
+ }
+
+ RefPtr<HTMLOptGroupElement> optGroupElement =
+ HTMLOptGroupElement::FromNode(node);
+
+ if (!optGroupElement) {
+ // If you put something else between you and the optgroup, you're a
+ // moron and you deserve not to have optgroup disabling work.
+ return false;
+ }
+
+ if (optGroupElement->Disabled()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+void HTMLSelectElement::GetValue(DOMString& aValue) const {
+ int32_t selectedIndex = SelectedIndex();
+ if (selectedIndex < 0) {
+ return;
+ }
+
+ RefPtr<HTMLOptionElement> option = Item(static_cast<uint32_t>(selectedIndex));
+
+ if (!option) {
+ return;
+ }
+
+ option->GetValue(aValue);
+}
+
+void HTMLSelectElement::SetValue(const nsAString& aValue) {
+ uint32_t length = Length();
+
+ for (uint32_t i = 0; i < length; i++) {
+ RefPtr<HTMLOptionElement> option = Item(i);
+ if (!option) {
+ continue;
+ }
+
+ nsAutoString optionVal;
+ option->GetValue(optionVal);
+ if (optionVal.Equals(aValue)) {
+ SetSelectedIndexInternal(int32_t(i), true);
+ return;
+ }
+ }
+ // No matching option was found.
+ SetSelectedIndexInternal(-1, true);
+}
+
+int32_t HTMLSelectElement::TabIndexDefault() { return 0; }
+
+bool HTMLSelectElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable(
+ aWithMouse, aIsFocusable, aTabIndex)) {
+ return true;
+ }
+
+ *aIsFocusable = !IsDisabled();
+
+ return false;
+}
+
+bool HTMLSelectElement::CheckSelectSomething(bool aNotify) {
+ if (mIsDoneAddingChildren) {
+ if (mSelectedIndex < 0 && IsCombobox()) {
+ return SelectSomething(aNotify);
+ }
+ }
+ return false;
+}
+
+bool HTMLSelectElement::SelectSomething(bool aNotify) {
+ // If we're not done building the select, don't play with this yet.
+ if (!mIsDoneAddingChildren) {
+ return false;
+ }
+
+ uint32_t count = Length();
+ for (uint32_t i = 0; i < count; i++) {
+ bool disabled;
+ nsresult rv = IsOptionDisabled(i, &disabled);
+
+ if (NS_FAILED(rv) || !disabled) {
+ SetSelectedIndexInternal(i, aNotify);
+
+ UpdateValueMissingValidityState();
+ UpdateValidityElementStates(aNotify);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+nsresult HTMLSelectElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv =
+ nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If there is a disabled fieldset in the parent chain, the element is now
+ // barred from constraint validation.
+ // XXXbz is this still needed now that fieldset changes always call
+ // FieldSetDisabledChanged?
+ UpdateBarredFromConstraintValidation();
+
+ // And now make sure our state is up to date
+ UpdateValidityElementStates(false);
+
+ return rv;
+}
+
+void HTMLSelectElement::UnbindFromTree(bool aNullParent) {
+ nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent);
+
+ // We might be no longer disabled because our parent chain changed.
+ // XXXbz is this still needed now that fieldset changes always call
+ // FieldSetDisabledChanged?
+ UpdateBarredFromConstraintValidation();
+
+ // And now make sure our state is up to date
+ UpdateValidityElementStates(false);
+}
+
+void HTMLSelectElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::disabled) {
+ if (aNotify) {
+ mDisabledChanged = true;
+ }
+ } else if (aName == nsGkAtoms::multiple) {
+ if (!aValue && aNotify && mSelectedIndex >= 0) {
+ // We're changing from being a multi-select to a single-select.
+ // Make sure we only have one option selected before we do that.
+ // Note that this needs to come before we really unset the attr,
+ // since SetOptionsSelectedByIndex does some bail-out type
+ // optimization for cases when the select is not multiple that
+ // would lead to only a single option getting deselected.
+ SetSelectedIndexInternal(mSelectedIndex, aNotify);
+ }
+ }
+ }
+
+ return nsGenericHTMLFormControlElementWithState::BeforeSetAttr(
+ aNameSpaceID, aName, aValue, aNotify);
+}
+
+void HTMLSelectElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::disabled) {
+ // This *has* to be called *before* validity state check because
+ // UpdateBarredFromConstraintValidation and
+ // UpdateValueMissingValidityState depend on our disabled state.
+ UpdateDisabledState(aNotify);
+
+ UpdateValueMissingValidityState();
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(aNotify);
+ } else if (aName == nsGkAtoms::required) {
+ // This *has* to be called *before* UpdateValueMissingValidityState
+ // because UpdateValueMissingValidityState depends on our required
+ // state.
+ UpdateRequiredState(!!aValue, aNotify);
+ UpdateValueMissingValidityState();
+ UpdateValidityElementStates(aNotify);
+ } else if (aName == nsGkAtoms::autocomplete) {
+ // Clear the cached @autocomplete attribute and autocompleteInfo state.
+ mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown;
+ mAutocompleteInfoState = nsContentUtils::eAutocompleteAttrState_Unknown;
+ } else if (aName == nsGkAtoms::multiple) {
+ if (!aValue && aNotify) {
+ // We might have become a combobox; make sure _something_ gets
+ // selected in that case
+ CheckSelectSomething(aNotify);
+ }
+ }
+ }
+
+ return nsGenericHTMLFormControlElementWithState::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLSelectElement::DoneAddingChildren(bool aHaveNotified) {
+ mIsDoneAddingChildren = true;
+
+ nsISelectControlFrame* selectFrame = GetSelectFrame();
+
+ // If we foolishly tried to restore before we were done adding
+ // content, restore the rest of the options proper-like
+ if (mRestoreState) {
+ RestoreStateTo(*mRestoreState);
+ mRestoreState = nullptr;
+ }
+
+ // Notify the frame
+ if (selectFrame) {
+ selectFrame->DoneAddingChildren(true);
+ }
+
+ if (!mInhibitStateRestoration) {
+ GenerateStateKey();
+ RestoreFormControlState();
+ }
+
+ // Now that we're done, select something (if it's a single select something
+ // must be selected)
+ if (!CheckSelectSomething(false)) {
+ // If an option has @selected set, it will be selected during parsing but
+ // with an empty value. We have to make sure the select element updates it's
+ // validity state to take this into account.
+ UpdateValueMissingValidityState();
+
+ // And now make sure we update our content state too
+ UpdateValidityElementStates(aHaveNotified);
+ }
+
+ mDefaultSelectionSet = true;
+}
+
+bool HTMLSelectElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (kNameSpaceID_None == aNamespaceID) {
+ if (aAttribute == nsGkAtoms::size) {
+ return aResult.ParsePositiveIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::autocomplete) {
+ aResult.ParseAtomArray(aValue);
+ return true;
+ }
+ }
+ return nsGenericHTMLFormControlElementWithState::ParseAttribute(
+ aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLSelectElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ nsGenericHTMLFormControlElementWithState::MapImageAlignAttributeInto(
+ aBuilder);
+ nsGenericHTMLFormControlElementWithState::MapCommonAttributesInto(aBuilder);
+}
+
+nsChangeHint HTMLSelectElement::GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const {
+ nsChangeHint retval =
+ nsGenericHTMLFormControlElementWithState::GetAttributeChangeHint(
+ aAttribute, aModType);
+ if (aAttribute == nsGkAtoms::multiple || aAttribute == nsGkAtoms::size) {
+ retval |= nsChangeHint_ReconstructFrame;
+ }
+ return retval;
+}
+
+NS_IMETHODIMP_(bool)
+HTMLSelectElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry* const map[] = {sCommonAttributeMap,
+ sImageAlignAttributeMap};
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLSelectElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+bool HTMLSelectElement::IsDisabledForEvents(WidgetEvent* aEvent) {
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(false);
+ nsIFrame* formFrame = nullptr;
+ if (formControlFrame) {
+ formFrame = do_QueryFrame(formControlFrame);
+ }
+ return IsElementDisabledForEvents(aEvent, formFrame);
+}
+
+void HTMLSelectElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ aVisitor.mCanHandle = false;
+ if (IsDisabledForEvents(aVisitor.mEvent)) {
+ return;
+ }
+
+ nsGenericHTMLFormControlElementWithState::GetEventTargetParent(aVisitor);
+}
+
+void HTMLSelectElement::UpdateValidityElementStates(bool aNotify) {
+ AutoStateChangeNotifier notifier(*this, aNotify);
+ RemoveStatesSilently(ElementState::VALIDITY_STATES);
+ if (!IsCandidateForConstraintValidation()) {
+ return;
+ }
+
+ ElementState state;
+ if (IsValid()) {
+ state |= ElementState::VALID;
+ if (mUserInteracted) {
+ state |= ElementState::USER_VALID;
+ }
+ } else {
+ state |= ElementState::INVALID;
+ if (mUserInteracted) {
+ state |= ElementState::USER_INVALID;
+ }
+ }
+
+ AddStatesSilently(state);
+}
+
+void HTMLSelectElement::SaveState() {
+ PresState* presState = GetPrimaryPresState();
+ if (!presState) {
+ return;
+ }
+
+ SelectContentData state;
+
+ uint32_t len = Length();
+
+ for (uint32_t optIndex = 0; optIndex < len; optIndex++) {
+ HTMLOptionElement* option = Item(optIndex);
+ if (option && option->Selected()) {
+ nsAutoString value;
+ option->GetValue(value);
+ if (value.IsEmpty()) {
+ state.indices().AppendElement(optIndex);
+ } else {
+ state.values().AppendElement(std::move(value));
+ }
+ }
+ }
+
+ presState->contentData() = std::move(state);
+
+ if (mDisabledChanged) {
+ // We do not want to save the real disabled state but the disabled
+ // attribute.
+ presState->disabled() = HasAttr(nsGkAtoms::disabled);
+ presState->disabledSet() = true;
+ }
+}
+
+bool HTMLSelectElement::RestoreState(PresState* aState) {
+ // Get the presentation state object to retrieve our stuff out of.
+ const PresContentData& state = aState->contentData();
+ if (state.type() == PresContentData::TSelectContentData) {
+ RestoreStateTo(state.get_SelectContentData());
+
+ // Don't flush, if the frame doesn't exist yet it doesn't care if
+ // we're reset or not.
+ DispatchContentReset();
+ }
+
+ if (aState->disabledSet() && !aState->disabled()) {
+ SetDisabled(false, IgnoreErrors());
+ }
+
+ return false;
+}
+
+void HTMLSelectElement::RestoreStateTo(const SelectContentData& aNewSelected) {
+ if (!mIsDoneAddingChildren) {
+ // Make a copy of the state for us to restore from in the future.
+ mRestoreState = MakeUnique<SelectContentData>(aNewSelected);
+ return;
+ }
+
+ uint32_t len = Length();
+ OptionFlags mask{OptionFlag::IsSelected, OptionFlag::ClearAll,
+ OptionFlag::SetDisabled, OptionFlag::Notify};
+
+ // First clear all
+ SetOptionsSelectedByIndex(-1, -1, mask);
+
+ // Select by index.
+ for (uint32_t idx : aNewSelected.indices()) {
+ if (idx < len) {
+ SetOptionsSelectedByIndex(idx, idx,
+ {OptionFlag::IsSelected,
+ OptionFlag::SetDisabled, OptionFlag::Notify});
+ }
+ }
+
+ // Select by value.
+ for (uint32_t i = 0; i < len; ++i) {
+ HTMLOptionElement* option = Item(i);
+ if (option) {
+ nsAutoString value;
+ option->GetValue(value);
+ if (aNewSelected.values().Contains(value)) {
+ SetOptionsSelectedByIndex(
+ i, i,
+ {OptionFlag::IsSelected, OptionFlag::SetDisabled,
+ OptionFlag::Notify});
+ }
+ }
+ }
+}
+
+// nsIFormControl
+
+NS_IMETHODIMP
+HTMLSelectElement::Reset() {
+ uint32_t numSelected = 0;
+
+ //
+ // Cycle through the options array and reset the options
+ //
+ uint32_t numOptions = Length();
+
+ for (uint32_t i = 0; i < numOptions; i++) {
+ RefPtr<HTMLOptionElement> option = Item(i);
+ if (option) {
+ //
+ // Reset the option to its default value
+ //
+
+ OptionFlags mask = {OptionFlag::SetDisabled, OptionFlag::Notify,
+ OptionFlag::NoReselect};
+ if (option->DefaultSelected()) {
+ mask += OptionFlag::IsSelected;
+ numSelected++;
+ }
+
+ SetOptionsSelectedByIndex(i, i, mask);
+ option->SetSelectedChanged(false);
+ }
+ }
+
+ //
+ // If nothing was selected and it's not multiple, select something
+ //
+ if (numSelected == 0 && IsCombobox()) {
+ SelectSomething(true);
+ }
+
+ OnSelectionChanged();
+ SetUserInteracted(false);
+
+ // Let the frame know we were reset
+ //
+ // Don't flush, if there's no frame yet it won't care about us being
+ // reset even if we forced it to be created now.
+ //
+ DispatchContentReset();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLSelectElement::SubmitNamesValues(FormData* aFormData) {
+ //
+ // Get the name (if no name, no submit)
+ //
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ if (name.IsEmpty()) {
+ return NS_OK;
+ }
+
+ //
+ // Submit
+ //
+ uint32_t len = Length();
+
+ for (uint32_t optIndex = 0; optIndex < len; optIndex++) {
+ HTMLOptionElement* option = Item(optIndex);
+
+ // Don't send disabled options
+ if (!option || IsOptionDisabled(option)) {
+ continue;
+ }
+
+ if (!option->Selected()) {
+ continue;
+ }
+
+ nsString value;
+ option->GetValue(value);
+
+ aFormData->AddNameValuePair(name, value);
+ }
+
+ return NS_OK;
+}
+
+void HTMLSelectElement::DispatchContentReset() {
+ if (nsIFormControlFrame* formControlFrame = GetFormControlFrame(false)) {
+ if (nsListControlFrame* listFrame = do_QueryFrame(formControlFrame)) {
+ listFrame->OnContentReset();
+ }
+ }
+}
+
+static void AddOptions(nsIContent* aRoot, HTMLOptionsCollection* aArray) {
+ for (nsIContent* child = aRoot->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ HTMLOptionElement* opt = HTMLOptionElement::FromNode(child);
+ if (opt) {
+ aArray->AppendOption(opt);
+ } else if (child->IsHTMLElement(nsGkAtoms::optgroup)) {
+ for (nsIContent* grandchild = child->GetFirstChild(); grandchild;
+ grandchild = grandchild->GetNextSibling()) {
+ opt = HTMLOptionElement::FromNode(grandchild);
+ if (opt) {
+ aArray->AppendOption(opt);
+ }
+ }
+ }
+ }
+}
+
+void HTMLSelectElement::RebuildOptionsArray(bool aNotify) {
+ mOptions->Clear();
+ AddOptions(this, mOptions);
+ FindSelectedIndex(0, aNotify);
+}
+
+bool HTMLSelectElement::IsValueMissing() const {
+ if (!Required()) {
+ return false;
+ }
+
+ uint32_t length = Length();
+
+ for (uint32_t i = 0; i < length; ++i) {
+ RefPtr<HTMLOptionElement> option = Item(i);
+ // Check for a placeholder label option, don't count it as a valid value.
+ if (i == 0 && !Multiple() && Size() <= 1 && option->GetParent() == this) {
+ nsAutoString value;
+ option->GetValue(value);
+ if (value.IsEmpty()) {
+ continue;
+ }
+ }
+
+ if (!option->Selected()) {
+ continue;
+ }
+
+ return false;
+ }
+
+ return true;
+}
+
+void HTMLSelectElement::UpdateValueMissingValidityState() {
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING, IsValueMissing());
+}
+
+nsresult HTMLSelectElement::GetValidationMessage(nsAString& aValidationMessage,
+ ValidityStateType aType) {
+ switch (aType) {
+ case VALIDITY_STATE_VALUE_MISSING: {
+ nsAutoString message;
+ nsresult rv = nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationSelectMissing",
+ OwnerDoc(), message);
+ aValidationMessage = message;
+ return rv;
+ }
+ default: {
+ return ConstraintValidation::GetValidationMessage(aValidationMessage,
+ aType);
+ }
+ }
+}
+
+#ifdef DEBUG
+
+void HTMLSelectElement::VerifyOptionsArray() {
+ int32_t index = 0;
+ for (nsIContent* child = nsINode::GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ HTMLOptionElement* opt = HTMLOptionElement::FromNode(child);
+ if (opt) {
+ NS_ASSERTION(opt == mOptions->ItemAsOption(index++),
+ "Options collection broken");
+ } else if (child->IsHTMLElement(nsGkAtoms::optgroup)) {
+ for (nsIContent* grandchild = child->GetFirstChild(); grandchild;
+ grandchild = grandchild->GetNextSibling()) {
+ opt = HTMLOptionElement::FromNode(grandchild);
+ if (opt) {
+ NS_ASSERTION(opt == mOptions->ItemAsOption(index++),
+ "Options collection broken");
+ }
+ }
+ }
+ }
+}
+
+#endif
+
+void HTMLSelectElement::UpdateBarredFromConstraintValidation() {
+ SetBarredFromConstraintValidation(
+ HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR) || IsDisabled());
+}
+
+void HTMLSelectElement::FieldSetDisabledChanged(bool aNotify) {
+ // This *has* to be called before UpdateBarredFromConstraintValidation and
+ // UpdateValueMissingValidityState because these two functions depend on our
+ // disabled state.
+ nsGenericHTMLFormControlElementWithState::FieldSetDisabledChanged(aNotify);
+
+ UpdateValueMissingValidityState();
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(aNotify);
+}
+
+void HTMLSelectElement::OnSelectionChanged() {
+ if (!mDefaultSelectionSet) {
+ return;
+ }
+ UpdateSelectedOptions();
+}
+
+void HTMLSelectElement::UpdateSelectedOptions() {
+ if (mSelectedOptions) {
+ mSelectedOptions->SetDirty();
+ }
+}
+
+void HTMLSelectElement::SetUserInteracted(bool aInteracted) {
+ if (mUserInteracted == aInteracted) {
+ return;
+ }
+ mUserInteracted = aInteracted;
+ UpdateValidityElementStates(true);
+}
+
+void HTMLSelectElement::SetPreviewValue(const nsAString& aValue) {
+ mPreviewValue = aValue;
+ nsContentUtils::RemoveNewlines(mPreviewValue);
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(false);
+ nsComboboxControlFrame* comboFrame = do_QueryFrame(formControlFrame);
+ if (comboFrame) {
+ comboFrame->RedisplaySelectedText();
+ }
+}
+
+void HTMLSelectElement::UserFinishedInteracting(bool aChanged) {
+ SetUserInteracted(true);
+ if (!aChanged) {
+ return;
+ }
+
+ // Dispatch the input event.
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+
+ // Dispatch the change event.
+ nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, u"change"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+}
+
+JSObject* HTMLSelectElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLSelectElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLSelectElement.h b/dom/html/HTMLSelectElement.h
new file mode 100644
index 0000000000..223da65c31
--- /dev/null
+++ b/dom/html/HTMLSelectElement.h
@@ -0,0 +1,520 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLSelectElement_h
+#define mozilla_dom_HTMLSelectElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/ConstraintValidation.h"
+#include "nsGenericHTMLElement.h"
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/HTMLOptionsCollection.h"
+#include "mozilla/EnumSet.h"
+#include "nsCheapSets.h"
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "nsContentUtils.h"
+
+class nsContentList;
+class nsIDOMHTMLOptionElement;
+class nsIHTMLCollection;
+class nsISelectControlFrame;
+
+namespace mozilla {
+
+class ErrorResult;
+class EventChainPostVisitor;
+class EventChainPreVisitor;
+class SelectContentData;
+class PresState;
+
+namespace dom {
+
+class FormData;
+class HTMLSelectElement;
+
+class MOZ_STACK_CLASS SafeOptionListMutation {
+ public:
+ /**
+ * @param aSelect The select element which option list is being mutated.
+ * Can be null.
+ * @param aParent The content object which is being mutated.
+ * @param aKid If not null, a new child element is being inserted to
+ * aParent. Otherwise a child element will be removed.
+ * @param aIndex The index of the content object in the parent.
+ */
+ SafeOptionListMutation(nsIContent* aSelect, nsIContent* aParent,
+ nsIContent* aKid, uint32_t aIndex, bool aNotify);
+ ~SafeOptionListMutation();
+ void MutationFailed() { mNeedsRebuild = true; }
+
+ private:
+ static void* operator new(size_t) noexcept(true) { return nullptr; }
+ static void operator delete(void*, size_t) {}
+ /** The select element which option list is being mutated. */
+ RefPtr<HTMLSelectElement> mSelect;
+ /** true if the current mutation is the first one in the stack. */
+ bool mTopLevelMutation;
+ /** true if it is known that the option list must be recreated. */
+ bool mNeedsRebuild;
+ /** Whether we should be notifying when we make various method calls on
+ mSelect */
+ const bool mNotify;
+ /** The selected option at mutation start. */
+ RefPtr<HTMLOptionElement> mInitialSelectedOption;
+ /** Option list must be recreated if more than one mutation is detected. */
+ nsMutationGuard mGuard;
+};
+
+/**
+ * Implementation of &lt;select&gt;
+ */
+class HTMLSelectElement final : public nsGenericHTMLFormControlElementWithState,
+ public ConstraintValidation {
+ public:
+ /**
+ * IsSelected whether to set the option(s) to true or false
+ *
+ * ClearAll whether to clear all other options (for example, if you
+ * are normal-clicking on the current option)
+ *
+ * SetDisabled whether it is permissible to set disabled options
+ * (for JavaScript)
+ *
+ * Notify whether to notify frames and such
+ *
+ * NoReselect no need to select something after an option is
+ * deselected (for reset)
+ *
+ * InsertingOptions if an option has just been inserted some bailouts can't
+ * be taken
+ */
+ enum class OptionFlag : uint8_t {
+ IsSelected,
+ ClearAll,
+ SetDisabled,
+ Notify,
+ NoReselect,
+ InsertingOptions
+ };
+ using OptionFlags = EnumSet<OptionFlag>;
+
+ using ConstraintValidation::GetValidationMessage;
+
+ explicit HTMLSelectElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser = NOT_FROM_PARSER);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLSelectElement, select)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ int32_t TabIndexDefault() override;
+
+ // Element
+ bool IsInteractiveHTMLContent() const override { return true; }
+
+ // WebIdl HTMLSelectElement
+ void GetAutocomplete(DOMString& aValue);
+ void SetAutocomplete(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::autocomplete, aValue, aRv);
+ }
+
+ void GetAutocompleteInfo(AutocompleteInfo& aInfo);
+
+ // Sets the user interacted flag and fires input/change events if needed.
+ MOZ_CAN_RUN_SCRIPT void UserFinishedInteracting(bool aChanged);
+
+ bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); }
+ void SetDisabled(bool aVal, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::disabled, aVal, aRv);
+ }
+ bool Multiple() const { return GetBoolAttr(nsGkAtoms::multiple); }
+ void SetMultiple(bool aVal, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::multiple, aVal, aRv);
+ }
+
+ void GetName(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); }
+ void SetName(const nsAString& aName, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aRv);
+ }
+ bool Required() const { return State().HasState(ElementState::REQUIRED); }
+ void SetRequired(bool aVal, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::required, aVal, aRv);
+ }
+ uint32_t Size() const { return GetUnsignedIntAttr(nsGkAtoms::size, 0); }
+ void SetSize(uint32_t aSize, ErrorResult& aRv) {
+ SetUnsignedIntAttr(nsGkAtoms::size, aSize, 0, aRv);
+ }
+
+ void GetType(nsAString& aValue);
+
+ HTMLOptionsCollection* Options() const { return mOptions; }
+ uint32_t Length() const { return mOptions->Length(); }
+ void SetLength(uint32_t aLength, ErrorResult& aRv);
+ Element* IndexedGetter(uint32_t aIdx, bool& aFound) const {
+ return mOptions->IndexedGetter(aIdx, aFound);
+ }
+ HTMLOptionElement* Item(uint32_t aIdx) const {
+ return mOptions->ItemAsOption(aIdx);
+ }
+ HTMLOptionElement* NamedItem(const nsAString& aName) const {
+ return mOptions->GetNamedItem(aName);
+ }
+ void Add(const HTMLOptionElementOrHTMLOptGroupElement& aElement,
+ const Nullable<HTMLElementOrLong>& aBefore, ErrorResult& aRv);
+ void Remove(int32_t aIndex) const;
+ void IndexedSetter(uint32_t aIndex, HTMLOptionElement* aOption,
+ ErrorResult& aRv) {
+ mOptions->IndexedSetter(aIndex, aOption, aRv);
+ }
+
+ static bool MatchSelectedOptions(Element* aElement, int32_t, nsAtom*, void*);
+
+ nsIHTMLCollection* SelectedOptions();
+
+ int32_t SelectedIndex() const { return mSelectedIndex; }
+ void SetSelectedIndex(int32_t aIdx) { SetSelectedIndexInternal(aIdx, true); }
+ void GetValue(DOMString& aValue) const;
+ void SetValue(const nsAString& aValue);
+
+ // Override SetCustomValidity so we update our state properly when it's called
+ // via bindings.
+ void SetCustomValidity(const nsAString& aError);
+
+ void ShowPicker(ErrorResult& aRv);
+
+ using nsINode::Remove;
+
+ // nsINode
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ // nsIContent
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+ void InsertChildBefore(nsIContent* aKid, nsIContent* aBeforeThis,
+ bool aNotify, ErrorResult& aRv) override;
+ void RemoveChildNode(nsIContent* aKid, bool aNotify) override;
+
+ // nsGenericHTMLElement
+ bool IsDisabledForEvents(WidgetEvent* aEvent) override;
+
+ // nsGenericHTMLFormElement
+ void SaveState() override;
+ bool RestoreState(PresState* aState) override;
+
+ // Overriden nsIFormControl methods
+ NS_IMETHOD Reset() override;
+ NS_IMETHOD SubmitNamesValues(FormData* aFormData) override;
+
+ void FieldSetDisabledChanged(bool aNotify) override;
+
+ /**
+ * To be called when stuff is added under a child of the select--but *before*
+ * they are actually added.
+ *
+ * @param aOptions the content that was added (usually just an option, but
+ * could be an optgroup node with many child options)
+ * @param aParent the parent the options were added to (could be an optgroup)
+ * @param aContentIndex the index where the options are being added within the
+ * parent (if the parent is an optgroup, the index within the optgroup)
+ */
+ NS_IMETHOD WillAddOptions(nsIContent* aOptions, nsIContent* aParent,
+ int32_t aContentIndex, bool aNotify);
+
+ /**
+ * To be called when stuff is removed under a child of the select--but
+ * *before* they are actually removed.
+ *
+ * @param aParent the parent the option(s) are being removed from
+ * @param aContentIndex the index of the option(s) within the parent (if the
+ * parent is an optgroup, the index within the optgroup)
+ */
+ NS_IMETHOD WillRemoveOptions(nsIContent* aParent, int32_t aContentIndex,
+ bool aNotify);
+
+ /**
+ * Checks whether an option is disabled (even if it's part of an optgroup)
+ *
+ * @param aIndex the index of the option to check
+ * @return whether the option is disabled
+ */
+ NS_IMETHOD IsOptionDisabled(int32_t aIndex, bool* aIsDisabled);
+ bool IsOptionDisabled(HTMLOptionElement* aOption) const;
+
+ /**
+ * Sets multiple options (or just sets startIndex if select is single)
+ * and handles notifications and cleanup and everything under the sun.
+ * When this method exits, the select will be in a consistent state. i.e.
+ * if you set the last option to false, it will select an option anyway.
+ *
+ * @param aStartIndex the first index to set
+ * @param aEndIndex the last index to set (set same as first index for one
+ * option)
+ * @param aOptionsMask determines whether to set, clear all or disable
+ * options and whether frames are to be notified of such.
+ * @return whether any options were actually changed
+ */
+ bool SetOptionsSelectedByIndex(int32_t aStartIndex, int32_t aEndIndex,
+ OptionFlags aOptionsMask);
+
+ /**
+ * Called when an attribute is about to be changed
+ */
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent) override;
+ void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ void DoneAddingChildren(bool aHaveNotified) override;
+ bool IsDoneAddingChildren() const { return mIsDoneAddingChildren; }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
+ HTMLSelectElement, nsGenericHTMLFormControlElementWithState)
+
+ HTMLOptionsCollection* GetOptions() { return mOptions; }
+
+ // ConstraintValidation
+ nsresult GetValidationMessage(nsAString& aValidationMessage,
+ ValidityStateType aType) override;
+
+ void UpdateValueMissingValidityState();
+ void UpdateValidityElementStates(bool aNotify);
+ /**
+ * Insert aElement before the node given by aBefore
+ */
+ void Add(nsGenericHTMLElement& aElement, nsGenericHTMLElement* aBefore,
+ ErrorResult& aError);
+ void Add(nsGenericHTMLElement& aElement, int32_t aIndex,
+ ErrorResult& aError) {
+ // If item index is out of range, insert to last.
+ // (since beforeElement becomes null, it is inserted to last)
+ nsIContent* beforeContent = mOptions->GetElementAt(aIndex);
+ return Add(aElement, nsGenericHTMLElement::FromNodeOrNull(beforeContent),
+ aError);
+ }
+
+ /**
+ * Is this a combobox?
+ */
+ bool IsCombobox() const { return !Multiple() && Size() <= 1; }
+
+ bool OpenInParentProcess() const { return mIsOpenInParentProcess; }
+ void SetOpenInParentProcess(bool aVal) { mIsOpenInParentProcess = aVal; }
+
+ void GetPreviewValue(nsAString& aValue) { aValue = mPreviewValue; }
+ void SetPreviewValue(const nsAString& aValue);
+
+ protected:
+ virtual ~HTMLSelectElement() = default;
+
+ friend class SafeOptionListMutation;
+
+ // Helper Methods
+ /**
+ * Check whether the option specified by the index is selected
+ * @param aIndex the index
+ * @return whether the option at the index is selected
+ */
+ bool IsOptionSelectedByIndex(int32_t aIndex) const;
+ /**
+ * Starting with (and including) aStartIndex, find the first selected index
+ * and set mSelectedIndex to it.
+ * @param aStartIndex the index to start with
+ */
+ void FindSelectedIndex(int32_t aStartIndex, bool aNotify);
+ /**
+ * Select some option if possible (generally the first non-disabled option).
+ * @return true if something was selected, false otherwise
+ */
+ bool SelectSomething(bool aNotify);
+ /**
+ * Call SelectSomething(), but only if nothing is selected
+ * @see SelectSomething()
+ * @return true if something was selected, false otherwise
+ */
+ bool CheckSelectSomething(bool aNotify);
+ /**
+ * Called to trigger notifications of frames and fixing selected index
+ *
+ * @param aSelectFrame the frame for this content (could be null)
+ * @param aIndex the index that was selected or deselected
+ * @param aSelected whether the index was selected or deselected
+ * @param aChangeOptionState if false, don't do anything to the
+ * HTMLOptionElement at aIndex. If true, change
+ * its selected state to aSelected.
+ * @param aNotify whether to notify the style system and such
+ */
+ void OnOptionSelected(nsISelectControlFrame* aSelectFrame, int32_t aIndex,
+ bool aSelected, bool aChangeOptionState, bool aNotify);
+ /**
+ * Restore state to a particular state string (representing the options)
+ * @param aNewSelected the state string to restore to
+ */
+ void RestoreStateTo(const SelectContentData& aNewSelected);
+
+ // Adding options
+ /**
+ * Insert option(s) into the options[] array and perform notifications
+ * @param aOptions the option or optgroup being added
+ * @param aListIndex the index to start adding options into the list at
+ * @param aDepth the depth of aOptions (1=direct child of select ...)
+ */
+ void InsertOptionsIntoList(nsIContent* aOptions, int32_t aListIndex,
+ int32_t aDepth, bool aNotify);
+ /**
+ * Remove option(s) from the options[] array
+ * @param aOptions the option or optgroup being added
+ * @param aListIndex the index to start removing options from the list at
+ * @param aDepth the depth of aOptions (1=direct child of select ...)
+ */
+ nsresult RemoveOptionsFromList(nsIContent* aOptions, int32_t aListIndex,
+ int32_t aDepth, bool aNotify);
+
+ // nsIConstraintValidation
+ void UpdateBarredFromConstraintValidation();
+ bool IsValueMissing() const;
+
+ /**
+ * Get the index of the first option at, under or following the content in
+ * the select, or length of options[] if none are found
+ * @param aOptions the content
+ * @return the index of the first option
+ */
+ int32_t GetOptionIndexAt(nsIContent* aOptions);
+ /**
+ * Get the next option following the content in question (not at or under)
+ * (this could include siblings of the current content or siblings of the
+ * parent or children of siblings of the parent).
+ * @param aOptions the content
+ * @return the index of the next option after the content
+ */
+ int32_t GetOptionIndexAfter(nsIContent* aOptions);
+ /**
+ * Get the first option index at or under the content in question.
+ * @param aOptions the content
+ * @return the index of the first option at or under the content
+ */
+ int32_t GetFirstOptionIndex(nsIContent* aOptions);
+ /**
+ * Get the first option index under the content in question, within the
+ * range specified.
+ * @param aOptions the content
+ * @param aStartIndex the first child to look at
+ * @param aEndIndex the child *after* the last child to look at
+ * @return the index of the first option at or under the content
+ */
+ int32_t GetFirstChildOptionIndex(nsIContent* aOptions, int32_t aStartIndex,
+ int32_t aEndIndex);
+
+ /**
+ * Get the frame as an nsISelectControlFrame (MAY RETURN nullptr)
+ * @return the select frame, or null
+ */
+ nsISelectControlFrame* GetSelectFrame();
+
+ /**
+ * Helper method for dispatching ContentReset notifications to list box
+ * frames.
+ */
+ void DispatchContentReset();
+
+ /**
+ * Rebuilds the options array from scratch as a fallback in error cases.
+ */
+ void RebuildOptionsArray(bool aNotify);
+
+#ifdef DEBUG
+ void VerifyOptionsArray();
+#endif
+
+ void SetSelectedIndexInternal(int32_t aIndex, bool aNotify);
+
+ void OnSelectionChanged();
+
+ /**
+ * Marks the selectedOptions list as dirty, so that it'll populate itself
+ * again.
+ */
+ void UpdateSelectedOptions();
+
+ void SetUserInteracted(bool) final;
+
+ /** The options[] array */
+ RefPtr<HTMLOptionsCollection> mOptions;
+ nsContentUtils::AutocompleteAttrState mAutocompleteAttrState;
+ nsContentUtils::AutocompleteAttrState mAutocompleteInfoState;
+ /** false if the parser is in the middle of adding children. */
+ bool mIsDoneAddingChildren : 1;
+ /** true if our disabled state has changed from the default **/
+ bool mDisabledChanged : 1;
+ /** true if child nodes are being added or removed.
+ * Used by SafeOptionListMutation.
+ */
+ bool mMutating : 1;
+ /**
+ * True if DoneAddingChildren will get called but shouldn't restore state.
+ */
+ bool mInhibitStateRestoration : 1;
+ /** https://html.spec.whatwg.org/#user-interacted */
+ bool mUserInteracted : 1;
+ /** True if the default selected option has been set. */
+ bool mDefaultSelectionSet : 1;
+ /** True if we're open in the parent process */
+ bool mIsOpenInParentProcess : 1;
+
+ /** The number of non-options as children of the select */
+ uint32_t mNonOptionChildren;
+ /** The number of optgroups anywhere under the select */
+ uint32_t mOptGroupCount;
+ /**
+ * The current selected index for selectedIndex (will be the first selected
+ * index if multiple are selected)
+ */
+ int32_t mSelectedIndex;
+ /**
+ * The temporary restore state in case we try to restore before parser is
+ * done adding options
+ */
+ UniquePtr<SelectContentData> mRestoreState;
+
+ /**
+ * The live list of selected options.
+ */
+ RefPtr<nsContentList> mSelectedOptions;
+
+ /**
+ * The current displayed preview text.
+ */
+ nsString mPreviewValue;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_HTMLSelectElement_h
diff --git a/dom/html/HTMLSharedElement.cpp b/dom/html/HTMLSharedElement.cpp
new file mode 100644
index 0000000000..b18f5e3339
--- /dev/null
+++ b/dom/html/HTMLSharedElement.cpp
@@ -0,0 +1,223 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLSharedElement.h"
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/HTMLBaseElementBinding.h"
+#include "mozilla/dom/HTMLDirectoryElementBinding.h"
+#include "mozilla/dom/HTMLHeadElementBinding.h"
+#include "mozilla/dom/HTMLHtmlElementBinding.h"
+#include "mozilla/dom/HTMLParamElementBinding.h"
+#include "mozilla/dom/HTMLQuoteElementBinding.h"
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "nsContentUtils.h"
+#include "nsIContentSecurityPolicy.h"
+#include "nsIURI.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Shared)
+
+namespace mozilla::dom {
+
+HTMLSharedElement::~HTMLSharedElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLSharedElement)
+
+void HTMLSharedElement::GetHref(nsAString& aValue) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::base),
+ "This should only get called for <base> elements");
+ nsAutoString href;
+ GetAttr(nsGkAtoms::href, href);
+
+ nsCOMPtr<nsIURI> uri;
+ Document* doc = OwnerDoc();
+ nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), href, doc,
+ doc->GetFallbackBaseURI());
+
+ if (!uri) {
+ aValue = href;
+ return;
+ }
+
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+ CopyUTF8toUTF16(spec, aValue);
+}
+
+void HTMLSharedElement::DoneAddingChildren(bool aHaveNotified) {
+ if (mNodeInfo->Equals(nsGkAtoms::head)) {
+ if (nsCOMPtr<Document> doc = GetUncomposedDoc()) {
+ doc->OnL10nResourceContainerParsed();
+ if (!doc->IsLoadedAsData()) {
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, u"DOMHeadElementParsed"_ns,
+ CanBubble::eYes, ChromeOnlyDispatch::eYes);
+ // Always run async in order to avoid running script when the content
+ // sink isn't expecting it.
+ asyncDispatcher->PostDOMEvent();
+ }
+ }
+ }
+}
+
+static void SetBaseURIUsingFirstBaseWithHref(Document* aDocument,
+ nsIContent* aMustMatch) {
+ MOZ_ASSERT(aDocument, "Need a document!");
+
+ for (nsIContent* child = aDocument->GetFirstChild(); child;
+ child = child->GetNextNode()) {
+ if (child->IsHTMLElement(nsGkAtoms::base) &&
+ child->AsElement()->HasAttr(nsGkAtoms::href)) {
+ if (aMustMatch && child != aMustMatch) {
+ return;
+ }
+
+ // Resolve the <base> element's href relative to our document's
+ // fallback base URI.
+ nsAutoString href;
+ child->AsElement()->GetAttr(nsGkAtoms::href, href);
+
+ nsCOMPtr<nsIURI> newBaseURI;
+ nsContentUtils::NewURIWithDocumentCharset(
+ getter_AddRefs(newBaseURI), href, aDocument,
+ aDocument->GetFallbackBaseURI());
+
+ // Check if CSP allows this base-uri
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp();
+ if (csp && newBaseURI) {
+ // base-uri is only enforced if explicitly defined in the
+ // policy - do *not* consult default-src, see:
+ // http://www.w3.org/TR/CSP2/#directive-default-src
+ bool cspPermitsBaseURI = true;
+ rv = csp->Permits(
+ child->AsElement(), nullptr /* nsICSPEventListener */, newBaseURI,
+ nsIContentSecurityPolicy::BASE_URI_DIRECTIVE, true /* aSpecific */,
+ true /* aSendViolationReports */, &cspPermitsBaseURI);
+ if (NS_FAILED(rv) || !cspPermitsBaseURI) {
+ newBaseURI = nullptr;
+ }
+ }
+ aDocument->SetBaseURI(newBaseURI);
+ aDocument->SetChromeXHRDocBaseURI(nullptr);
+ return;
+ }
+ }
+
+ aDocument->SetBaseURI(nullptr);
+}
+
+static void SetBaseTargetUsingFirstBaseWithTarget(Document* aDocument,
+ nsIContent* aMustMatch) {
+ MOZ_ASSERT(aDocument, "Need a document!");
+
+ for (nsIContent* child = aDocument->GetFirstChild(); child;
+ child = child->GetNextNode()) {
+ if (child->IsHTMLElement(nsGkAtoms::base) &&
+ child->AsElement()->HasAttr(nsGkAtoms::target)) {
+ if (aMustMatch && child != aMustMatch) {
+ return;
+ }
+
+ nsString target;
+ child->AsElement()->GetAttr(nsGkAtoms::target, target);
+ aDocument->SetBaseTarget(target);
+ return;
+ }
+ }
+
+ aDocument->SetBaseTarget(u""_ns);
+}
+
+void HTMLSharedElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::href) {
+ // If the href attribute of a <base> tag is changing, we may need to
+ // update the document's base URI, which will cause all the links on the
+ // page to be re-resolved given the new base.
+ // If the href is being unset (aValue is null), we will need to find a new
+ // <base>.
+ if (mNodeInfo->Equals(nsGkAtoms::base) && IsInUncomposedDoc()) {
+ SetBaseURIUsingFirstBaseWithHref(GetUncomposedDoc(),
+ aValue ? this : nullptr);
+ }
+ } else if (aName == nsGkAtoms::target) {
+ // The target attribute is in pretty much the same situation as the href
+ // attribute, above.
+ if (mNodeInfo->Equals(nsGkAtoms::base) && IsInUncomposedDoc()) {
+ SetBaseTargetUsingFirstBaseWithTarget(GetUncomposedDoc(),
+ aValue ? this : nullptr);
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+nsresult HTMLSharedElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The document stores a pointer to its base URI and base target, which we may
+ // need to update here.
+ if (mNodeInfo->Equals(nsGkAtoms::base) && IsInUncomposedDoc()) {
+ if (HasAttr(nsGkAtoms::href)) {
+ SetBaseURIUsingFirstBaseWithHref(&aContext.OwnerDoc(), this);
+ }
+ if (HasAttr(nsGkAtoms::target)) {
+ SetBaseTargetUsingFirstBaseWithTarget(&aContext.OwnerDoc(), this);
+ }
+ }
+
+ return NS_OK;
+}
+
+void HTMLSharedElement::UnbindFromTree(bool aNullParent) {
+ Document* doc = GetUncomposedDoc();
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+
+ // If we're removing a <base> from a document, we may need to update the
+ // document's base URI and base target
+ if (doc && mNodeInfo->Equals(nsGkAtoms::base)) {
+ if (HasAttr(nsGkAtoms::href)) {
+ SetBaseURIUsingFirstBaseWithHref(doc, nullptr);
+ }
+ if (HasAttr(nsGkAtoms::target)) {
+ SetBaseTargetUsingFirstBaseWithTarget(doc, nullptr);
+ }
+ }
+}
+
+JSObject* HTMLSharedElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ if (mNodeInfo->Equals(nsGkAtoms::param)) {
+ return HTMLParamElement_Binding::Wrap(aCx, this, aGivenProto);
+ }
+ if (mNodeInfo->Equals(nsGkAtoms::base)) {
+ return HTMLBaseElement_Binding::Wrap(aCx, this, aGivenProto);
+ }
+ if (mNodeInfo->Equals(nsGkAtoms::dir)) {
+ return HTMLDirectoryElement_Binding::Wrap(aCx, this, aGivenProto);
+ }
+ if (mNodeInfo->Equals(nsGkAtoms::q) ||
+ mNodeInfo->Equals(nsGkAtoms::blockquote)) {
+ return HTMLQuoteElement_Binding::Wrap(aCx, this, aGivenProto);
+ }
+ if (mNodeInfo->Equals(nsGkAtoms::head)) {
+ return HTMLHeadElement_Binding::Wrap(aCx, this, aGivenProto);
+ }
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::html));
+ return HTMLHtmlElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLSharedElement.h b/dom/html/HTMLSharedElement.h
new file mode 100644
index 0000000000..cb1e5ff288
--- /dev/null
+++ b/dom/html/HTMLSharedElement.h
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLSharedElement_h
+#define mozilla_dom_HTMLSharedElement_h
+
+#include "nsGenericHTMLElement.h"
+
+#include "nsGkAtoms.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Assertions.h"
+
+namespace mozilla::dom {
+
+class HTMLSharedElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLSharedElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ if (mNodeInfo->Equals(nsGkAtoms::head) ||
+ mNodeInfo->Equals(nsGkAtoms::html)) {
+ SetHasWeirdParserInsertionMode();
+ }
+ }
+
+ // nsIContent
+ void DoneAddingChildren(bool aHaveNotified) override;
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // WebIDL API
+ // HTMLParamElement
+ void GetName(DOMString& aValue) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param));
+ GetHTMLAttr(nsGkAtoms::name, aValue);
+ }
+ void SetName(const nsAString& aValue, ErrorResult& aResult) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param));
+ SetHTMLAttr(nsGkAtoms::name, aValue, aResult);
+ }
+ void GetValue(DOMString& aValue) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param));
+ GetHTMLAttr(nsGkAtoms::value, aValue);
+ }
+ void SetValue(const nsAString& aValue, ErrorResult& aResult) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param));
+ SetHTMLAttr(nsGkAtoms::value, aValue, aResult);
+ }
+ void GetType(DOMString& aValue) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param));
+ GetHTMLAttr(nsGkAtoms::type, aValue);
+ }
+ void SetType(const nsAString& aValue, ErrorResult& aResult) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param));
+ SetHTMLAttr(nsGkAtoms::type, aValue, aResult);
+ }
+ void GetValueType(DOMString& aValue) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param));
+ GetHTMLAttr(nsGkAtoms::valuetype, aValue);
+ }
+ void SetValueType(const nsAString& aValue, ErrorResult& aResult) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param));
+ SetHTMLAttr(nsGkAtoms::valuetype, aValue, aResult);
+ }
+
+ // HTMLBaseElement
+ void GetTarget(DOMString& aValue) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::base));
+ GetHTMLAttr(nsGkAtoms::target, aValue);
+ }
+ void SetTarget(const nsAString& aValue, ErrorResult& aResult) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::base));
+ SetHTMLAttr(nsGkAtoms::target, aValue, aResult);
+ }
+
+ void GetHref(nsAString& aValue);
+ void SetHref(const nsAString& aValue, ErrorResult& aResult) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::base));
+ SetHTMLAttr(nsGkAtoms::href, aValue, aResult);
+ }
+
+ // HTMLDirectoryElement
+ bool Compact() const {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::dir));
+ return GetBoolAttr(nsGkAtoms::compact);
+ }
+ void SetCompact(bool aCompact, ErrorResult& aResult) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::dir));
+ SetHTMLBoolAttr(nsGkAtoms::compact, aCompact, aResult);
+ }
+
+ // HTMLQuoteElement
+ void GetCite(nsString& aCite) { GetHTMLURIAttr(nsGkAtoms::cite, aCite); }
+
+ void SetCite(const nsAString& aValue, ErrorResult& aResult) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::q) ||
+ mNodeInfo->Equals(nsGkAtoms::blockquote));
+ SetHTMLAttr(nsGkAtoms::cite, aValue, aResult);
+ }
+
+ // HTMLHtmlElement
+ void GetVersion(DOMString& aValue) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::html));
+ GetHTMLAttr(nsGkAtoms::version, aValue);
+ }
+ void SetVersion(const nsAString& aValue, ErrorResult& aResult) {
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::html));
+ SetHTMLAttr(nsGkAtoms::version, aValue, aResult);
+ }
+
+ protected:
+ virtual ~HTMLSharedElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLSharedElement_h
diff --git a/dom/html/HTMLSharedListElement.cpp b/dom/html/HTMLSharedListElement.cpp
new file mode 100644
index 0000000000..b5a86a8154
--- /dev/null
+++ b/dom/html/HTMLSharedListElement.cpp
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLSharedListElement.h"
+#include "mozilla/dom/HTMLDListElementBinding.h"
+#include "mozilla/dom/HTMLOListElementBinding.h"
+#include "mozilla/dom/HTMLUListElementBinding.h"
+#include "mozilla/dom/HTMLLIElement.h"
+
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsGenericHTMLElement.h"
+#include "nsAttrValueInlines.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(SharedList)
+
+namespace mozilla::dom {
+
+HTMLSharedListElement::~HTMLSharedListElement() = default;
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLSharedListElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLSharedListElement)
+
+bool HTMLSharedListElement::ParseAttribute(
+ int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (mNodeInfo->Equals(nsGkAtoms::ul)) {
+ if (aAttribute == nsGkAtoms::type) {
+ return aResult.ParseEnumValue(aValue, HTMLLIElement::kULTypeTable,
+ false);
+ }
+ }
+ if (mNodeInfo->Equals(nsGkAtoms::ol)) {
+ if (aAttribute == nsGkAtoms::type) {
+ return aResult.ParseEnumValue(aValue, HTMLLIElement::kOLTypeTable,
+ true);
+ }
+ if (aAttribute == nsGkAtoms::start) {
+ return aResult.ParseIntValue(aValue);
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLSharedListElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty_list_style_type)) {
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::type);
+ if (value && value->Type() == nsAttrValue::eEnum) {
+ aBuilder.SetKeywordValue(eCSSProperty_list_style_type,
+ value->GetEnumValue());
+ }
+ }
+
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+void HTMLSharedListElement::MapOLAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty_counter_reset)) {
+ const nsAttrValue* startAttr = aBuilder.GetAttr(nsGkAtoms::start);
+ bool haveStart = startAttr && startAttr->Type() == nsAttrValue::eInteger;
+ int32_t start = 0;
+ if (haveStart) {
+ start = startAttr->GetIntegerValue() - 1;
+ }
+ bool haveReversed = !!aBuilder.GetAttr(nsGkAtoms::reversed);
+ if (haveReversed) {
+ if (haveStart) {
+ start += 2; // i.e. the attr value + 1
+ } else {
+ start = std::numeric_limits<int32_t>::min();
+ }
+ }
+ if (haveStart || haveReversed) {
+ aBuilder.SetCounterResetListItem(start, haveReversed);
+ }
+ }
+
+ HTMLSharedListElement::MapAttributesIntoRule(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLSharedListElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ if (mNodeInfo->Equals(nsGkAtoms::ul)) {
+ static const MappedAttributeEntry attributes[] = {{nsGkAtoms::type},
+ {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+ }
+
+ if (mNodeInfo->Equals(nsGkAtoms::ol)) {
+ static const MappedAttributeEntry attributes[] = {{nsGkAtoms::type},
+ {nsGkAtoms::start},
+ {nsGkAtoms::reversed},
+ {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+ }
+
+ return nsGenericHTMLElement::IsAttributeMapped(aAttribute);
+}
+
+nsMapRuleToAttributesFunc HTMLSharedListElement::GetAttributeMappingFunction()
+ const {
+ if (mNodeInfo->Equals(nsGkAtoms::ul)) {
+ return &MapAttributesIntoRule;
+ }
+ if (mNodeInfo->Equals(nsGkAtoms::ol)) {
+ return &MapOLAttributesIntoRule;
+ }
+
+ return nsGenericHTMLElement::GetAttributeMappingFunction();
+}
+
+JSObject* HTMLSharedListElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ if (mNodeInfo->Equals(nsGkAtoms::ol)) {
+ return HTMLOListElement_Binding::Wrap(aCx, this, aGivenProto);
+ }
+ if (mNodeInfo->Equals(nsGkAtoms::dl)) {
+ return HTMLDListElement_Binding::Wrap(aCx, this, aGivenProto);
+ }
+ MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::ul));
+ return HTMLUListElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLSharedListElement.h b/dom/html/HTMLSharedListElement.h
new file mode 100644
index 0000000000..0b9b2e72dc
--- /dev/null
+++ b/dom/html/HTMLSharedListElement.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLSharedListElement_h
+#define mozilla_dom_HTMLSharedListElement_h
+
+#include "mozilla/Attributes.h"
+
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLSharedListElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLSharedListElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ bool Reversed() const { return GetBoolAttr(nsGkAtoms::reversed); }
+ void SetReversed(bool aReversed, mozilla::ErrorResult& rv) {
+ SetHTMLBoolAttr(nsGkAtoms::reversed, aReversed, rv);
+ }
+ int32_t Start() const { return GetIntAttr(nsGkAtoms::start, 1); }
+ void SetStart(int32_t aStart, mozilla::ErrorResult& rv) {
+ SetHTMLIntAttr(nsGkAtoms::start, aStart, rv);
+ }
+ void GetType(DOMString& aType) { GetHTMLAttr(nsGkAtoms::type, aType); }
+ void SetType(const nsAString& aType, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::type, aType, rv);
+ }
+ bool Compact() const { return GetBoolAttr(nsGkAtoms::compact); }
+ void SetCompact(bool aCompact, mozilla::ErrorResult& rv) {
+ SetHTMLBoolAttr(nsGkAtoms::compact, aCompact, rv);
+ }
+
+ protected:
+ virtual ~HTMLSharedListElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+ static void MapOLAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLSharedListElement_h
diff --git a/dom/html/HTMLSlotElement.cpp b/dom/html/HTMLSlotElement.cpp
new file mode 100644
index 0000000000..9fb3986e93
--- /dev/null
+++ b/dom/html/HTMLSlotElement.cpp
@@ -0,0 +1,371 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLSlotElement.h"
+#include "mozilla/dom/HTMLUnknownElement.h"
+#include "mozilla/dom/ShadowRoot.h"
+#include "mozilla/dom/Text.h"
+#include "mozilla/AppShutdown.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+
+nsGenericHTMLElement* NS_NewHTMLSlotElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo(std::move(aNodeInfo));
+ auto* nim = nodeInfo->NodeInfoManager();
+ return new (nim) mozilla::dom::HTMLSlotElement(nodeInfo.forget());
+}
+
+namespace mozilla::dom {
+
+HTMLSlotElement::HTMLSlotElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLSlotElement::~HTMLSlotElement() {
+ for (const auto& node : mManuallyAssignedNodes) {
+ MOZ_ASSERT(node->AsContent()->GetManualSlotAssignment() == this);
+ node->AsContent()->SetManualSlotAssignment(nullptr);
+ }
+}
+
+NS_IMPL_ADDREF_INHERITED(HTMLSlotElement, nsGenericHTMLElement)
+NS_IMPL_RELEASE_INHERITED(HTMLSlotElement, nsGenericHTMLElement)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLSlotElement, nsGenericHTMLElement,
+ mAssignedNodes)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLSlotElement)
+NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLSlotElement)
+
+nsresult HTMLSlotElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ RefPtr<ShadowRoot> oldContainingShadow = GetContainingShadow();
+
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ShadowRoot* containingShadow = GetContainingShadow();
+ mInManualShadowRoot =
+ containingShadow &&
+ containingShadow->SlotAssignment() == SlotAssignmentMode::Manual;
+ if (containingShadow && !oldContainingShadow) {
+ containingShadow->AddSlot(this);
+ }
+
+ return NS_OK;
+}
+
+void HTMLSlotElement::UnbindFromTree(bool aNullParent) {
+ RefPtr<ShadowRoot> oldContainingShadow = GetContainingShadow();
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+
+ if (oldContainingShadow && !GetContainingShadow()) {
+ oldContainingShadow->RemoveSlot(this);
+ }
+}
+
+void HTMLSlotElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::name) {
+ if (ShadowRoot* containingShadow = GetContainingShadow()) {
+ containingShadow->RemoveSlot(this);
+ }
+ }
+
+ return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue,
+ aNotify);
+}
+
+void HTMLSlotElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::name) {
+ if (ShadowRoot* containingShadow = GetContainingShadow()) {
+ containingShadow->AddSlot(this);
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+/**
+ * Flatten assigned nodes given a slot, as in:
+ * https://dom.spec.whatwg.org/#find-flattened-slotables
+ */
+static void FlattenAssignedNodes(HTMLSlotElement* aSlot,
+ nsTArray<RefPtr<nsINode>>& aNodes) {
+ if (!aSlot->GetContainingShadow()) {
+ return;
+ }
+
+ const nsTArray<RefPtr<nsINode>>& assignedNodes = aSlot->AssignedNodes();
+
+ // If assignedNodes is empty, use children of slot as fallback content.
+ if (assignedNodes.IsEmpty()) {
+ for (nsIContent* child = aSlot->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (!child->IsSlotable()) {
+ continue;
+ }
+
+ if (auto* slot = HTMLSlotElement::FromNode(child)) {
+ FlattenAssignedNodes(slot, aNodes);
+ } else {
+ aNodes.AppendElement(child);
+ }
+ }
+ return;
+ }
+
+ for (const RefPtr<nsINode>& assignedNode : assignedNodes) {
+ auto* slot = HTMLSlotElement::FromNode(assignedNode);
+ if (slot && slot->GetContainingShadow()) {
+ FlattenAssignedNodes(slot, aNodes);
+ } else {
+ aNodes.AppendElement(assignedNode);
+ }
+ }
+}
+
+void HTMLSlotElement::AssignedNodes(const AssignedNodesOptions& aOptions,
+ nsTArray<RefPtr<nsINode>>& aNodes) {
+ if (aOptions.mFlatten) {
+ return FlattenAssignedNodes(this, aNodes);
+ }
+
+ aNodes = mAssignedNodes.Clone();
+}
+
+void HTMLSlotElement::AssignedElements(const AssignedNodesOptions& aOptions,
+ nsTArray<RefPtr<Element>>& aElements) {
+ AutoTArray<RefPtr<nsINode>, 128> assignedNodes;
+ AssignedNodes(aOptions, assignedNodes);
+ for (const RefPtr<nsINode>& assignedNode : assignedNodes) {
+ if (assignedNode->IsElement()) {
+ aElements.AppendElement(assignedNode->AsElement());
+ }
+ }
+}
+
+const nsTArray<RefPtr<nsINode>>& HTMLSlotElement::AssignedNodes() const {
+ return mAssignedNodes;
+}
+
+const nsTArray<nsINode*>& HTMLSlotElement::ManuallyAssignedNodes() const {
+ return mManuallyAssignedNodes;
+}
+
+void HTMLSlotElement::Assign(const Sequence<OwningElementOrText>& aNodes) {
+ nsAutoScriptBlocker scriptBlocker;
+
+ // no-op if the input nodes and the assigned nodes are identical
+ // This also works if the two 'assign' calls are like
+ // > slot.assign(node1, node2);
+ // > slot.assign(node1, node2, node1, node2);
+ if (!mAssignedNodes.IsEmpty() && aNodes.Length() >= mAssignedNodes.Length()) {
+ nsTHashMap<nsPtrHashKey<nsIContent>, size_t> nodeIndexMap;
+ for (size_t i = 0; i < aNodes.Length(); ++i) {
+ nsIContent* content;
+ if (aNodes[i].IsElement()) {
+ content = aNodes[i].GetAsElement();
+ } else {
+ content = aNodes[i].GetAsText();
+ }
+ MOZ_ASSERT(content);
+ // We only care about the first index this content appears
+ // in the array
+ nodeIndexMap.LookupOrInsert(content, i);
+ }
+
+ if (nodeIndexMap.Count() == mAssignedNodes.Length()) {
+ bool isIdentical = true;
+ for (size_t i = 0; i < mAssignedNodes.Length(); ++i) {
+ size_t indexInInputNodes;
+ if (!nodeIndexMap.Get(mAssignedNodes[i]->AsContent(),
+ &indexInInputNodes) ||
+ indexInInputNodes != i) {
+ isIdentical = false;
+ break;
+ }
+ }
+ if (isIdentical) {
+ return;
+ }
+ }
+ }
+
+ // 1. For each node of this's manually assigned nodes, set node's manual slot
+ // assignment to null.
+ for (nsINode* node : mManuallyAssignedNodes) {
+ MOZ_ASSERT(node->AsContent()->GetManualSlotAssignment() == this);
+ node->AsContent()->SetManualSlotAssignment(nullptr);
+ }
+
+ // 2. Let nodesSet be a new ordered set.
+ mManuallyAssignedNodes.Clear();
+
+ nsIContent* host = nullptr;
+ ShadowRoot* root = GetContainingShadow();
+
+ // An optimization to keep track which slots need to enqueue
+ // slotchange event, such that they can be enqueued later in
+ // tree order.
+ nsTHashSet<RefPtr<HTMLSlotElement>> changedSlots;
+
+ // Clear out existing assigned nodes
+ if (mInManualShadowRoot) {
+ if (!mAssignedNodes.IsEmpty()) {
+ changedSlots.EnsureInserted(this);
+ if (root) {
+ root->InvalidateStyleAndLayoutOnSubtree(this);
+ }
+ ClearAssignedNodes();
+ }
+
+ MOZ_ASSERT(mAssignedNodes.IsEmpty());
+ host = GetContainingShadowHost();
+ }
+
+ for (const OwningElementOrText& elementOrText : aNodes) {
+ nsIContent* content;
+ if (elementOrText.IsElement()) {
+ content = elementOrText.GetAsElement();
+ } else {
+ content = elementOrText.GetAsText();
+ }
+
+ MOZ_ASSERT(content);
+ // XXXsmaug Should we have a helper for
+ // https://infra.spec.whatwg.org/#ordered-set?
+ if (content->GetManualSlotAssignment() != this) {
+ if (HTMLSlotElement* oldSlot = content->GetAssignedSlot()) {
+ if (changedSlots.EnsureInserted(oldSlot)) {
+ if (root) {
+ MOZ_ASSERT(oldSlot->GetContainingShadow() == root);
+ root->InvalidateStyleAndLayoutOnSubtree(oldSlot);
+ }
+ }
+ }
+
+ if (changedSlots.EnsureInserted(this)) {
+ if (root) {
+ root->InvalidateStyleAndLayoutOnSubtree(this);
+ }
+ }
+ // 3.1 (HTML Spec) If content's manual slot assignment refers to a slot,
+ // then remove node from that slot's manually assigned nodes. 3.2 (HTML
+ // Spec) Set content's manual slot assignment to this.
+ if (HTMLSlotElement* oldSlot = content->GetManualSlotAssignment()) {
+ oldSlot->RemoveManuallyAssignedNode(*content);
+ }
+ content->SetManualSlotAssignment(this);
+ mManuallyAssignedNodes.AppendElement(content);
+
+ if (root && host && content->GetParent() == host) {
+ // Equivalent to 4.2.2.4.3 (DOM Spec) `Set slot's assigned nodes to
+ // slottables`
+ root->MaybeReassignContent(*content);
+ }
+ }
+ }
+
+ // The `assign slottables` step is completed already at this point,
+ // however we haven't fired the `slotchange` event yet because this
+ // needs to be done in tree order.
+ if (root) {
+ for (nsIContent* child = root->GetFirstChild(); child;
+ child = child->GetNextNode()) {
+ if (HTMLSlotElement* slot = HTMLSlotElement::FromNode(child)) {
+ if (changedSlots.EnsureRemoved(slot)) {
+ slot->EnqueueSlotChangeEvent();
+ }
+ }
+ }
+ MOZ_ASSERT(changedSlots.IsEmpty());
+ }
+}
+
+void HTMLSlotElement::InsertAssignedNode(uint32_t aIndex, nsIContent& aNode) {
+ MOZ_ASSERT(!aNode.GetAssignedSlot(), "Losing track of a slot");
+ mAssignedNodes.InsertElementAt(aIndex, &aNode);
+ aNode.SetAssignedSlot(this);
+ SlotAssignedNodeChanged(this, aNode);
+}
+
+void HTMLSlotElement::AppendAssignedNode(nsIContent& aNode) {
+ MOZ_ASSERT(!aNode.GetAssignedSlot(), "Losing track of a slot");
+ mAssignedNodes.AppendElement(&aNode);
+ aNode.SetAssignedSlot(this);
+ SlotAssignedNodeChanged(this, aNode);
+}
+
+void HTMLSlotElement::RemoveAssignedNode(nsIContent& aNode) {
+ // This one runs from unlinking, so we can't guarantee that the slot pointer
+ // hasn't been cleared.
+ MOZ_ASSERT(!aNode.GetAssignedSlot() || aNode.GetAssignedSlot() == this,
+ "How exactly?");
+ mAssignedNodes.RemoveElement(&aNode);
+ aNode.SetAssignedSlot(nullptr);
+ SlotAssignedNodeChanged(this, aNode);
+}
+
+void HTMLSlotElement::ClearAssignedNodes() {
+ for (RefPtr<nsINode>& node : mAssignedNodes) {
+ MOZ_ASSERT(!node->AsContent()->GetAssignedSlot() ||
+ node->AsContent()->GetAssignedSlot() == this,
+ "How exactly?");
+ node->AsContent()->SetAssignedSlot(nullptr);
+ }
+
+ mAssignedNodes.Clear();
+}
+
+void HTMLSlotElement::EnqueueSlotChangeEvent() {
+ if (mInSignalSlotList) {
+ return;
+ }
+
+ // FIXME(bug 1459704): Need to figure out how to deal with microtasks posted
+ // during shutdown.
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) {
+ return;
+ }
+
+ DocGroup* docGroup = OwnerDoc()->GetDocGroup();
+ if (!docGroup) {
+ return;
+ }
+
+ mInSignalSlotList = true;
+ docGroup->SignalSlotChange(*this);
+}
+
+void HTMLSlotElement::FireSlotChangeEvent() {
+ nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, u"slotchange"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+}
+
+void HTMLSlotElement::RemoveManuallyAssignedNode(nsIContent& aNode) {
+ mManuallyAssignedNodes.RemoveElement(&aNode);
+ RemoveAssignedNode(aNode);
+}
+
+JSObject* HTMLSlotElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLSlotElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLSlotElement.h b/dom/html/HTMLSlotElement.h
new file mode 100644
index 0000000000..fa12f0df26
--- /dev/null
+++ b/dom/html/HTMLSlotElement.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLSlotElement_h
+#define mozilla_dom_HTMLSlotElement_h
+
+#include "nsGenericHTMLElement.h"
+#include "nsTArray.h"
+#include "mozilla/dom/HTMLSlotElementBinding.h"
+
+namespace mozilla::dom {
+
+class HTMLSlotElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLSlotElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLSlotElement, slot)
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLSlotElement,
+ nsGenericHTMLElement)
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // nsIContent
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent) override;
+
+ void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ // WebIDL
+ void SetName(const nsAString& aName, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aRv);
+ }
+
+ void GetName(nsAString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); }
+
+ void AssignedNodes(const AssignedNodesOptions& aOptions,
+ nsTArray<RefPtr<nsINode>>& aNodes);
+
+ void AssignedElements(const AssignedNodesOptions& aOptions,
+ nsTArray<RefPtr<Element>>& aNodes);
+
+ void Assign(const Sequence<OwningElementOrText>& aNodes);
+
+ // Helper methods
+ const nsTArray<RefPtr<nsINode>>& AssignedNodes() const;
+ const nsTArray<nsINode*>& ManuallyAssignedNodes() const;
+ void InsertAssignedNode(uint32_t aIndex, nsIContent&);
+ void AppendAssignedNode(nsIContent&);
+ void RemoveAssignedNode(nsIContent&);
+ void ClearAssignedNodes();
+
+ void EnqueueSlotChangeEvent();
+ void RemovedFromSignalSlotList() {
+ MOZ_ASSERT(mInSignalSlotList);
+ mInSignalSlotList = false;
+ }
+
+ void FireSlotChangeEvent();
+
+ void RemoveManuallyAssignedNode(nsIContent&);
+
+ protected:
+ virtual ~HTMLSlotElement();
+ JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final;
+
+ nsTArray<RefPtr<nsINode>> mAssignedNodes;
+ nsTArray<nsINode*> mManuallyAssignedNodes;
+
+ // Whether we're in the signal slot list of our unit of related similar-origin
+ // browsing contexts.
+ //
+ // https://dom.spec.whatwg.org/#signal-slot-list
+ bool mInSignalSlotList = false;
+
+ bool mInManualShadowRoot = false;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLSlotElement_h
diff --git a/dom/html/HTMLSourceElement.cpp b/dom/html/HTMLSourceElement.cpp
new file mode 100644
index 0000000000..b08d449594
--- /dev/null
+++ b/dom/html/HTMLSourceElement.cpp
@@ -0,0 +1,230 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLSourceElement.h"
+#include "mozilla/dom/HTMLSourceElementBinding.h"
+
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/HTMLImageElement.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/ResponsiveImageSelector.h"
+#include "mozilla/dom/MediaList.h"
+#include "mozilla/dom/MediaSource.h"
+
+#include "mozilla/dom/BlobURLProtocolHandler.h"
+#include "mozilla/AttributeStyles.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/Preferences.h"
+
+#include "nsGkAtoms.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Source)
+
+namespace mozilla::dom {
+
+HTMLSourceElement::HTMLSourceElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLSourceElement::~HTMLSourceElement() = default;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLSourceElement, nsGenericHTMLElement,
+ mSrcMediaSource)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLSourceElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLSourceElement)
+
+bool HTMLSourceElement::MatchesCurrentMedia() {
+ if (mMediaList) {
+ return mMediaList->Matches(*OwnerDoc());
+ }
+
+ // No media specified
+ return true;
+}
+
+/* static */
+bool HTMLSourceElement::WouldMatchMediaForDocument(const nsAString& aMedia,
+ const Document* aDocument) {
+ if (aMedia.IsEmpty()) {
+ return true;
+ }
+
+ RefPtr<MediaList> mediaList =
+ MediaList::Create(NS_ConvertUTF16toUTF8(aMedia));
+ return mediaList->Matches(*aDocument);
+}
+
+void HTMLSourceElement::UpdateMediaList(const nsAttrValue* aValue) {
+ mMediaList = nullptr;
+ if (!aValue) {
+ return;
+ }
+
+ NS_ConvertUTF16toUTF8 mediaStr(aValue->GetStringValue());
+ mMediaList = MediaList::Create(mediaStr);
+}
+
+bool HTMLSourceElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None &&
+ (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height)) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLSourceElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::srcset) {
+ mSrcsetTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue ? aValue->GetStringValue() : EmptyString(),
+ aMaybeScriptedPrincipal);
+ }
+ // If we are associated with a <picture> with a valid <img>, notify it of
+ // responsive parameter changes
+ if (aNameSpaceID == kNameSpaceID_None &&
+ (aName == nsGkAtoms::srcset || aName == nsGkAtoms::sizes ||
+ aName == nsGkAtoms::media || aName == nsGkAtoms::type) &&
+ IsInPicture()) {
+ if (aName == nsGkAtoms::media) {
+ UpdateMediaList(aValue);
+ }
+
+ nsString strVal = aValue ? aValue->GetStringValue() : EmptyString();
+ // Find all img siblings after this <source> and notify them of the change
+ nsCOMPtr<nsIContent> sibling = AsContent();
+ while ((sibling = sibling->GetNextSibling())) {
+ if (auto* img = HTMLImageElement::FromNode(sibling)) {
+ if (aName == nsGkAtoms::srcset) {
+ img->PictureSourceSrcsetChanged(this, strVal, aNotify);
+ } else if (aName == nsGkAtoms::sizes) {
+ img->PictureSourceSizesChanged(this, strVal, aNotify);
+ } else if (aName == nsGkAtoms::media || aName == nsGkAtoms::type) {
+ img->PictureSourceMediaOrTypeChanged(this, aNotify);
+ }
+ }
+ }
+ } else if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::media) {
+ UpdateMediaList(aValue);
+ } else if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::src) {
+ mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue ? aValue->GetStringValue() : EmptyString(),
+ aMaybeScriptedPrincipal);
+ mSrcMediaSource = nullptr;
+ if (aValue) {
+ nsString srcStr = aValue->GetStringValue();
+ nsCOMPtr<nsIURI> uri;
+ NewURIFromString(srcStr, getter_AddRefs(uri));
+ if (uri && IsMediaSourceURI(uri)) {
+ NS_GetSourceForMediaSourceURI(uri, getter_AddRefs(mSrcMediaSource));
+ }
+ }
+ } else if (aNameSpaceID == kNameSpaceID_None &&
+ IsAttributeMappedToImages(aName) && IsInPicture()) {
+ BuildMappedAttributesForImage();
+
+ nsCOMPtr<nsIContent> sibling = AsContent();
+ while ((sibling = sibling->GetNextSibling())) {
+ if (auto* img = HTMLImageElement::FromNode(sibling)) {
+ img->PictureSourceDimensionChanged(this, aNotify);
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+nsresult HTMLSourceElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (auto* media = HTMLMediaElement::FromNode(aParent)) {
+ media->NotifyAddedSource();
+ }
+
+ if (aParent.IsHTMLElement(nsGkAtoms::picture)) {
+ BuildMappedAttributesForImage();
+ } else {
+ mMappedAttributesForImage = nullptr;
+ }
+
+ return NS_OK;
+}
+
+void HTMLSourceElement::UnbindFromTree(bool aNullParent) {
+ mMappedAttributesForImage = nullptr;
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+JSObject* HTMLSourceElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLSourceElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/**
+ * Helper to map the image source attributes.
+ * Note: This will override the declaration created by the presentation
+ * attributes of HTMLImageElement (i.e. mapped by MapImageSizeAttributeInto).
+ * https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element
+ */
+void HTMLSourceElement::BuildMappedAttributesForImage() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mMappedAttributesForImage = nullptr;
+
+ Document* document = GetComposedDoc();
+ if (!document) {
+ return;
+ }
+
+ const nsAttrValue* width = mAttrs.GetAttr(nsGkAtoms::width);
+ const nsAttrValue* height = mAttrs.GetAttr(nsGkAtoms::height);
+ if (!width && !height) {
+ return;
+ }
+
+ MappedDeclarationsBuilder builder(*this, *document);
+ // We should set the missing property values with auto value to make sure it
+ // overrides the declaration created by the presentation attributes of
+ // HTMLImageElement. This can make sure we compute the ratio-dependent axis
+ // size properly by the natural aspect-ratio of the image.
+ //
+ // Note: The spec doesn't specify this, so we follow the implementation in
+ // other browsers.
+ // Spec issue: https://github.com/whatwg/html/issues/8178.
+ if (width) {
+ MapDimensionAttributeInto(builder, eCSSProperty_width, *width);
+ } else {
+ builder.SetAutoValue(eCSSProperty_width);
+ }
+
+ if (height) {
+ MapDimensionAttributeInto(builder, eCSSProperty_height, *height);
+ } else {
+ builder.SetAutoValue(eCSSProperty_height);
+ }
+
+ if (width && height) {
+ DoMapAspectRatio(*width, *height, builder);
+ } else {
+ builder.SetAutoValue(eCSSProperty_aspect_ratio);
+ }
+ mMappedAttributesForImage = builder.TakeDeclarationBlock();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLSourceElement.h b/dom/html/HTMLSourceElement.h
new file mode 100644
index 0000000000..4d4a1b212d
--- /dev/null
+++ b/dom/html/HTMLSourceElement.h
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLSourceElement_h
+#define mozilla_dom_HTMLSourceElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+
+class nsAttrValue;
+
+namespace mozilla::dom {
+
+class MediaList;
+
+class HTMLSourceElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLSourceElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLSourceElement,
+ nsGenericHTMLElement)
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLSourceElement, source)
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // Override BindToTree() so that we can trigger a load when we add a
+ // child source element.
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+
+ void UnbindFromTree(bool aNullParent) override;
+
+ // If this element's media attr matches for its owner document. Returns true
+ // if no media attr was set.
+ bool MatchesCurrentMedia();
+
+ // True if a source tag would match the given media attribute for the
+ // specified document. Used by the preloader to determine valid <source> tags
+ // prior to DOM creation.
+ static bool WouldMatchMediaForDocument(const nsAString& aMediaStr,
+ const Document* aDocument);
+
+ // Return the MediaSource object if any associated with the src attribute
+ // when it was set.
+ MediaSource* GetSrcMediaSource() { return mSrcMediaSource; };
+
+ // WebIDL
+ void GetSrc(nsString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); }
+ void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal,
+ mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, rv);
+ }
+
+ nsIPrincipal* GetSrcTriggeringPrincipal() const {
+ return mSrcTriggeringPrincipal;
+ }
+
+ nsIPrincipal* GetSrcsetTriggeringPrincipal() const {
+ return mSrcsetTriggeringPrincipal;
+ }
+
+ void GetType(DOMString& aType) { GetHTMLAttr(nsGkAtoms::type, aType); }
+ void SetType(const nsAString& aType, ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::type, aType, rv);
+ }
+
+ void GetSrcset(DOMString& aSrcset) {
+ GetHTMLAttr(nsGkAtoms::srcset, aSrcset);
+ }
+ void SetSrcset(const nsAString& aSrcset, nsIPrincipal* aTriggeringPrincipal,
+ mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::srcset, aSrcset, aTriggeringPrincipal, rv);
+ }
+
+ void GetSizes(DOMString& aSizes) { GetHTMLAttr(nsGkAtoms::sizes, aSizes); }
+ void SetSizes(const nsAString& aSizes, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::sizes, aSizes, rv);
+ }
+
+ void GetMedia(DOMString& aMedia) { GetHTMLAttr(nsGkAtoms::media, aMedia); }
+ void SetMedia(const nsAString& aMedia, mozilla::ErrorResult& rv) {
+ SetHTMLAttr(nsGkAtoms::media, aMedia, rv);
+ }
+
+ uint32_t Width() const {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::width, 0);
+ }
+ void SetWidth(uint32_t aWidth, ErrorResult& aRv) {
+ SetUnsignedIntAttr(nsGkAtoms::width, aWidth, 0, aRv);
+ }
+
+ uint32_t Height() const {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::height, 0);
+ }
+ void SetHeight(uint32_t aHeight, ErrorResult& aRv) {
+ SetUnsignedIntAttr(nsGkAtoms::height, aHeight, 0, aRv);
+ }
+
+ const StyleLockedDeclarationBlock* GetAttributesMappedForImage() const {
+ return mMappedAttributesForImage;
+ }
+
+ static bool IsAttributeMappedToImages(const nsAtom* aAttribute) {
+ return aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height;
+ }
+
+ protected:
+ virtual ~HTMLSourceElement();
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+
+ private:
+ // Generates a new MediaList using the given input
+ void UpdateMediaList(const nsAttrValue* aValue);
+
+ void BuildMappedAttributesForImage();
+
+ bool IsInPicture() const {
+ return GetParentElement() &&
+ GetParentElement()->IsHTMLElement(nsGkAtoms::picture);
+ }
+
+ RefPtr<MediaList> mMediaList;
+ RefPtr<MediaSource> mSrcMediaSource;
+
+ // The triggering principal for the src attribute.
+ nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal;
+
+ // The triggering principal for the srcset attribute.
+ nsCOMPtr<nsIPrincipal> mSrcsetTriggeringPrincipal;
+
+ // The mapped attributes to HTMLImageElement if we are associated with a
+ // <picture> with a valid <img>.
+ RefPtr<StyleLockedDeclarationBlock> mMappedAttributesForImage;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLSourceElement_h
diff --git a/dom/html/HTMLSpanElement.cpp b/dom/html/HTMLSpanElement.cpp
new file mode 100644
index 0000000000..a7c6efe919
--- /dev/null
+++ b/dom/html/HTMLSpanElement.cpp
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLSpanElement.h"
+#include "mozilla/dom/HTMLSpanElementBinding.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Span)
+
+namespace mozilla::dom {
+
+HTMLSpanElement::~HTMLSpanElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLSpanElement)
+
+JSObject* HTMLSpanElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLSpanElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLSpanElement.h b/dom/html/HTMLSpanElement.h
new file mode 100644
index 0000000000..311ecf2647
--- /dev/null
+++ b/dom/html/HTMLSpanElement.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLSpanElement_h
+#define mozilla_dom_HTMLSpanElement_h
+
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLSpanElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLSpanElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLSpanElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLSpanElement_h
diff --git a/dom/html/HTMLStyleElement.cpp b/dom/html/HTMLStyleElement.cpp
new file mode 100644
index 0000000000..ed4c141897
--- /dev/null
+++ b/dom/html/HTMLStyleElement.cpp
@@ -0,0 +1,202 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "mozilla/dom/HTMLStyleElement.h"
+#include "mozilla/dom/HTMLStyleElementBinding.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/FetchPriority.h"
+#include "mozilla/dom/ReferrerInfo.h"
+#include "nsUnicharUtils.h"
+#include "nsThreadUtils.h"
+#include "nsContentUtils.h"
+#include "nsStubMutationObserver.h"
+#include "nsDOMTokenList.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Style)
+
+namespace mozilla::dom {
+
+HTMLStyleElement::HTMLStyleElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ AddMutationObserver(this);
+}
+
+HTMLStyleElement::~HTMLStyleElement() = default;
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLStyleElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLStyleElement,
+ nsGenericHTMLElement)
+ tmp->LinkStyle::Traverse(cb);
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlocking)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLStyleElement,
+ nsGenericHTMLElement)
+ tmp->LinkStyle::Unlink();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlocking)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLStyleElement,
+ nsGenericHTMLElement,
+ nsIMutationObserver)
+
+NS_IMPL_ELEMENT_CLONE(HTMLStyleElement)
+
+bool HTMLStyleElement::Disabled() const {
+ StyleSheet* ss = GetSheet();
+ return ss && ss->Disabled();
+}
+
+void HTMLStyleElement::SetDisabled(bool aDisabled) {
+ if (StyleSheet* ss = GetSheet()) {
+ ss->SetDisabled(aDisabled);
+ }
+}
+
+void HTMLStyleElement::CharacterDataChanged(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {
+ ContentChanged(aContent);
+}
+
+void HTMLStyleElement::ContentAppended(nsIContent* aFirstNewContent) {
+ ContentChanged(aFirstNewContent->GetParent());
+}
+
+void HTMLStyleElement::ContentInserted(nsIContent* aChild) {
+ ContentChanged(aChild);
+}
+
+void HTMLStyleElement::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ ContentChanged(aChild);
+}
+
+void HTMLStyleElement::ContentChanged(nsIContent* aContent) {
+ mTriggeringPrincipal = nullptr;
+ if (nsContentUtils::IsInSameAnonymousTree(this, aContent)) {
+ Unused << UpdateStyleSheetInternal(nullptr, nullptr);
+ }
+}
+
+nsresult HTMLStyleElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ LinkStyle::BindToTree();
+ return rv;
+}
+
+void HTMLStyleElement::UnbindFromTree(bool aNullParent) {
+ RefPtr<Document> oldDoc = GetUncomposedDoc();
+ ShadowRoot* oldShadow = GetContainingShadow();
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+
+ Unused << UpdateStyleSheetInternal(oldDoc, oldShadow);
+}
+
+void HTMLStyleElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::title || aName == nsGkAtoms::media ||
+ aName == nsGkAtoms::type) {
+ Unused << UpdateStyleSheetInternal(nullptr, nullptr, ForceUpdate::Yes);
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+void HTMLStyleElement::GetInnerHTML(nsAString& aInnerHTML,
+ OOMReporter& aError) {
+ if (!nsContentUtils::GetNodeTextContent(this, false, aInnerHTML, fallible)) {
+ aError.ReportOOM();
+ }
+}
+
+void HTMLStyleElement::SetInnerHTML(const nsAString& aInnerHTML,
+ nsIPrincipal* aScriptedPrincipal,
+ ErrorResult& aError) {
+ SetTextContentInternal(aInnerHTML, aScriptedPrincipal, aError);
+}
+
+void HTMLStyleElement::SetTextContentInternal(const nsAString& aTextContent,
+ nsIPrincipal* aScriptedPrincipal,
+ ErrorResult& aError) {
+ // Per spec, if we're setting text content to an empty string and don't
+ // already have any children, we should not trigger any mutation observers, or
+ // re-parse the stylesheet.
+ if (aTextContent.IsEmpty() && !GetFirstChild()) {
+ nsIPrincipal* principal =
+ mTriggeringPrincipal ? mTriggeringPrincipal.get() : NodePrincipal();
+ if (principal == aScriptedPrincipal) {
+ return;
+ }
+ }
+
+ const bool updatesWereEnabled = mUpdatesEnabled;
+ DisableUpdates();
+
+ aError = nsContentUtils::SetNodeTextContent(this, aTextContent, true);
+ if (updatesWereEnabled) {
+ mTriggeringPrincipal = aScriptedPrincipal;
+ Unused << EnableUpdatesAndUpdateStyleSheet(nullptr);
+ }
+}
+
+void HTMLStyleElement::SetDevtoolsAsTriggeringPrincipal() {
+ mTriggeringPrincipal = CreateDevtoolsPrincipal();
+}
+
+Maybe<LinkStyle::SheetInfo> HTMLStyleElement::GetStyleSheetInfo() {
+ if (!IsCSSMimeTypeAttributeForStyleElement(*this)) {
+ return Nothing();
+ }
+
+ nsAutoString title;
+ nsAutoString media;
+ GetTitleAndMediaForElement(*this, title, media);
+
+ return Some(SheetInfo{
+ *OwnerDoc(),
+ this,
+ nullptr,
+ do_AddRef(mTriggeringPrincipal),
+ MakeAndAddRef<ReferrerInfo>(*this),
+ CORS_NONE,
+ title,
+ media,
+ /* integrity = */ u""_ns,
+ /* nsStyleUtil::CSPAllowsInlineStyle takes care of nonce checking for
+ inline styles. Bug 1607011 */
+ /* nonce = */ u""_ns,
+ HasAlternateRel::No,
+ IsInline::Yes,
+ IsExplicitlyEnabled::No,
+ FetchPriority::Auto,
+ });
+}
+
+JSObject* HTMLStyleElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLStyleElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsDOMTokenList* HTMLStyleElement::Blocking() {
+ if (!mBlocking) {
+ mBlocking =
+ new nsDOMTokenList(this, nsGkAtoms::blocking, sSupportedBlockingValues);
+ }
+ return mBlocking;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLStyleElement.h b/dom/html/HTMLStyleElement.h
new file mode 100644
index 0000000000..5815740ac5
--- /dev/null
+++ b/dom/html/HTMLStyleElement.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLStyleElement_h
+#define mozilla_dom_HTMLStyleElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/LinkStyle.h"
+#include "nsGenericHTMLElement.h"
+#include "nsStubMutationObserver.h"
+
+class nsDOMTokenList;
+
+namespace mozilla::dom {
+
+class HTMLStyleElement final : public nsGenericHTMLElement,
+ public LinkStyle,
+ public nsStubMutationObserver {
+ public:
+ explicit HTMLStyleElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // CC
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLStyleElement,
+ nsGenericHTMLElement)
+
+ void GetInnerHTML(nsAString& aInnerHTML, OOMReporter& aError) override;
+ using nsGenericHTMLElement::SetInnerHTML;
+ virtual void SetInnerHTML(const nsAString& aInnerHTML,
+ nsIPrincipal* aSubjectPrincipal,
+ mozilla::ErrorResult& aError) override;
+ virtual void SetTextContentInternal(const nsAString& aTextContent,
+ nsIPrincipal* aSubjectPrincipal,
+ mozilla::ErrorResult& aError) override;
+ /**
+ * Mark this style element with a devtools-specific principal that
+ * skips Content Security Policy unsafe-inline checks. This triggering
+ * principal will be overwritten by any callers that set textContent
+ * or innerHTML on this element.
+ */
+ void SetDevtoolsAsTriggeringPrincipal();
+
+ virtual nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ virtual void UnbindFromTree(bool aNullParent = true) override;
+ virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // nsIMutationObserver
+ NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+
+ bool Disabled() const;
+ void SetDisabled(bool aDisabled);
+ void GetMedia(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::media, aValue); }
+ void SetMedia(const nsAString& aMedia, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::media, aMedia, aError);
+ }
+ void GetType(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::type, aValue); }
+ void SetType(const nsAString& aType, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::type, aType, aError);
+ }
+
+ nsDOMTokenList* Blocking();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ protected:
+ virtual ~HTMLStyleElement();
+
+ nsIContent& AsContent() final { return *this; }
+ const LinkStyle* AsLinkStyle() const final { return this; }
+ Maybe<SheetInfo> GetStyleSheetInfo() final;
+
+ /**
+ * Common method to call from the various mutation observer methods.
+ * aContent is a content node that's either the one that changed or its
+ * parent; we should only respond to the change if aContent is non-anonymous.
+ */
+ void ContentChanged(nsIContent* aContent);
+
+ RefPtr<nsDOMTokenList> mBlocking;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/html/HTMLSummaryElement.cpp b/dom/html/HTMLSummaryElement.cpp
new file mode 100644
index 0000000000..d1fcf22598
--- /dev/null
+++ b/dom/html/HTMLSummaryElement.cpp
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLSummaryElement.h"
+
+#include "mozilla/dom/HTMLDetailsElement.h"
+#include "mozilla/dom/HTMLElementBinding.h"
+#include "mozilla/dom/HTMLUnknownElement.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TextEvents.h"
+#include "nsFocusManager.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Summary)
+
+namespace mozilla::dom {
+
+HTMLSummaryElement::~HTMLSummaryElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLSummaryElement)
+
+nsresult HTMLSummaryElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
+ nsresult rv = NS_OK;
+ if (!aVisitor.mPresContext) {
+ return rv;
+ }
+
+ if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) {
+ return rv;
+ }
+
+ if (!IsMainSummary()) {
+ return rv;
+ }
+
+ WidgetEvent* const event = aVisitor.mEvent;
+ nsCOMPtr<Element> target =
+ do_QueryInterface(event->GetOriginalDOMEventTarget());
+ if (nsContentUtils::IsInInteractiveHTMLContent(target, this)) {
+ return NS_OK;
+ }
+
+ if (event->HasMouseEventMessage()) {
+ WidgetMouseEvent* mouseEvent = event->AsMouseEvent();
+
+ if (mouseEvent->IsLeftClickEvent()) {
+ RefPtr<HTMLDetailsElement> details = GetDetails();
+ MOZ_ASSERT(details,
+ "Expected to find details since this is the main summary!");
+
+ // When dispatching a synthesized mouse click event to a details element
+ // with 'display: none', both Chrome and Safari do not toggle the 'open'
+ // attribute. We had tried to be compatible with this behavior, but found
+ // more inconsistency in test cases in bug 1245424. So we stop doing that.
+ details->ToggleOpen();
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ return NS_OK;
+ }
+ } // event->HasMouseEventMessage()
+
+ if (event->HasKeyEventMessage() && event->IsTrusted()) {
+ HandleKeyboardActivation(aVisitor);
+ }
+ return rv;
+}
+
+bool HTMLSummaryElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ bool disallowOverridingFocusability = nsGenericHTMLElement::IsHTMLFocusable(
+ aWithMouse, aIsFocusable, aTabIndex);
+
+ if (disallowOverridingFocusability || !IsMainSummary()) {
+ return disallowOverridingFocusability;
+ }
+
+ // The main summary element is focusable.
+ *aIsFocusable = true;
+
+ // Give a chance to allow the subclass to override aIsFocusable.
+ return false;
+}
+
+int32_t HTMLSummaryElement::TabIndexDefault() {
+ // Make the main summary be able to navigate via tab, and be focusable.
+ // See nsGenericHTMLElement::IsHTMLFocusable().
+ return IsMainSummary() ? 0 : nsGenericHTMLElement::TabIndexDefault();
+}
+
+bool HTMLSummaryElement::IsMainSummary() const {
+ HTMLDetailsElement* details = GetDetails();
+ if (!details) {
+ return false;
+ }
+
+ return details->GetFirstSummary() == this ||
+ GetContainingShadow() == details->GetShadowRoot();
+}
+
+HTMLDetailsElement* HTMLSummaryElement::GetDetails() const {
+ if (auto* details = HTMLDetailsElement::FromNodeOrNull(GetParent())) {
+ return details;
+ }
+ if (!HasBeenInUAWidget()) {
+ return nullptr;
+ }
+ return HTMLDetailsElement::FromNodeOrNull(GetContainingShadowHost());
+}
+
+JSObject* HTMLSummaryElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLSummaryElement.h b/dom/html/HTMLSummaryElement.h
new file mode 100644
index 0000000000..b70e8eebbb
--- /dev/null
+++ b/dom/html/HTMLSummaryElement.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLSummaryElement_h
+#define mozilla_dom_HTMLSummaryElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+class HTMLDetailsElement;
+
+// HTMLSummaryElement implements the <summary> tag, which is used as a summary
+// or legend of the <details> tag. Please see the spec for more information.
+// https://html.spec.whatwg.org/multipage/forms.html#the-details-element
+//
+class HTMLSummaryElement final : public nsGenericHTMLElement {
+ public:
+ using NodeInfo = mozilla::dom::NodeInfo;
+
+ explicit HTMLSummaryElement(already_AddRefed<NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLSummaryElement, summary)
+
+ nsresult Clone(NodeInfo*, nsINode** aResult) const override;
+
+ nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+
+ int32_t TabIndexDefault() override;
+
+ // Return true if this is the first summary element child of a details or the
+ // default summary element.
+ bool IsMainSummary() const;
+
+ // Return the details element which contains this summary. Otherwise return
+ // nullptr if there is no such details element.
+ HTMLDetailsElement* GetDetails() const;
+
+ protected:
+ virtual ~HTMLSummaryElement();
+
+ JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLSummaryElement_h */
diff --git a/dom/html/HTMLTableCaptionElement.cpp b/dom/html/HTMLTableCaptionElement.cpp
new file mode 100644
index 0000000000..8b2657b4f3
--- /dev/null
+++ b/dom/html/HTMLTableCaptionElement.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTableCaptionElement.h"
+
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsAttrValueInlines.h"
+#include "nsStyleConsts.h"
+#include "mozilla/dom/HTMLTableCaptionElementBinding.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(TableCaption)
+
+namespace mozilla::dom {
+
+HTMLTableCaptionElement::~HTMLTableCaptionElement() = default;
+
+JSObject* HTMLTableCaptionElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTableCaptionElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLTableCaptionElement)
+
+static const nsAttrValue::EnumTable kCaptionAlignTable[] = {
+ {"top", StyleCaptionSide::Top},
+ {"bottom", StyleCaptionSide::Bottom},
+ {nullptr, 0}};
+
+bool HTMLTableCaptionElement::ParseAttribute(
+ int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) {
+ if (aAttribute == nsGkAtoms::align && aNamespaceID == kNameSpaceID_None) {
+ return aResult.ParseEnumValue(aValue, kCaptionAlignTable, false);
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLTableCaptionElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty_caption_side)) {
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align);
+ if (value && value->Type() == nsAttrValue::eEnum) {
+ aBuilder.SetKeywordValue(eCSSProperty_caption_side,
+ value->GetEnumValue());
+ }
+ }
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLTableCaptionElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {{nsGkAtoms::align},
+ {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLTableCaptionElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTableCaptionElement.h b/dom/html/HTMLTableCaptionElement.h
new file mode 100644
index 0000000000..452ef7833d
--- /dev/null
+++ b/dom/html/HTMLTableCaptionElement.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLTableCaptionElement_h
+#define mozilla_dom_HTMLTableCaptionElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLTableCaptionElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLTableCaptionElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetHasWeirdParserInsertionMode();
+ }
+
+ void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); }
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLTableCaptionElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLTableCaptionElement_h */
diff --git a/dom/html/HTMLTableCellElement.cpp b/dom/html/HTMLTableCellElement.cpp
new file mode 100644
index 0000000000..c260323a8b
--- /dev/null
+++ b/dom/html/HTMLTableCellElement.cpp
@@ -0,0 +1,219 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTableCellElement.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLTableElement.h"
+#include "mozilla/dom/HTMLTableRowElement.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsAttrValueInlines.h"
+#include "celldata.h"
+#include "mozilla/dom/HTMLTableCellElementBinding.h"
+
+namespace {
+enum class StyleCellScope : uint8_t { Row, Col, Rowgroup, Colgroup };
+} // namespace
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(TableCell)
+
+namespace mozilla::dom {
+
+HTMLTableCellElement::~HTMLTableCellElement() = default;
+
+JSObject* HTMLTableCellElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTableCellElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLTableCellElement)
+
+// protected method
+HTMLTableRowElement* HTMLTableCellElement::GetRow() const {
+ return HTMLTableRowElement::FromNodeOrNull(GetParent());
+}
+
+// protected method
+HTMLTableElement* HTMLTableCellElement::GetTable() const {
+ nsIContent* parent = GetParent();
+ if (!parent) {
+ return nullptr;
+ }
+
+ // parent should be a row.
+ nsIContent* section = parent->GetParent();
+ if (!section) {
+ return nullptr;
+ }
+
+ if (section->IsHTMLElement(nsGkAtoms::table)) {
+ // XHTML, without a row group.
+ return static_cast<HTMLTableElement*>(section);
+ }
+
+ // We have a row group.
+ nsIContent* result = section->GetParent();
+ if (result && result->IsHTMLElement(nsGkAtoms::table)) {
+ return static_cast<HTMLTableElement*>(result);
+ }
+
+ return nullptr;
+}
+
+int32_t HTMLTableCellElement::CellIndex() const {
+ HTMLTableRowElement* row = GetRow();
+ if (!row) {
+ return -1;
+ }
+
+ nsIHTMLCollection* cells = row->Cells();
+ if (!cells) {
+ return -1;
+ }
+
+ uint32_t numCells = cells->Length();
+ for (uint32_t i = 0; i < numCells; i++) {
+ if (cells->Item(i) == this) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+StyleLockedDeclarationBlock*
+HTMLTableCellElement::GetMappedAttributesInheritedFromTable() const {
+ if (HTMLTableElement* table = GetTable()) {
+ return table->GetAttributesMappedForCell();
+ }
+ return nullptr;
+}
+
+void HTMLTableCellElement::GetAlign(DOMString& aValue) {
+ if (!GetAttr(nsGkAtoms::align, aValue)) {
+ // There's no align attribute, ask the row for the alignment.
+ HTMLTableRowElement* row = GetRow();
+ if (row) {
+ row->GetAlign(aValue);
+ }
+ }
+}
+
+static const nsAttrValue::EnumTable kCellScopeTable[] = {
+ {"row", StyleCellScope::Row},
+ {"col", StyleCellScope::Col},
+ {"rowgroup", StyleCellScope::Rowgroup},
+ {"colgroup", StyleCellScope::Colgroup},
+ {nullptr, 0}};
+
+void HTMLTableCellElement::GetScope(DOMString& aScope) {
+ GetEnumAttr(nsGkAtoms::scope, nullptr, aScope);
+}
+
+bool HTMLTableCellElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ /* ignore these attributes, stored simply as strings
+ abbr, axis, ch, headers
+ */
+ if (aAttribute == nsGkAtoms::colspan) {
+ aResult.ParseClampedNonNegativeInt(aValue, 1, 1, MAX_COLSPAN);
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::rowspan) {
+ aResult.ParseClampedNonNegativeInt(aValue, 1, 0, MAX_ROWSPAN);
+ // quirks mode does not honor the special html 4 value of 0
+ if (aResult.GetIntegerValue() == 0 && InNavQuirksMode(OwnerDoc())) {
+ aResult.SetTo(1, &aValue);
+ }
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::height) {
+ return aResult.ParseNonzeroHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::width) {
+ return aResult.ParseNonzeroHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseTableCellHAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::bgcolor) {
+ return aResult.ParseColor(aValue);
+ }
+ if (aAttribute == nsGkAtoms::scope) {
+ return aResult.ParseEnumValue(aValue, kCellScopeTable, false);
+ }
+ if (aAttribute == nsGkAtoms::valign) {
+ return ParseTableVAlignValue(aValue, aResult);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseBackgroundAttribute(
+ aNamespaceID, aAttribute, aValue, aResult) ||
+ nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLTableCellElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapImageSizeAttributesInto(aBuilder);
+
+ if (!aBuilder.PropertyIsSet(eCSSProperty_text_wrap_mode)) {
+ // nowrap: enum
+ if (aBuilder.GetAttr(nsGkAtoms::nowrap)) {
+ // See if our width is not a nonzero integer width.
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::width);
+ nsCompatibility mode = aBuilder.Document().GetCompatibilityMode();
+ if (!value || value->Type() != nsAttrValue::eInteger ||
+ value->GetIntegerValue() == 0 || eCompatibility_NavQuirks != mode) {
+ aBuilder.SetKeywordValue(eCSSProperty_text_wrap_mode,
+ StyleTextWrapMode::Nowrap);
+ }
+ }
+ }
+
+ nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapVAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLTableCellElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::align},
+ {nsGkAtoms::valign},
+ {nsGkAtoms::nowrap},
+#if 0
+ // XXXldb If these are implemented, they might need to move to
+ // GetAttributeChangeHint (depending on how, and preferably not).
+ { nsGkAtoms::abbr },
+ { nsGkAtoms::axis },
+ { nsGkAtoms::headers },
+ { nsGkAtoms::scope },
+#endif
+ {nsGkAtoms::width},
+ {nsGkAtoms::height},
+ {nullptr}
+ };
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ sBackgroundAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLTableCellElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTableCellElement.h b/dom/html/HTMLTableCellElement.h
new file mode 100644
index 0000000000..cdf61f8928
--- /dev/null
+++ b/dom/html/HTMLTableCellElement.h
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLTableCellElement_h
+#define mozilla_dom_HTMLTableCellElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLTableElement;
+
+class HTMLTableCellElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLTableCellElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetHasWeirdParserInsertionMode();
+ }
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLTableCellElement,
+ nsGenericHTMLElement)
+
+ NS_IMPL_FROMNODE_HELPER(HTMLTableCellElement,
+ IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th))
+
+ uint32_t ColSpan() const { return GetUnsignedIntAttr(nsGkAtoms::colspan, 1); }
+ void SetColSpan(uint32_t aColSpan, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::colspan, aColSpan, 1, aError);
+ }
+ uint32_t RowSpan() const { return GetUnsignedIntAttr(nsGkAtoms::rowspan, 1); }
+ void SetRowSpan(uint32_t aRowSpan, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::rowspan, aRowSpan, 1, aError);
+ }
+ // already_AddRefed<nsDOMTokenList> Headers() const;
+ void GetHeaders(DOMString& aHeaders) {
+ GetHTMLAttr(nsGkAtoms::headers, aHeaders);
+ }
+ void SetHeaders(const nsAString& aHeaders, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::headers, aHeaders, aError);
+ }
+ int32_t CellIndex() const;
+
+ void GetAbbr(DOMString& aAbbr) { GetHTMLAttr(nsGkAtoms::abbr, aAbbr); }
+ void SetAbbr(const nsAString& aAbbr, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::abbr, aAbbr, aError);
+ }
+ void GetScope(DOMString& aScope);
+ void SetScope(const nsAString& aScope, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::scope, aScope, aError);
+ }
+ void GetAlign(DOMString& aAlign);
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+ void GetAxis(DOMString& aAxis) { GetHTMLAttr(nsGkAtoms::axis, aAxis); }
+ void SetAxis(const nsAString& aAxis, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::axis, aAxis, aError);
+ }
+ void GetHeight(DOMString& aHeight) {
+ GetHTMLAttr(nsGkAtoms::height, aHeight);
+ }
+ void SetHeight(const nsAString& aHeight, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::height, aHeight, aError);
+ }
+ void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); }
+ void SetWidth(const nsAString& aWidth, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::width, aWidth, aError);
+ }
+ void GetCh(DOMString& aCh) { GetHTMLAttr(nsGkAtoms::_char, aCh); }
+ void SetCh(const nsAString& aCh, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::_char, aCh, aError);
+ }
+ void GetChOff(DOMString& aChOff) { GetHTMLAttr(nsGkAtoms::charoff, aChOff); }
+ void SetChOff(const nsAString& aChOff, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::charoff, aChOff, aError);
+ }
+ bool NoWrap() { return GetBoolAttr(nsGkAtoms::nowrap); }
+ void SetNoWrap(bool aNoWrap, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::nowrap, aNoWrap, aError);
+ }
+ void GetVAlign(DOMString& aVAlign) {
+ GetHTMLAttr(nsGkAtoms::valign, aVAlign);
+ }
+ void SetVAlign(const nsAString& aVAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::valign, aVAlign, aError);
+ }
+ void GetBgColor(DOMString& aBgColor) {
+ GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor);
+ }
+ void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError);
+ }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ // Get mapped attributes of ancestor table, if any
+ StyleLockedDeclarationBlock* GetMappedAttributesInheritedFromTable() const;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLTableCellElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ HTMLTableElement* GetTable() const;
+
+ HTMLTableRowElement* GetRow() const;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLTableCellElement_h */
diff --git a/dom/html/HTMLTableColElement.cpp b/dom/html/HTMLTableColElement.cpp
new file mode 100644
index 0000000000..ae8e619a0f
--- /dev/null
+++ b/dom/html/HTMLTableColElement.cpp
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTableColElement.h"
+#include "mozilla/dom/HTMLTableColElementBinding.h"
+#include "nsAttrValueInlines.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(TableCol)
+
+namespace mozilla::dom {
+
+// use the same protection as ancient code did
+// http://lxr.mozilla.org/classic/source/lib/layout/laytable.c#46
+#define MAX_COLSPAN 1000
+
+HTMLTableColElement::~HTMLTableColElement() = default;
+
+JSObject* HTMLTableColElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTableColElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLTableColElement)
+
+bool HTMLTableColElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ /* ignore these attributes, stored simply as strings ch */
+ if (aAttribute == nsGkAtoms::span) {
+ /* protection from unrealistic large colspan values */
+ aResult.ParseClampedNonNegativeInt(aValue, 1, 1, MAX_COLSPAN);
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::width) {
+ // Spec says to use ParseNonzeroHTMLDimension, but Chrome and Safari both
+ // allow 0, and we did all along too, so keep that behavior. See
+ // https://github.com/whatwg/html/issues/4717
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseTableCellHAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::valign) {
+ return ParseTableVAlignValue(aValue, aResult);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLTableColElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty__x_span)) {
+ // span: int
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::span);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ int32_t val = value->GetIntegerValue();
+ // Note: Do NOT use this code for table cells! The value "0"
+ // means something special for colspan and rowspan, but for <col
+ // span> and <colgroup span> it's just disallowed.
+ if (val > 0) {
+ aBuilder.SetIntValue(eCSSProperty__x_span, value->GetIntegerValue());
+ }
+ }
+ }
+
+ nsGenericHTMLElement::MapWidthAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapVAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLTableColElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {{nsGkAtoms::width},
+ {nsGkAtoms::align},
+ {nsGkAtoms::valign},
+ {nsGkAtoms::span},
+ {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLTableColElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTableColElement.h b/dom/html/HTMLTableColElement.h
new file mode 100644
index 0000000000..c57ce1f2a9
--- /dev/null
+++ b/dom/html/HTMLTableColElement.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLTableColElement_h
+#define mozilla_dom_HTMLTableColElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+class HTMLTableColElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLTableColElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetHasWeirdParserInsertionMode();
+ }
+
+ uint32_t Span() const { return GetUnsignedIntAttr(nsGkAtoms::span, 1); }
+ void SetSpan(uint32_t aSpan, ErrorResult& aError) {
+ SetUnsignedIntAttr(nsGkAtoms::span, aSpan, 1, aError);
+ }
+
+ void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); }
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+ void GetCh(DOMString& aCh) { GetHTMLAttr(nsGkAtoms::_char, aCh); }
+ void SetCh(const nsAString& aCh, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::_char, aCh, aError);
+ }
+ void GetChOff(DOMString& aChOff) { GetHTMLAttr(nsGkAtoms::charoff, aChOff); }
+ void SetChOff(const nsAString& aChOff, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::charoff, aChOff, aError);
+ }
+ void GetVAlign(DOMString& aVAlign) {
+ GetHTMLAttr(nsGkAtoms::valign, aVAlign);
+ }
+ void SetVAlign(const nsAString& aVAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::valign, aVAlign, aError);
+ }
+ void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); }
+ void SetWidth(const nsAString& aWidth, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::width, aWidth, aError);
+ }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLTableColElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLTableColElement_h */
diff --git a/dom/html/HTMLTableElement.cpp b/dom/html/HTMLTableElement.cpp
new file mode 100644
index 0000000000..97329dcef1
--- /dev/null
+++ b/dom/html/HTMLTableElement.cpp
@@ -0,0 +1,995 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTableElement.h"
+#include "mozilla/AttributeStyles.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/DeclarationBlock.h"
+#include "nsAttrValueInlines.h"
+#include "nsWrapperCacheInlines.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLCollectionBinding.h"
+#include "mozilla/dom/HTMLTableElementBinding.h"
+#include "nsContentUtils.h"
+#include "nsLayoutUtils.h"
+#include "jsfriendapi.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Table)
+
+namespace mozilla::dom {
+
+/* ------------------------- TableRowsCollection --------------------------- */
+/**
+ * This class provides a late-bound collection of rows in a table.
+ * mParent is NOT ref-counted to avoid circular references
+ */
+class TableRowsCollection final : public nsIHTMLCollection,
+ public nsStubMutationObserver,
+ public nsWrapperCache {
+ public:
+ explicit TableRowsCollection(HTMLTableElement* aParent);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+ NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
+
+ uint32_t Length() override;
+ Element* GetElementAt(uint32_t aIndex) override;
+ nsINode* GetParentObject() override { return mParent; }
+
+ Element* GetFirstNamedElement(const nsAString& aName, bool& aFound) override;
+ void GetSupportedNames(nsTArray<nsString>& aNames) override;
+
+ NS_IMETHOD ParentDestroyed();
+
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(TableRowsCollection,
+ nsIHTMLCollection)
+
+ // nsWrapperCache
+ using nsWrapperCache::GetWrapperPreserveColor;
+ using nsWrapperCache::PreserveWrapper;
+ JSObject* WrapObject(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ protected:
+ // Unregister ourselves as a mutation observer, and clear our internal state.
+ void CleanUp();
+ void LastRelease() { CleanUp(); }
+ virtual ~TableRowsCollection() {
+ // we do NOT have a ref-counted reference to mParent, so do NOT
+ // release it! this is to avoid circular references. The
+ // instantiator who provided mParent is responsible for managing our
+ // reference for us.
+ CleanUp();
+ }
+
+ JSObject* GetWrapperPreserveColorInternal() override {
+ return nsWrapperCache::GetWrapperPreserveColor();
+ }
+ void PreserveWrapperInternal(nsISupports* aScriptObjectHolder) override {
+ nsWrapperCache::PreserveWrapper(aScriptObjectHolder);
+ }
+
+ // Ensure that HTMLTableElement is in a valid state. This must be called
+ // before inspecting the mRows object.
+ void EnsureInitialized();
+
+ // Checks if the passed-in container is interesting for the purposes of
+ // invalidation due to a mutation observer.
+ bool InterestingContainer(nsIContent* aContainer);
+
+ // Check if the passed-in nsIContent is a <tr> within the section defined by
+ // `aSection`. The root of the table is considered to be part of the `<tbody>`
+ // section.
+ bool IsAppropriateRow(nsAtom* aSection, nsIContent* aContent);
+
+ // Scan backwards starting from `aCurrent` in the table, looking for the
+ // previous row in the table which is within the section `aSection`.
+ nsIContent* PreviousRow(nsAtom* aSection, nsIContent* aCurrent);
+
+ // Handle the insertion of the child `aChild` into the container `aContainer`
+ // within the tree. The container must be an `InterestingContainer`. This
+ // method updates the mRows, mBodyStart, and mFootStart member variables.
+ //
+ // HandleInsert returns an integer which can be passed to the next call of the
+ // method in a loop inserting children into the same container. This will
+ // optimize subsequent insertions to require less work. This can either be -1,
+ // in which case we don't know where to insert the next row, and When passed
+ // to HandleInsert, it will use `PreviousRow` to locate the index to insert.
+ // Or, it can be an index to insert the next <tr> in the same container at.
+ int32_t HandleInsert(nsIContent* aContainer, nsIContent* aChild,
+ int32_t aIndexGuess = -1);
+
+ // The HTMLTableElement which this TableRowsCollection tracks the rows for.
+ HTMLTableElement* mParent;
+
+ // The current state of the TableRowsCollection. mBodyStart and mFootStart are
+ // indices into mRows which represent the location of the first row in the
+ // body or foot section. If there are no rows in a section, the index points
+ // at the location where the first element in that section would be inserted.
+ nsTArray<nsCOMPtr<nsIContent>> mRows;
+ uint32_t mBodyStart;
+ uint32_t mFootStart;
+ bool mInitialized;
+};
+
+TableRowsCollection::TableRowsCollection(HTMLTableElement* aParent)
+ : mParent(aParent), mBodyStart(0), mFootStart(0), mInitialized(false) {
+ MOZ_ASSERT(mParent);
+}
+
+void TableRowsCollection::EnsureInitialized() {
+ if (mInitialized) {
+ return;
+ }
+ mInitialized = true;
+
+ // Initialize mRows as the TableRowsCollection is created. The mutation
+ // observer should keep it up to date.
+ //
+ // It should be extremely unlikely that anyone creates a TableRowsCollection
+ // without calling a method on it, so lazily performing this initialization
+ // seems unnecessary.
+ AutoTArray<nsCOMPtr<nsIContent>, 32> body;
+ AutoTArray<nsCOMPtr<nsIContent>, 32> foot;
+ mRows.Clear();
+
+ auto addRowChildren = [&](nsTArray<nsCOMPtr<nsIContent>>& aArray,
+ nsIContent* aNode) {
+ for (nsIContent* inner = aNode->nsINode::GetFirstChild(); inner;
+ inner = inner->GetNextSibling()) {
+ if (inner->IsHTMLElement(nsGkAtoms::tr)) {
+ aArray.AppendElement(inner);
+ }
+ }
+ };
+
+ for (nsIContent* node = mParent->nsINode::GetFirstChild(); node;
+ node = node->GetNextSibling()) {
+ if (node->IsHTMLElement(nsGkAtoms::thead)) {
+ addRowChildren(mRows, node);
+ } else if (node->IsHTMLElement(nsGkAtoms::tbody)) {
+ addRowChildren(body, node);
+ } else if (node->IsHTMLElement(nsGkAtoms::tfoot)) {
+ addRowChildren(foot, node);
+ } else if (node->IsHTMLElement(nsGkAtoms::tr)) {
+ body.AppendElement(node);
+ }
+ }
+
+ mBodyStart = mRows.Length();
+ mRows.AppendElements(std::move(body));
+ mFootStart = mRows.Length();
+ mRows.AppendElements(std::move(foot));
+
+ mParent->AddMutationObserver(this);
+}
+
+void TableRowsCollection::CleanUp() {
+ // Unregister ourselves as a mutation observer.
+ if (mInitialized && mParent) {
+ mParent->RemoveMutationObserver(this);
+ }
+
+ // Clean up all of our internal state and make it empty in case someone looks
+ // at us.
+ mRows.Clear();
+ mBodyStart = 0;
+ mFootStart = 0;
+
+ // We set mInitialized to true in case someone still has a reference to us, as
+ // we don't need to try to initialize first.
+ mInitialized = true;
+ mParent = nullptr;
+}
+
+JSObject* TableRowsCollection::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLCollection_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TableRowsCollection, mRows)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TableRowsCollection)
+NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(TableRowsCollection,
+ LastRelease())
+
+NS_INTERFACE_TABLE_HEAD(TableRowsCollection)
+ NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY
+ NS_INTERFACE_TABLE(TableRowsCollection, nsIHTMLCollection,
+ nsIMutationObserver)
+ NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(TableRowsCollection)
+NS_INTERFACE_MAP_END
+
+uint32_t TableRowsCollection::Length() {
+ EnsureInitialized();
+ return mRows.Length();
+}
+
+Element* TableRowsCollection::GetElementAt(uint32_t aIndex) {
+ EnsureInitialized();
+ if (aIndex < mRows.Length()) {
+ return mRows[aIndex]->AsElement();
+ }
+ return nullptr;
+}
+
+Element* TableRowsCollection::GetFirstNamedElement(const nsAString& aName,
+ bool& aFound) {
+ EnsureInitialized();
+ aFound = false;
+ RefPtr<nsAtom> nameAtom = NS_Atomize(aName);
+ NS_ENSURE_TRUE(nameAtom, nullptr);
+
+ for (auto& node : mRows) {
+ if (node->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
+ nameAtom, eCaseMatters) ||
+ node->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id,
+ nameAtom, eCaseMatters)) {
+ aFound = true;
+ return node->AsElement();
+ }
+ }
+
+ return nullptr;
+}
+
+void TableRowsCollection::GetSupportedNames(nsTArray<nsString>& aNames) {
+ EnsureInitialized();
+ for (auto& node : mRows) {
+ if (node->HasID()) {
+ nsAtom* idAtom = node->GetID();
+ MOZ_ASSERT(idAtom != nsGkAtoms::_empty, "Empty ids don't get atomized");
+ nsDependentAtomString idStr(idAtom);
+ if (!aNames.Contains(idStr)) {
+ aNames.AppendElement(idStr);
+ }
+ }
+
+ nsGenericHTMLElement* el = nsGenericHTMLElement::FromNode(node);
+ if (el) {
+ const nsAttrValue* val = el->GetParsedAttr(nsGkAtoms::name);
+ if (val && val->Type() == nsAttrValue::eAtom) {
+ nsAtom* nameAtom = val->GetAtomValue();
+ MOZ_ASSERT(nameAtom != nsGkAtoms::_empty,
+ "Empty names don't get atomized");
+ nsDependentAtomString nameStr(nameAtom);
+ if (!aNames.Contains(nameStr)) {
+ aNames.AppendElement(nameStr);
+ }
+ }
+ }
+ }
+}
+
+NS_IMETHODIMP
+TableRowsCollection::ParentDestroyed() {
+ CleanUp();
+ return NS_OK;
+}
+
+bool TableRowsCollection::InterestingContainer(nsIContent* aContainer) {
+ return mParent && aContainer &&
+ (aContainer == mParent ||
+ (aContainer->GetParent() == mParent &&
+ aContainer->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tbody,
+ nsGkAtoms::tfoot)));
+}
+
+bool TableRowsCollection::IsAppropriateRow(nsAtom* aSection,
+ nsIContent* aContent) {
+ if (!aContent->IsHTMLElement(nsGkAtoms::tr)) {
+ return false;
+ }
+ // If it's in the root, then we consider it to be in a tbody.
+ nsIContent* parent = aContent->GetParent();
+ if (aSection == nsGkAtoms::tbody && parent == mParent) {
+ return true;
+ }
+ return parent->IsHTMLElement(aSection);
+}
+
+nsIContent* TableRowsCollection::PreviousRow(nsAtom* aSection,
+ nsIContent* aCurrent) {
+ // Keep going backwards until we've found a `tr` element. We want to always
+ // run at least once, as we don't want to find ourselves.
+ //
+ // Each spin of the loop we step backwards one element. If we're at the top of
+ // a section, we step out of it into the root, and if we step onto a section
+ // matching `aSection`, we step into it. We keep spinning the loop until
+ // either we reach the first element in mParent, or find a <tr> in an
+ // appropriate section.
+ nsIContent* prev = aCurrent;
+ do {
+ nsIContent* parent = prev->GetParent();
+ prev = prev->GetPreviousSibling();
+
+ // Ascend out of any sections we're currently in, if we've run out of
+ // elements.
+ if (!prev && parent != mParent) {
+ prev = parent->GetPreviousSibling();
+ }
+
+ // Descend into a section if we stepped onto one.
+ if (prev && prev->GetParent() == mParent && prev->IsHTMLElement(aSection)) {
+ prev = prev->GetLastChild();
+ }
+ } while (prev && !IsAppropriateRow(aSection, prev));
+ return prev;
+}
+
+int32_t TableRowsCollection::HandleInsert(nsIContent* aContainer,
+ nsIContent* aChild,
+ int32_t aIndexGuess) {
+ if (!nsContentUtils::IsInSameAnonymousTree(mParent, aChild)) {
+ return aIndexGuess; // Nothing inserted, guess hasn't changed.
+ }
+
+ // If we're adding a section to the root, add each of the rows in that section
+ // individually.
+ if (aContainer == mParent &&
+ aChild->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tbody,
+ nsGkAtoms::tfoot)) {
+ // If we're entering a tbody, we can persist the index guess we were passed,
+ // as the newly added items are in the same section as us, however, if we're
+ // entering thead or tfoot we will have to re-scan.
+ bool isTBody = aChild->IsHTMLElement(nsGkAtoms::tbody);
+ int32_t indexGuess = isTBody ? aIndexGuess : -1;
+
+ for (nsIContent* inner = aChild->GetFirstChild(); inner;
+ inner = inner->GetNextSibling()) {
+ indexGuess = HandleInsert(aChild, inner, indexGuess);
+ }
+
+ return isTBody ? indexGuess : -1;
+ }
+ if (!aChild->IsHTMLElement(nsGkAtoms::tr)) {
+ return aIndexGuess; // Nothing inserted, guess hasn't changed.
+ }
+
+ // We should have only been passed an insertion from an interesting container,
+ // so we can get the container we're inserting to fairly easily.
+ nsAtom* section = aContainer == mParent ? nsGkAtoms::tbody
+ : aContainer->NodeInfo()->NameAtom();
+
+ // Determine the default index we would to insert after if we don't find any
+ // previous row, and offset our section boundaries based on the section we're
+ // planning to insert into.
+ size_t index = 0;
+ if (section == nsGkAtoms::thead) {
+ mBodyStart++;
+ mFootStart++;
+ } else if (section == nsGkAtoms::tbody) {
+ index = mBodyStart;
+ mFootStart++;
+ } else if (section == nsGkAtoms::tfoot) {
+ index = mFootStart;
+ } else {
+ MOZ_ASSERT(false, "section should be one of thead, tbody, or tfoot");
+ }
+
+ // If we already have an index guess, we can skip scanning for the previous
+ // row.
+ if (aIndexGuess >= 0) {
+ index = aIndexGuess;
+ } else {
+ // Find the previous row in the section we're inserting into. If we find it,
+ // we can use it to override our insertion index. We don't need to modify
+ // mBodyStart or mFootStart anymore, as they have already been correctly
+ // updated based only on section.
+ nsIContent* insertAfter = PreviousRow(section, aChild);
+ if (insertAfter) {
+ // NOTE: We want to ensure that appending elements is quick, so we search
+ // from the end rather than from the beginning.
+ index = mRows.LastIndexOf(insertAfter) + 1;
+ MOZ_ASSERT(index != nsTArray<nsCOMPtr<nsIContent>>::NoIndex);
+ }
+ }
+
+#ifdef DEBUG
+ // Assert that we're inserting into the correct section.
+ if (section == nsGkAtoms::thead) {
+ MOZ_ASSERT(index < mBodyStart);
+ } else if (section == nsGkAtoms::tbody) {
+ MOZ_ASSERT(index >= mBodyStart);
+ MOZ_ASSERT(index < mFootStart);
+ } else if (section == nsGkAtoms::tfoot) {
+ MOZ_ASSERT(index >= mFootStart);
+ MOZ_ASSERT(index <= mRows.Length());
+ }
+
+ MOZ_ASSERT(mBodyStart <= mFootStart);
+ MOZ_ASSERT(mFootStart <= mRows.Length() + 1);
+#endif
+
+ mRows.InsertElementAt(index, aChild);
+ return index + 1;
+}
+
+// nsIMutationObserver
+
+void TableRowsCollection::ContentAppended(nsIContent* aFirstNewContent) {
+ nsIContent* container = aFirstNewContent->GetParent();
+ if (!nsContentUtils::IsInSameAnonymousTree(mParent, aFirstNewContent) ||
+ !InterestingContainer(container)) {
+ return;
+ }
+
+ // We usually can't guess where we need to start inserting, unless we're
+ // appending into mParent, in which case we can provide the guess that we
+ // should insert at the end of the body, which can help us avoid potentially
+ // expensive work in the common case.
+ int32_t indexGuess = mParent == container ? mFootStart : -1;
+
+ // Insert each of the newly added content one at a time. The indexGuess should
+ // make insertions of a large number of elements cheaper.
+ for (nsIContent* content = aFirstNewContent; content;
+ content = content->GetNextSibling()) {
+ indexGuess = HandleInsert(container, content, indexGuess);
+ }
+}
+
+void TableRowsCollection::ContentInserted(nsIContent* aChild) {
+ if (!nsContentUtils::IsInSameAnonymousTree(mParent, aChild) ||
+ !InterestingContainer(aChild->GetParent())) {
+ return;
+ }
+
+ HandleInsert(aChild->GetParent(), aChild);
+}
+
+void TableRowsCollection::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ if (!nsContentUtils::IsInSameAnonymousTree(mParent, aChild) ||
+ !InterestingContainer(aChild->GetParent())) {
+ return;
+ }
+
+ // If the element being removed is a `tr`, we can just remove it from our
+ // list. It shouldn't change the order of anything.
+ if (aChild->IsHTMLElement(nsGkAtoms::tr)) {
+ size_t index = mRows.IndexOf(aChild);
+ if (index != nsTArray<nsCOMPtr<nsIContent>>::NoIndex) {
+ mRows.RemoveElementAt(index);
+ if (mBodyStart > index) {
+ mBodyStart--;
+ }
+ if (mFootStart > index) {
+ mFootStart--;
+ }
+ }
+ return;
+ }
+
+ // If the element being removed is a `thead`, `tbody`, or `tfoot`, we can
+ // remove any `tr`s in our list which have that element as its parent node. In
+ // any other situation, the removal won't affect us, so we can ignore it.
+ if (!aChild->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tbody,
+ nsGkAtoms::tfoot)) {
+ return;
+ }
+
+ size_t beforeLength = mRows.Length();
+ mRows.RemoveElementsBy(
+ [&](nsIContent* element) { return element->GetParent() == aChild; });
+ size_t removed = beforeLength - mRows.Length();
+ if (aChild->IsHTMLElement(nsGkAtoms::thead)) {
+ // NOTE: Need to move both tbody and tfoot, as we removed from head.
+ mBodyStart -= removed;
+ mFootStart -= removed;
+ } else if (aChild->IsHTMLElement(nsGkAtoms::tbody)) {
+ // NOTE: Need to move tfoot, as we removed from body.
+ mFootStart -= removed;
+ }
+}
+
+void TableRowsCollection::NodeWillBeDestroyed(nsINode* aNode) {
+ // Set mInitialized to false so CleanUp doesn't try to remove our mutation
+ // observer, as we're going away. CleanUp() will reset mInitialized to true as
+ // it returns.
+ mInitialized = false;
+ CleanUp();
+}
+
+/* --------------------------- HTMLTableElement ---------------------------- */
+
+HTMLTableElement::HTMLTableElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetHasWeirdParserInsertionMode();
+}
+
+HTMLTableElement::~HTMLTableElement() {
+ if (mRows) {
+ mRows->ParentDestroyed();
+ }
+ ReleaseInheritedAttributes();
+}
+
+JSObject* HTMLTableElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTableElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLTableElement)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLTableElement,
+ nsGenericHTMLElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTBodies)
+ if (tmp->mRows) {
+ tmp->mRows->ParentDestroyed();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mRows)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLTableElement,
+ nsGenericHTMLElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTBodies)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRows)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTableElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLTableElement)
+
+// the DOM spec says border, cellpadding, cellSpacing are all "wstring"
+// in fact, they are integers or they are meaningless. so we store them
+// here as ints.
+
+nsIHTMLCollection* HTMLTableElement::Rows() {
+ if (!mRows) {
+ mRows = new TableRowsCollection(this);
+ }
+
+ return mRows;
+}
+
+nsIHTMLCollection* HTMLTableElement::TBodies() {
+ if (!mTBodies) {
+ // Not using NS_GetContentList because this should not be cached
+ mTBodies = new nsContentList(this, kNameSpaceID_XHTML, nsGkAtoms::tbody,
+ nsGkAtoms::tbody, false);
+ }
+
+ return mTBodies;
+}
+
+already_AddRefed<nsGenericHTMLElement> HTMLTableElement::CreateTHead() {
+ RefPtr<nsGenericHTMLElement> head = GetTHead();
+ if (!head) {
+ // Create a new head rowgroup.
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::thead,
+ getter_AddRefs(nodeInfo));
+
+ head = NS_NewHTMLTableSectionElement(nodeInfo.forget());
+ if (!head) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIContent> refNode = nullptr;
+ for (refNode = nsINode::GetFirstChild(); refNode;
+ refNode = refNode->GetNextSibling()) {
+ if (refNode->IsHTMLElement() &&
+ !refNode->IsHTMLElement(nsGkAtoms::caption) &&
+ !refNode->IsHTMLElement(nsGkAtoms::colgroup)) {
+ break;
+ }
+ }
+
+ nsINode::InsertBefore(*head, refNode, IgnoreErrors());
+ }
+ return head.forget();
+}
+
+void HTMLTableElement::DeleteTHead() {
+ RefPtr<HTMLTableSectionElement> tHead = GetTHead();
+ if (tHead) {
+ mozilla::IgnoredErrorResult rv;
+ nsINode::RemoveChild(*tHead, rv);
+ }
+}
+
+already_AddRefed<nsGenericHTMLElement> HTMLTableElement::CreateTFoot() {
+ RefPtr<nsGenericHTMLElement> foot = GetTFoot();
+ if (!foot) {
+ // create a new foot rowgroup
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tfoot,
+ getter_AddRefs(nodeInfo));
+
+ foot = NS_NewHTMLTableSectionElement(nodeInfo.forget());
+ if (!foot) {
+ return nullptr;
+ }
+ AppendChildTo(foot, true, IgnoreErrors());
+ }
+
+ return foot.forget();
+}
+
+void HTMLTableElement::DeleteTFoot() {
+ RefPtr<HTMLTableSectionElement> tFoot = GetTFoot();
+ if (tFoot) {
+ mozilla::IgnoredErrorResult rv;
+ nsINode::RemoveChild(*tFoot, rv);
+ }
+}
+
+already_AddRefed<nsGenericHTMLElement> HTMLTableElement::CreateCaption() {
+ RefPtr<nsGenericHTMLElement> caption = GetCaption();
+ if (!caption) {
+ // Create a new caption.
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::caption,
+ getter_AddRefs(nodeInfo));
+
+ caption = NS_NewHTMLTableCaptionElement(nodeInfo.forget());
+ if (!caption) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsINode> firsChild = nsINode::GetFirstChild();
+ nsINode::InsertBefore(*caption, firsChild, IgnoreErrors());
+ }
+ return caption.forget();
+}
+
+void HTMLTableElement::DeleteCaption() {
+ RefPtr<HTMLTableCaptionElement> caption = GetCaption();
+ if (caption) {
+ mozilla::IgnoredErrorResult rv;
+ nsINode::RemoveChild(*caption, rv);
+ }
+}
+
+already_AddRefed<nsGenericHTMLElement> HTMLTableElement::CreateTBody() {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo =
+ OwnerDoc()->NodeInfoManager()->GetNodeInfo(
+ nsGkAtoms::tbody, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE);
+ MOZ_ASSERT(nodeInfo);
+
+ RefPtr<nsGenericHTMLElement> newBody =
+ NS_NewHTMLTableSectionElement(nodeInfo.forget());
+ MOZ_ASSERT(newBody);
+
+ nsCOMPtr<nsIContent> referenceNode = nullptr;
+ for (nsIContent* child = nsINode::GetLastChild(); child;
+ child = child->GetPreviousSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::tbody)) {
+ referenceNode = child->GetNextSibling();
+ break;
+ }
+ }
+
+ nsINode::InsertBefore(*newBody, referenceNode, IgnoreErrors());
+
+ return newBody.forget();
+}
+
+already_AddRefed<nsGenericHTMLElement> HTMLTableElement::InsertRow(
+ int32_t aIndex, ErrorResult& aError) {
+ /* get the ref row at aIndex
+ if there is one,
+ get its parent
+ insert the new row just before the ref row
+ else
+ get the first row group
+ insert the new row as its first child
+ */
+ if (aIndex < -1) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return nullptr;
+ }
+
+ nsIHTMLCollection* rows = Rows();
+ uint32_t rowCount = rows->Length();
+ if ((uint32_t)aIndex > rowCount && aIndex != -1) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return nullptr;
+ }
+
+ // use local variable refIndex so we can remember original aIndex
+ uint32_t refIndex = (uint32_t)aIndex;
+
+ RefPtr<nsGenericHTMLElement> newRow;
+ if (rowCount > 0) {
+ if (refIndex == rowCount || aIndex == -1) {
+ // we set refIndex to the last row so we can get the last row's
+ // parent we then do an AppendChild below if (rowCount<aIndex)
+
+ refIndex = rowCount - 1;
+ }
+
+ RefPtr<Element> refRow = rows->Item(refIndex);
+ nsCOMPtr<nsINode> parent = refRow->GetParentNode();
+
+ // create the row
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tr,
+ getter_AddRefs(nodeInfo));
+
+ newRow = NS_NewHTMLTableRowElement(nodeInfo.forget());
+
+ if (newRow) {
+ // If aIndex is -1 or equal to the number of rows, the new row
+ // is appended.
+ if (aIndex == -1 || uint32_t(aIndex) == rowCount) {
+ parent->AppendChild(*newRow, aError);
+ } else {
+ // insert the new row before the reference row we found above
+ parent->InsertBefore(*newRow, refRow, aError);
+ }
+
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ }
+ } else {
+ // the row count was 0, so
+ // find the last row group and insert there as first child
+ nsCOMPtr<nsIContent> rowGroup;
+ for (nsIContent* child = nsINode::GetLastChild(); child;
+ child = child->GetPreviousSibling()) {
+ if (child->IsHTMLElement(nsGkAtoms::tbody)) {
+ rowGroup = child;
+ break;
+ }
+ }
+
+ if (!rowGroup) { // need to create a TBODY
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tbody,
+ getter_AddRefs(nodeInfo));
+
+ rowGroup = NS_NewHTMLTableSectionElement(nodeInfo.forget());
+ if (rowGroup) {
+ AppendChildTo(rowGroup, true, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ }
+ }
+
+ if (rowGroup) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tr,
+ getter_AddRefs(nodeInfo));
+
+ newRow = NS_NewHTMLTableRowElement(nodeInfo.forget());
+ if (newRow) {
+ HTMLTableSectionElement* section =
+ static_cast<HTMLTableSectionElement*>(rowGroup.get());
+ nsIHTMLCollection* rows = section->Rows();
+ nsCOMPtr<nsINode> refNode = rows->Item(0);
+ rowGroup->InsertBefore(*newRow, refNode, aError);
+ }
+ }
+ }
+
+ return newRow.forget();
+}
+
+void HTMLTableElement::DeleteRow(int32_t aIndex, ErrorResult& aError) {
+ if (aIndex < -1) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ nsIHTMLCollection* rows = Rows();
+ uint32_t refIndex;
+ if (aIndex == -1) {
+ refIndex = rows->Length();
+ if (refIndex == 0) {
+ return;
+ }
+
+ --refIndex;
+ } else {
+ refIndex = (uint32_t)aIndex;
+ }
+
+ nsCOMPtr<nsIContent> row = rows->Item(refIndex);
+ if (!row) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ row->RemoveFromParent();
+}
+
+bool HTMLTableElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ /* ignore summary, just a string */
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::cellspacing ||
+ aAttribute == nsGkAtoms::cellpadding ||
+ aAttribute == nsGkAtoms::border) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ }
+ if (aAttribute == nsGkAtoms::height) {
+ // Purposeful spec violation (spec says to use ParseNonzeroHTMLDimension)
+ // to stay compatible with our old behavior and other browsers. See
+ // https://github.com/whatwg/html/issues/4715
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::width) {
+ return aResult.ParseNonzeroHTMLDimension(aValue);
+ }
+
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseTableHAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::bgcolor ||
+ aAttribute == nsGkAtoms::bordercolor) {
+ return aResult.ParseColor(aValue);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseBackgroundAttribute(
+ aNamespaceID, aAttribute, aValue, aResult) ||
+ nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLTableElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ // XXX Bug 211636: This function is used by a single style rule
+ // that's used to match two different type of elements -- tables, and
+ // table cells. (nsHTMLTableCellElement overrides
+ // WalkContentStyleRules so that this happens.) This violates the
+ // nsIStyleRule contract, since it's the same style rule object doing
+ // the mapping in two different ways. It's also incorrect since it's
+ // testing the display type of the ComputedStyle rather than checking
+ // which *element* it's matching (style rules should not stop matching
+ // when the display type is changed).
+
+ // cellspacing
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::cellspacing);
+ if (value && value->Type() == nsAttrValue::eInteger &&
+ !aBuilder.PropertyIsSet(eCSSProperty_border_spacing)) {
+ aBuilder.SetPixelValue(eCSSProperty_border_spacing,
+ float(value->GetIntegerValue()));
+ }
+ // align; Check for enumerated type (it may be another type if
+ // illegal)
+ value = aBuilder.GetAttr(nsGkAtoms::align);
+ if (value && value->Type() == nsAttrValue::eEnum) {
+ if (value->GetEnumValue() == uint8_t(StyleTextAlign::Center) ||
+ value->GetEnumValue() == uint8_t(StyleTextAlign::MozCenter)) {
+ aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_left);
+ aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_right);
+ }
+ }
+
+ // bordercolor
+ value = aBuilder.GetAttr(nsGkAtoms::bordercolor);
+ nscolor color;
+ if (value && value->GetColorValue(color)) {
+ aBuilder.SetColorValueIfUnset(eCSSProperty_border_top_color, color);
+ aBuilder.SetColorValueIfUnset(eCSSProperty_border_left_color, color);
+ aBuilder.SetColorValueIfUnset(eCSSProperty_border_bottom_color, color);
+ aBuilder.SetColorValueIfUnset(eCSSProperty_border_right_color, color);
+ }
+
+ // border
+ if (const nsAttrValue* borderValue = aBuilder.GetAttr(nsGkAtoms::border)) {
+ // border = 1 pixel default
+ int32_t borderThickness = 1;
+ if (borderValue->Type() == nsAttrValue::eInteger) {
+ borderThickness = borderValue->GetIntegerValue();
+ }
+
+ // by default, set all border sides to the specified width
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_top_width,
+ (float)borderThickness);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_left_width,
+ (float)borderThickness);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_bottom_width,
+ (float)borderThickness);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_right_width,
+ (float)borderThickness);
+ }
+
+ nsGenericHTMLElement::MapImageSizeAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLTableElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::cellpadding}, {nsGkAtoms::cellspacing},
+ {nsGkAtoms::border}, {nsGkAtoms::width},
+ {nsGkAtoms::height},
+
+ {nsGkAtoms::bordercolor},
+
+ {nsGkAtoms::align}, {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ sBackgroundAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLTableElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+void HTMLTableElement::BuildInheritedAttributes() {
+ MOZ_ASSERT(!mTableInheritedAttributes, "potential leak, plus waste of work");
+ MOZ_ASSERT(NS_IsMainThread());
+ Document* document = GetComposedDoc();
+ if (!document) {
+ return;
+ }
+ const nsAttrValue* value = GetParsedAttr(nsGkAtoms::cellpadding);
+ if (!value || value->Type() != nsAttrValue::eInteger) {
+ return;
+ }
+ // We have cellpadding. This will override our padding values if we don't
+ // have any set.
+ float pad = float(value->GetIntegerValue());
+ MappedDeclarationsBuilder builder(*this, *document);
+ builder.SetPixelValue(eCSSProperty_padding_top, pad);
+ builder.SetPixelValue(eCSSProperty_padding_right, pad);
+ builder.SetPixelValue(eCSSProperty_padding_bottom, pad);
+ builder.SetPixelValue(eCSSProperty_padding_left, pad);
+ mTableInheritedAttributes = builder.TakeDeclarationBlock();
+}
+
+void HTMLTableElement::ReleaseInheritedAttributes() {
+ mTableInheritedAttributes = nullptr;
+}
+
+nsresult HTMLTableElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ ReleaseInheritedAttributes();
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ BuildInheritedAttributes();
+ return NS_OK;
+}
+
+void HTMLTableElement::UnbindFromTree(bool aNullParent) {
+ ReleaseInheritedAttributes();
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+void HTMLTableElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) {
+ if (aName == nsGkAtoms::cellpadding && aNameSpaceID == kNameSpaceID_None) {
+ ReleaseInheritedAttributes();
+ }
+ return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue,
+ aNotify);
+}
+
+void HTMLTableElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aName == nsGkAtoms::cellpadding && aNameSpaceID == kNameSpaceID_None) {
+ BuildInheritedAttributes();
+ // This affects our cell styles.
+ // TODO(emilio): Maybe GetAttributeChangeHint should also allow you to
+ // specify a restyle hint and this could move there?
+ nsLayoutUtils::PostRestyleEvent(this, RestyleHint::RestyleSubtree(),
+ nsChangeHint(0));
+ }
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTableElement.h b/dom/html/HTMLTableElement.h
new file mode 100644
index 0000000000..38d3e24a83
--- /dev/null
+++ b/dom/html/HTMLTableElement.h
@@ -0,0 +1,205 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLTableElement_h
+#define mozilla_dom_HTMLTableElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "mozilla/dom/HTMLTableCaptionElement.h"
+#include "mozilla/dom/HTMLTableSectionElement.h"
+
+namespace mozilla::dom {
+
+class TableRowsCollection;
+
+class HTMLTableElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLTableElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTableElement, table)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ HTMLTableCaptionElement* GetCaption() const {
+ return static_cast<HTMLTableCaptionElement*>(GetChild(nsGkAtoms::caption));
+ }
+ void SetCaption(HTMLTableCaptionElement* aCaption, ErrorResult& aError) {
+ DeleteCaption();
+ if (aCaption) {
+ nsCOMPtr<nsINode> firstChild = nsINode::GetFirstChild();
+ nsINode::InsertBefore(*aCaption, firstChild, aError);
+ }
+ }
+
+ void DeleteTFoot();
+
+ already_AddRefed<nsGenericHTMLElement> CreateCaption();
+
+ void DeleteCaption();
+
+ HTMLTableSectionElement* GetTHead() const {
+ return static_cast<HTMLTableSectionElement*>(GetChild(nsGkAtoms::thead));
+ }
+ void SetTHead(HTMLTableSectionElement* aTHead, ErrorResult& aError) {
+ if (aTHead && !aTHead->IsHTMLElement(nsGkAtoms::thead)) {
+ aError.ThrowHierarchyRequestError("New value must be a thead element.");
+ return;
+ }
+
+ DeleteTHead();
+ if (aTHead) {
+ nsCOMPtr<nsIContent> refNode = nullptr;
+ for (refNode = nsINode::GetFirstChild(); refNode;
+ refNode = refNode->GetNextSibling()) {
+ if (refNode->IsHTMLElement() &&
+ !refNode->IsHTMLElement(nsGkAtoms::caption) &&
+ !refNode->IsHTMLElement(nsGkAtoms::colgroup)) {
+ break;
+ }
+ }
+
+ nsINode::InsertBefore(*aTHead, refNode, aError);
+ }
+ }
+ already_AddRefed<nsGenericHTMLElement> CreateTHead();
+
+ void DeleteTHead();
+
+ HTMLTableSectionElement* GetTFoot() const {
+ return static_cast<HTMLTableSectionElement*>(GetChild(nsGkAtoms::tfoot));
+ }
+ void SetTFoot(HTMLTableSectionElement* aTFoot, ErrorResult& aError) {
+ if (aTFoot && !aTFoot->IsHTMLElement(nsGkAtoms::tfoot)) {
+ aError.ThrowHierarchyRequestError("New value must be a tfoot element.");
+ return;
+ }
+
+ DeleteTFoot();
+ if (aTFoot) {
+ nsINode::AppendChild(*aTFoot, aError);
+ }
+ }
+ already_AddRefed<nsGenericHTMLElement> CreateTFoot();
+
+ nsIHTMLCollection* TBodies();
+
+ already_AddRefed<nsGenericHTMLElement> CreateTBody();
+
+ nsIHTMLCollection* Rows();
+
+ already_AddRefed<nsGenericHTMLElement> InsertRow(int32_t aIndex,
+ ErrorResult& aError);
+ void DeleteRow(int32_t aIndex, ErrorResult& aError);
+
+ void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); }
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+ void GetBorder(DOMString& aBorder) {
+ GetHTMLAttr(nsGkAtoms::border, aBorder);
+ }
+ void SetBorder(const nsAString& aBorder, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::border, aBorder, aError);
+ }
+ void GetFrame(DOMString& aFrame) { GetHTMLAttr(nsGkAtoms::frame, aFrame); }
+ void SetFrame(const nsAString& aFrame, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::frame, aFrame, aError);
+ }
+ void GetRules(DOMString& aRules) { GetHTMLAttr(nsGkAtoms::rules, aRules); }
+ void SetRules(const nsAString& aRules, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::rules, aRules, aError);
+ }
+ void GetSummary(nsString& aSummary) {
+ GetHTMLAttr(nsGkAtoms::summary, aSummary);
+ }
+ void GetSummary(DOMString& aSummary) {
+ GetHTMLAttr(nsGkAtoms::summary, aSummary);
+ }
+ void SetSummary(const nsAString& aSummary, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::summary, aSummary, aError);
+ }
+ void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); }
+ void SetWidth(const nsAString& aWidth, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::width, aWidth, aError);
+ }
+ void GetBgColor(DOMString& aBgColor) {
+ GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor);
+ }
+ void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError);
+ }
+ void GetCellPadding(DOMString& aCellPadding) {
+ GetHTMLAttr(nsGkAtoms::cellpadding, aCellPadding);
+ }
+ void SetCellPadding(const nsAString& aCellPadding, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::cellpadding, aCellPadding, aError);
+ }
+ void GetCellSpacing(DOMString& aCellSpacing) {
+ GetHTMLAttr(nsGkAtoms::cellspacing, aCellSpacing);
+ }
+ void SetCellSpacing(const nsAString& aCellSpacing, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::cellspacing, aCellSpacing, aError);
+ }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+ /**
+ * Called when an attribute is about to be changed
+ */
+ void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+ /**
+ * Called when an attribute has just been changed
+ */
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTableElement,
+ nsGenericHTMLElement)
+ StyleLockedDeclarationBlock* GetAttributesMappedForCell() const {
+ return mTableInheritedAttributes;
+ }
+
+ protected:
+ virtual ~HTMLTableElement();
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ nsIContent* GetChild(nsAtom* aTag) const {
+ for (nsIContent* cur = nsINode::GetFirstChild(); cur;
+ cur = cur->GetNextSibling()) {
+ if (cur->IsHTMLElement(aTag)) {
+ return cur;
+ }
+ }
+ return nullptr;
+ }
+
+ RefPtr<nsContentList> mTBodies;
+ RefPtr<TableRowsCollection> mRows;
+ RefPtr<StyleLockedDeclarationBlock> mTableInheritedAttributes;
+ void BuildInheritedAttributes();
+ void ReleaseInheritedAttributes();
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLTableElement_h */
diff --git a/dom/html/HTMLTableRowElement.cpp b/dom/html/HTMLTableRowElement.cpp
new file mode 100644
index 0000000000..af36dc5265
--- /dev/null
+++ b/dom/html/HTMLTableRowElement.cpp
@@ -0,0 +1,249 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTableRowElement.h"
+#include "mozilla/dom/HTMLTableElement.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsAttrValueInlines.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/HTMLTableRowElementBinding.h"
+#include "nsContentList.h"
+#include "nsContentUtils.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(TableRow)
+
+namespace mozilla::dom {
+
+HTMLTableRowElement::~HTMLTableRowElement() = default;
+
+JSObject* HTMLTableRowElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTableRowElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTableRowElement, nsGenericHTMLElement,
+ mCells)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTableRowElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLTableRowElement)
+
+// protected method
+HTMLTableSectionElement* HTMLTableRowElement::GetSection() const {
+ nsIContent* parent = GetParent();
+ if (parent && parent->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tbody,
+ nsGkAtoms::tfoot)) {
+ return static_cast<HTMLTableSectionElement*>(parent);
+ }
+ return nullptr;
+}
+
+// protected method
+HTMLTableElement* HTMLTableRowElement::GetTable() const {
+ nsIContent* parent = GetParent();
+ if (!parent) {
+ return nullptr;
+ }
+
+ // We may not be in a section
+ HTMLTableElement* table = HTMLTableElement::FromNode(parent);
+ if (table) {
+ return table;
+ }
+
+ return HTMLTableElement::FromNodeOrNull(parent->GetParent());
+}
+
+int32_t HTMLTableRowElement::RowIndex() const {
+ HTMLTableElement* table = GetTable();
+ if (!table) {
+ return -1;
+ }
+
+ nsIHTMLCollection* rows = table->Rows();
+
+ uint32_t numRows = rows->Length();
+
+ for (uint32_t i = 0; i < numRows; i++) {
+ if (rows->GetElementAt(i) == this) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+int32_t HTMLTableRowElement::SectionRowIndex() const {
+ HTMLTableSectionElement* section = GetSection();
+ if (!section) {
+ return -1;
+ }
+
+ nsCOMPtr<nsIHTMLCollection> coll = section->Rows();
+ uint32_t numRows = coll->Length();
+ for (uint32_t i = 0; i < numRows; i++) {
+ if (coll->GetElementAt(i) == this) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+static bool IsCell(Element* aElement, int32_t aNamespaceID, nsAtom* aAtom,
+ void* aData) {
+ return aElement->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
+}
+
+nsIHTMLCollection* HTMLTableRowElement::Cells() {
+ if (!mCells) {
+ mCells = new nsContentList(this, IsCell,
+ nullptr, // destroy func
+ nullptr, // closure data
+ false, nullptr, kNameSpaceID_XHTML, false);
+ }
+
+ return mCells;
+}
+
+already_AddRefed<nsGenericHTMLElement> HTMLTableRowElement::InsertCell(
+ int32_t aIndex, ErrorResult& aError) {
+ if (aIndex < -1) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return nullptr;
+ }
+
+ // Make sure mCells is initialized.
+ nsIHTMLCollection* cells = Cells();
+
+ NS_ASSERTION(mCells, "How did that happen?");
+
+ nsCOMPtr<nsINode> nextSibling;
+ // -1 means append, so should use null nextSibling
+ if (aIndex != -1) {
+ nextSibling = cells->Item(aIndex);
+ // Check whether we're inserting past end of list. We want to avoid doing
+ // this unless we really have to, since this has to walk all our kids. If
+ // we have a nextSibling, we're clearly not past end of list.
+ if (!nextSibling) {
+ uint32_t cellCount = cells->Length();
+ if (aIndex > int32_t(cellCount)) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return nullptr;
+ }
+ }
+ }
+
+ // create the cell
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::td,
+ getter_AddRefs(nodeInfo));
+
+ RefPtr<nsGenericHTMLElement> cell =
+ NS_NewHTMLTableCellElement(nodeInfo.forget());
+ if (!cell) {
+ aError.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ nsINode::InsertBefore(*cell, nextSibling, aError);
+
+ return cell.forget();
+}
+
+void HTMLTableRowElement::DeleteCell(int32_t aValue, ErrorResult& aError) {
+ if (aValue < -1) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ nsIHTMLCollection* cells = Cells();
+
+ uint32_t refIndex;
+ if (aValue == -1) {
+ refIndex = cells->Length();
+ if (refIndex == 0) {
+ return;
+ }
+
+ --refIndex;
+ } else {
+ refIndex = (uint32_t)aValue;
+ }
+
+ nsCOMPtr<nsINode> cell = cells->Item(refIndex);
+ if (!cell) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ nsINode::RemoveChild(*cell, aError);
+}
+
+bool HTMLTableRowElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ /*
+ * ignore these attributes, stored simply as strings
+ *
+ * ch
+ */
+
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::height) {
+ // Per spec should be ParseNonzeroHTMLDimension, but no browsers do that.
+ // See https://github.com/whatwg/html/issues/4716
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseTableCellHAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::bgcolor) {
+ return aResult.ParseColor(aValue);
+ }
+ if (aAttribute == nsGkAtoms::valign) {
+ return ParseTableVAlignValue(aValue, aResult);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseBackgroundAttribute(
+ aNamespaceID, aAttribute, aValue, aResult) ||
+ nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLTableRowElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ nsGenericHTMLElement::MapHeightAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapVAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLTableRowElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::align}, {nsGkAtoms::valign}, {nsGkAtoms::height}, {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ sBackgroundAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLTableRowElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTableRowElement.h b/dom/html/HTMLTableRowElement.h
new file mode 100644
index 0000000000..768aefe6d9
--- /dev/null
+++ b/dom/html/HTMLTableRowElement.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLTableRowElement_h
+#define mozilla_dom_HTMLTableRowElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+class nsContentList;
+
+namespace mozilla::dom {
+
+class HTMLTableSectionElement;
+
+class HTMLTableRowElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLTableRowElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetHasWeirdParserInsertionMode();
+ }
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTableRowElement, tr)
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ int32_t RowIndex() const;
+ int32_t SectionRowIndex() const;
+ nsIHTMLCollection* Cells();
+ already_AddRefed<nsGenericHTMLElement> InsertCell(int32_t aIndex,
+ ErrorResult& aError);
+ void DeleteCell(int32_t aValue, ErrorResult& aError);
+
+ void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); }
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+ void GetCh(DOMString& aCh) { GetHTMLAttr(nsGkAtoms::_char, aCh); }
+ void SetCh(const nsAString& aCh, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::_char, aCh, aError);
+ }
+ void GetChOff(DOMString& aChOff) { GetHTMLAttr(nsGkAtoms::charoff, aChOff); }
+ void SetChOff(const nsAString& aChOff, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::charoff, aChOff, aError);
+ }
+ void GetVAlign(DOMString& aVAlign) {
+ GetHTMLAttr(nsGkAtoms::valign, aVAlign);
+ }
+ void SetVAlign(const nsAString& aVAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::valign, aVAlign, aError);
+ }
+ void GetBgColor(DOMString& aBgColor) {
+ GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor);
+ }
+ void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError);
+ }
+
+ virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ virtual nsMapRuleToAttributesFunc GetAttributeMappingFunction()
+ const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTableRowElement,
+ nsGenericHTMLElement)
+
+ protected:
+ virtual ~HTMLTableRowElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ HTMLTableSectionElement* GetSection() const;
+ HTMLTableElement* GetTable() const;
+ RefPtr<nsContentList> mCells;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLTableRowElement_h */
diff --git a/dom/html/HTMLTableSectionElement.cpp b/dom/html/HTMLTableSectionElement.cpp
new file mode 100644
index 0000000000..358657b8d4
--- /dev/null
+++ b/dom/html/HTMLTableSectionElement.cpp
@@ -0,0 +1,177 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTableSectionElement.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "nsAttrValueInlines.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/HTMLTableSectionElementBinding.h"
+#include "nsContentUtils.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(TableSection)
+
+namespace mozilla::dom {
+
+// you will see the phrases "rowgroup" and "section" used interchangably
+
+HTMLTableSectionElement::~HTMLTableSectionElement() = default;
+
+JSObject* HTMLTableSectionElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTableSectionElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTableSectionElement,
+ nsGenericHTMLElement, mRows)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTableSectionElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_ELEMENT_CLONE(HTMLTableSectionElement)
+
+nsIHTMLCollection* HTMLTableSectionElement::Rows() {
+ if (!mRows) {
+ mRows = new nsContentList(this, mNodeInfo->NamespaceID(), nsGkAtoms::tr,
+ nsGkAtoms::tr, false);
+ }
+
+ return mRows;
+}
+
+already_AddRefed<nsGenericHTMLElement> HTMLTableSectionElement::InsertRow(
+ int32_t aIndex, ErrorResult& aError) {
+ if (aIndex < -1) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return nullptr;
+ }
+
+ nsIHTMLCollection* rows = Rows();
+
+ uint32_t rowCount = rows->Length();
+ if (aIndex > (int32_t)rowCount) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return nullptr;
+ }
+
+ bool doInsert = (aIndex < int32_t(rowCount)) && (aIndex != -1);
+
+ // create the row
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tr,
+ getter_AddRefs(nodeInfo));
+
+ RefPtr<nsGenericHTMLElement> rowContent =
+ NS_NewHTMLTableRowElement(nodeInfo.forget());
+ if (!rowContent) {
+ aError.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ if (doInsert) {
+ nsCOMPtr<nsINode> refNode = rows->Item(aIndex);
+ nsINode::InsertBefore(*rowContent, refNode, aError);
+ } else {
+ nsINode::AppendChild(*rowContent, aError);
+ }
+ return rowContent.forget();
+}
+
+void HTMLTableSectionElement::DeleteRow(int32_t aValue, ErrorResult& aError) {
+ if (aValue < -1) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ nsIHTMLCollection* rows = Rows();
+
+ uint32_t refIndex;
+ if (aValue == -1) {
+ refIndex = rows->Length();
+ if (refIndex == 0) {
+ return;
+ }
+
+ --refIndex;
+ } else {
+ refIndex = (uint32_t)aValue;
+ }
+
+ nsCOMPtr<nsINode> row = rows->Item(refIndex);
+ if (!row) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ nsINode::RemoveChild(*row, aError);
+}
+
+bool HTMLTableSectionElement::ParseAttribute(
+ int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ /* ignore these attributes, stored simply as strings
+ ch
+ */
+ if (aAttribute == nsGkAtoms::height) {
+ // Per HTML spec there should be nothing special here, but all browsers
+ // implement height mapping to style. See
+ // <https://github.com/whatwg/html/issues/4718>. All browsers allow 0, so
+ // keep doing that.
+ return aResult.ParseHTMLDimension(aValue);
+ }
+ if (aAttribute == nsGkAtoms::align) {
+ return ParseTableCellHAlignValue(aValue, aResult);
+ }
+ if (aAttribute == nsGkAtoms::bgcolor) {
+ return aResult.ParseColor(aValue);
+ }
+ if (aAttribute == nsGkAtoms::valign) {
+ return ParseTableVAlignValue(aValue, aResult);
+ }
+ }
+
+ return nsGenericHTMLElement::ParseBackgroundAttribute(
+ aNamespaceID, aAttribute, aValue, aResult) ||
+ nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLTableSectionElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ // height: value
+ if (!aBuilder.PropertyIsSet(eCSSProperty_height)) {
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::height);
+ if (value && value->Type() == nsAttrValue::eInteger) {
+ aBuilder.SetPixelValue(eCSSProperty_height,
+ (float)value->GetIntegerValue());
+ }
+ }
+ nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapVAlignAttributeInto(aBuilder);
+ nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder);
+ nsGenericHTMLElement::MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLTableSectionElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::align}, {nsGkAtoms::valign}, {nsGkAtoms::height}, {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sCommonAttributeMap,
+ sBackgroundAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLTableSectionElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTableSectionElement.h b/dom/html/HTMLTableSectionElement.h
new file mode 100644
index 0000000000..e1ea59622c
--- /dev/null
+++ b/dom/html/HTMLTableSectionElement.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLTableSectionElement_h
+#define mozilla_dom_HTMLTableSectionElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsContentList.h" // For ctor.
+
+namespace mozilla::dom {
+
+class HTMLTableSectionElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLTableSectionElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetHasWeirdParserInsertionMode();
+ }
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ nsIHTMLCollection* Rows();
+ already_AddRefed<nsGenericHTMLElement> InsertRow(int32_t aIndex,
+ ErrorResult& aError);
+ void DeleteRow(int32_t aValue, ErrorResult& aError);
+
+ void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); }
+ void SetAlign(const nsAString& aAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::align, aAlign, aError);
+ }
+ void GetCh(DOMString& aCh) { GetHTMLAttr(nsGkAtoms::_char, aCh); }
+ void SetCh(const nsAString& aCh, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::_char, aCh, aError);
+ }
+ void GetChOff(DOMString& aChOff) { GetHTMLAttr(nsGkAtoms::charoff, aChOff); }
+ void SetChOff(const nsAString& aChOff, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::charoff, aChOff, aError);
+ }
+ void GetVAlign(DOMString& aVAlign) {
+ GetHTMLAttr(nsGkAtoms::valign, aVAlign);
+ }
+ void SetVAlign(const nsAString& aVAlign, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::valign, aVAlign, aError);
+ }
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTableSectionElement,
+ nsGenericHTMLElement)
+ protected:
+ virtual ~HTMLTableSectionElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ RefPtr<nsContentList> mRows;
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLTableSectionElement_h */
diff --git a/dom/html/HTMLTemplateElement.cpp b/dom/html/HTMLTemplateElement.cpp
new file mode 100644
index 0000000000..2a608f1c50
--- /dev/null
+++ b/dom/html/HTMLTemplateElement.cpp
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTemplateElement.h"
+#include "mozilla/dom/HTMLTemplateElementBinding.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/NameSpaceConstants.h"
+#include "mozilla/dom/ShadowRootBinding.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "nsStyleConsts.h"
+#include "nsAtom.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Template)
+
+namespace mozilla::dom {
+
+static constexpr nsAttrValue::EnumTable kShadowRootModeTable[] = {
+ {"open", ShadowRootMode::Open},
+ {"closed", ShadowRootMode::Closed},
+ {nullptr, {}}};
+
+const nsAttrValue::EnumTable* kShadowRootModeDefault = &kShadowRootModeTable[2];
+
+HTMLTemplateElement::HTMLTemplateElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ SetHasWeirdParserInsertionMode();
+
+ Document* contentsOwner = OwnerDoc()->GetTemplateContentsOwner();
+ if (!contentsOwner) {
+ MOZ_CRASH("There should always be a template contents owner.");
+ }
+
+ mContent = contentsOwner->CreateDocumentFragment();
+ mContent->SetHost(this);
+}
+
+HTMLTemplateElement::~HTMLTemplateElement() {
+ if (mContent && mContent->GetHost() == this) {
+ mContent->SetHost(nullptr);
+ }
+}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTemplateElement,
+ nsGenericHTMLElement)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLTemplateElement)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLTemplateElement,
+ nsGenericHTMLElement)
+ if (tmp->mContent) {
+ if (tmp->mContent->GetHost() == tmp) {
+ tmp->mContent->SetHost(nullptr);
+ }
+ tmp->mContent = nullptr;
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLTemplateElement,
+ nsGenericHTMLElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ELEMENT_CLONE(HTMLTemplateElement)
+
+JSObject* HTMLTemplateElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTemplateElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void HTMLTemplateElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None && aName == nsGkAtoms::shadowrootmode &&
+ aValue && aValue->Type() == nsAttrValue::ValueType::eEnum &&
+ !mShadowRootMode.isSome()) {
+ mShadowRootMode.emplace(
+ static_cast<ShadowRootMode>(aValue->GetEnumValue()));
+ }
+
+ nsGenericHTMLElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue,
+ aMaybeScriptedPrincipal, aNotify);
+}
+
+bool HTMLTemplateElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None &&
+ aAttribute == nsGkAtoms::shadowrootmode) {
+ return aResult.ParseEnumValue(aValue, kShadowRootModeTable, false, nullptr);
+ }
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLTemplateElement::SetHTMLUnsafe(const nsAString& aHTML) {
+ RefPtr<DocumentFragment> content = mContent;
+ nsContentUtils::SetHTMLUnsafe(content, this, aHTML);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTemplateElement.h b/dom/html/HTMLTemplateElement.h
new file mode 100644
index 0000000000..be643d215b
--- /dev/null
+++ b/dom/html/HTMLTemplateElement.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLTemplateElement_h
+#define mozilla_dom_HTMLTemplateElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "nsGenericHTMLElement.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/ShadowRootBinding.h"
+#include "nsGkAtoms.h"
+
+namespace mozilla::dom {
+
+class HTMLTemplateElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLTemplateElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTemplateElement, _template);
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTemplateElement,
+ nsGenericHTMLElement)
+
+ void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ DocumentFragment* Content() { return mContent; }
+ void SetContent(DocumentFragment* aContent) { mContent = aContent; }
+
+ void GetShadowRootMode(nsAString& aResult) const {
+ GetEnumAttr(nsGkAtoms::shadowrootmode, nullptr, aResult);
+ }
+ void SetShadowRootMode(const nsAString& aValue) {
+ SetHTMLAttr(nsGkAtoms::shadowrootmode, aValue);
+ }
+
+ bool ShadowRootDelegatesFocus() {
+ return GetBoolAttr(nsGkAtoms::shadowrootdelegatesfocus);
+ }
+ void SetShadowRootDelegatesFocus(bool aValue) {
+ SetHTMLBoolAttr(nsGkAtoms::shadowrootdelegatesfocus, aValue,
+ IgnoredErrorResult());
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ void SetHTMLUnsafe(const nsAString& aHTML) final;
+
+ protected:
+ virtual ~HTMLTemplateElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ RefPtr<DocumentFragment> mContent;
+ Maybe<ShadowRootMode> mShadowRootMode;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLTemplateElement_h
diff --git a/dom/html/HTMLTextAreaElement.cpp b/dom/html/HTMLTextAreaElement.cpp
new file mode 100644
index 0000000000..ce28575a4d
--- /dev/null
+++ b/dom/html/HTMLTextAreaElement.cpp
@@ -0,0 +1,1164 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTextAreaElement.h"
+
+#include "mozAutoDocUpdate.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/FormData.h"
+#include "mozilla/dom/HTMLTextAreaElementBinding.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresState.h"
+#include "mozilla/TextControlState.h"
+#include "nsAttrValueInlines.h"
+#include "nsBaseCommandController.h"
+#include "nsContentCID.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsError.h"
+#include "nsFocusManager.h"
+#include "nsIConstraintValidation.h"
+#include "nsIControllers.h"
+#include "mozilla/dom/Document.h"
+#include "nsIFormControlFrame.h"
+#include "nsIFormControl.h"
+#include "nsIFrame.h"
+#include "nsITextControlFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsLinebreakConverter.h"
+#include "nsPresContext.h"
+#include "nsReadableUtils.h"
+#include "nsStyleConsts.h"
+#include "nsTextControlFrame.h"
+#include "nsThreadUtils.h"
+#include "nsXULControllers.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(TextArea)
+
+namespace mozilla::dom {
+
+HTMLTextAreaElement::HTMLTextAreaElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser)
+ : TextControlElement(std::move(aNodeInfo), aFromParser,
+ FormControlType::Textarea),
+ mDoneAddingChildren(!aFromParser),
+ mInhibitStateRestoration(!!(aFromParser & FROM_PARSER_FRAGMENT)),
+ mAutocompleteAttrState(nsContentUtils::eAutocompleteAttrState_Unknown),
+ mState(TextControlState::Construct(this)) {
+ AddMutationObserver(this);
+
+ // Set up our default state. By default we're enabled (since we're
+ // a control type that can be disabled but not actually disabled right now),
+ // optional, read-write, and valid. Also by default we don't have to show
+ // validity UI and so forth.
+ AddStatesSilently(ElementState::ENABLED | ElementState::OPTIONAL_ |
+ ElementState::READWRITE | ElementState::VALID |
+ ElementState::VALUE_EMPTY);
+ RemoveStatesSilently(ElementState::READONLY);
+}
+
+HTMLTextAreaElement::~HTMLTextAreaElement() {
+ mState->Destroy();
+ mState = nullptr;
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLTextAreaElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLTextAreaElement,
+ TextControlElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControllers)
+ if (tmp->mState) {
+ tmp->mState->Traverse(cb);
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLTextAreaElement,
+ TextControlElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mControllers)
+ if (tmp->mState) {
+ tmp->mState->Unlink();
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLTextAreaElement,
+ TextControlElement,
+ nsIMutationObserver,
+ nsIConstraintValidation)
+
+// nsIDOMHTMLTextAreaElement
+
+nsresult HTMLTextAreaElement::Clone(dom::NodeInfo* aNodeInfo,
+ nsINode** aResult) const {
+ *aResult = nullptr;
+ RefPtr<HTMLTextAreaElement> it = new (aNodeInfo->NodeInfoManager())
+ HTMLTextAreaElement(do_AddRef(aNodeInfo));
+
+ nsresult rv = const_cast<HTMLTextAreaElement*>(this)->CopyInnerTo(it);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ it->SetLastValueChangeWasInteractive(mLastValueChangeWasInteractive);
+ it.forget(aResult);
+ return NS_OK;
+}
+
+// nsIContent
+
+void HTMLTextAreaElement::Select() {
+ if (FocusState() != FocusTristate::eUnfocusable) {
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ fm->SetFocus(this, nsIFocusManager::FLAG_NOSCROLL);
+ }
+ }
+
+ SetSelectionRange(0, UINT32_MAX, mozilla::dom::Optional<nsAString>(),
+ IgnoreErrors());
+}
+
+NS_IMETHODIMP
+HTMLTextAreaElement::SelectAll(nsPresContext* aPresContext) {
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(true);
+
+ if (formControlFrame) {
+ formControlFrame->SetFormProperty(nsGkAtoms::select, u""_ns);
+ }
+
+ return NS_OK;
+}
+
+bool HTMLTextAreaElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable(
+ aWithMouse, aIsFocusable, aTabIndex)) {
+ return true;
+ }
+
+ // disabled textareas are not focusable
+ *aIsFocusable = !IsDisabled();
+ return false;
+}
+
+int32_t HTMLTextAreaElement::TabIndexDefault() { return 0; }
+
+void HTMLTextAreaElement::GetType(nsAString& aType) {
+ aType.AssignLiteral("textarea");
+}
+
+void HTMLTextAreaElement::GetValue(nsAString& aValue) {
+ GetValueInternal(aValue, true);
+ MOZ_ASSERT(aValue.FindChar(static_cast<char16_t>('\r')) == -1);
+}
+
+void HTMLTextAreaElement::GetValueInternal(nsAString& aValue,
+ bool aIgnoreWrap) const {
+ MOZ_ASSERT(mState);
+ mState->GetValue(aValue, aIgnoreWrap, /* aForDisplay = */ true);
+}
+
+nsIEditor* HTMLTextAreaElement::GetEditorForBindings() {
+ if (!GetPrimaryFrame()) {
+ GetPrimaryFrame(FlushType::Frames);
+ }
+ return GetTextEditor();
+}
+
+TextEditor* HTMLTextAreaElement::GetTextEditor() {
+ MOZ_ASSERT(mState);
+ return mState->GetTextEditor();
+}
+
+TextEditor* HTMLTextAreaElement::GetTextEditorWithoutCreation() const {
+ MOZ_ASSERT(mState);
+ return mState->GetTextEditorWithoutCreation();
+}
+
+nsISelectionController* HTMLTextAreaElement::GetSelectionController() {
+ MOZ_ASSERT(mState);
+ return mState->GetSelectionController();
+}
+
+nsFrameSelection* HTMLTextAreaElement::GetConstFrameSelection() {
+ MOZ_ASSERT(mState);
+ return mState->GetConstFrameSelection();
+}
+
+nsresult HTMLTextAreaElement::BindToFrame(nsTextControlFrame* aFrame) {
+ MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript());
+ MOZ_ASSERT(mState);
+ return mState->BindToFrame(aFrame);
+}
+
+void HTMLTextAreaElement::UnbindFromFrame(nsTextControlFrame* aFrame) {
+ MOZ_ASSERT(mState);
+ if (aFrame) {
+ mState->UnbindFromFrame(aFrame);
+ }
+}
+
+nsresult HTMLTextAreaElement::CreateEditor() {
+ MOZ_ASSERT(mState);
+ return mState->PrepareEditor();
+}
+
+void HTMLTextAreaElement::SetPreviewValue(const nsAString& aValue) {
+ MOZ_ASSERT(mState);
+ mState->SetPreviewText(aValue, true);
+}
+
+void HTMLTextAreaElement::GetPreviewValue(nsAString& aValue) {
+ MOZ_ASSERT(mState);
+ mState->GetPreviewText(aValue);
+}
+
+void HTMLTextAreaElement::EnablePreview() {
+ if (mIsPreviewEnabled) {
+ return;
+ }
+
+ mIsPreviewEnabled = true;
+ // Reconstruct the frame to append an anonymous preview node
+ nsLayoutUtils::PostRestyleEvent(this, RestyleHint{0},
+ nsChangeHint_ReconstructFrame);
+}
+
+bool HTMLTextAreaElement::IsPreviewEnabled() { return mIsPreviewEnabled; }
+
+nsresult HTMLTextAreaElement::SetValueInternal(
+ const nsAString& aValue, const ValueSetterOptions& aOptions) {
+ MOZ_ASSERT(mState);
+
+ // Need to set the value changed flag here if our value has in fact changed
+ // (i.e. if ValueSetterOption::SetValueChanged is in aOptions), so that
+ // retrieves the correct value if needed.
+ if (aOptions.contains(ValueSetterOption::SetValueChanged)) {
+ SetValueChanged(true);
+ }
+
+ if (!mState->SetValue(aValue, aOptions)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+void HTMLTextAreaElement::SetValue(const nsAString& aValue,
+ ErrorResult& aError) {
+ // If the value has been set by a script, we basically want to keep the
+ // current change event state. If the element is ready to fire a change
+ // event, we should keep it that way. Otherwise, we should make sure the
+ // element will not fire any event because of the script interaction.
+ //
+ // NOTE: this is currently quite expensive work (too much string
+ // manipulation). We should probably optimize that.
+ nsAutoString currentValue;
+ GetValueInternal(currentValue, true);
+
+ nsresult rv = SetValueInternal(
+ aValue,
+ {ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged,
+ ValueSetterOption::MoveCursorToEndIfValueChanged});
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aError.Throw(rv);
+ return;
+ }
+
+ if (mFocusedValue.Equals(currentValue)) {
+ GetValueInternal(mFocusedValue, true);
+ }
+}
+
+void HTMLTextAreaElement::SetUserInput(const nsAString& aValue,
+ nsIPrincipal& aSubjectPrincipal) {
+ SetValueInternal(aValue, {ValueSetterOption::BySetUserInputAPI,
+ ValueSetterOption::SetValueChanged,
+ ValueSetterOption::MoveCursorToEndIfValueChanged});
+}
+
+void HTMLTextAreaElement::SetValueChanged(bool aValueChanged) {
+ MOZ_ASSERT(mState);
+
+ bool previousValue = mValueChanged;
+ mValueChanged = aValueChanged;
+ if (!aValueChanged && !mState->IsEmpty()) {
+ mState->EmptyValue();
+ }
+ if (mValueChanged == previousValue) {
+ return;
+ }
+ UpdateTooLongValidityState();
+ UpdateTooShortValidityState();
+ UpdateValidityElementStates(true);
+}
+
+void HTMLTextAreaElement::SetLastValueChangeWasInteractive(
+ bool aWasInteractive) {
+ if (aWasInteractive == mLastValueChangeWasInteractive) {
+ return;
+ }
+ mLastValueChangeWasInteractive = aWasInteractive;
+ const bool wasValid = IsValid();
+ UpdateTooLongValidityState();
+ UpdateTooShortValidityState();
+ if (wasValid != IsValid()) {
+ UpdateValidityElementStates(true);
+ }
+}
+
+void HTMLTextAreaElement::GetDefaultValue(nsAString& aDefaultValue,
+ ErrorResult& aError) const {
+ if (!nsContentUtils::GetNodeTextContent(this, false, aDefaultValue,
+ fallible)) {
+ aError.Throw(NS_ERROR_OUT_OF_MEMORY);
+ }
+}
+
+void HTMLTextAreaElement::SetDefaultValue(const nsAString& aDefaultValue,
+ ErrorResult& aError) {
+ // setting the value of an textarea element using `.defaultValue = "foo"`
+ // must be interpreted as a two-step operation:
+ // 1. clearing all child nodes
+ // 2. adding a new text node with the new content
+ // Step 1 must therefore collapse the Selection to 0.
+ // Calling `SetNodeTextContent()` with an empty string will do that for us.
+ nsContentUtils::SetNodeTextContent(this, EmptyString(), true);
+ nsresult rv = nsContentUtils::SetNodeTextContent(this, aDefaultValue, true);
+ if (NS_SUCCEEDED(rv) && !mValueChanged) {
+ Reset();
+ }
+ if (NS_FAILED(rv)) {
+ aError.Throw(rv);
+ }
+}
+
+bool HTMLTextAreaElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::maxlength ||
+ aAttribute == nsGkAtoms::minlength) {
+ return aResult.ParseNonNegativeIntValue(aValue);
+ } else if (aAttribute == nsGkAtoms::cols) {
+ aResult.ParseIntWithFallback(aValue, DEFAULT_COLS);
+ return true;
+ } else if (aAttribute == nsGkAtoms::rows) {
+ aResult.ParseIntWithFallback(aValue, DEFAULT_ROWS_TEXTAREA);
+ return true;
+ } else if (aAttribute == nsGkAtoms::autocomplete) {
+ aResult.ParseAtomArray(aValue);
+ return true;
+ }
+ }
+ return TextControlElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLTextAreaElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ // wrap=off
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::wrap);
+ if (value && value->Type() == nsAttrValue::eString &&
+ value->Equals(nsGkAtoms::OFF, eIgnoreCase)) {
+ // Equivalent to expanding `white-space; pre`
+ aBuilder.SetKeywordValue(eCSSProperty_white_space_collapse,
+ StyleWhiteSpaceCollapse::Preserve);
+ aBuilder.SetKeywordValue(eCSSProperty_text_wrap_mode,
+ StyleTextWrapMode::Nowrap);
+ }
+
+ nsGenericHTMLFormControlElementWithState::MapDivAlignAttributeInto(aBuilder);
+ nsGenericHTMLFormControlElementWithState::MapCommonAttributesInto(aBuilder);
+}
+
+nsChangeHint HTMLTextAreaElement::GetAttributeChangeHint(
+ const nsAtom* aAttribute, int32_t aModType) const {
+ nsChangeHint retval =
+ nsGenericHTMLFormControlElementWithState::GetAttributeChangeHint(
+ aAttribute, aModType);
+
+ const bool isAdditionOrRemoval =
+ aModType == MutationEvent_Binding::ADDITION ||
+ aModType == MutationEvent_Binding::REMOVAL;
+
+ if (aAttribute == nsGkAtoms::rows || aAttribute == nsGkAtoms::cols) {
+ retval |= NS_STYLE_HINT_REFLOW;
+ } else if (aAttribute == nsGkAtoms::wrap) {
+ retval |= nsChangeHint_ReconstructFrame;
+ } else if (aAttribute == nsGkAtoms::placeholder && isAdditionOrRemoval) {
+ retval |= nsChangeHint_ReconstructFrame;
+ }
+ return retval;
+}
+
+NS_IMETHODIMP_(bool)
+HTMLTextAreaElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {{nsGkAtoms::wrap},
+ {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {
+ attributes,
+ sDivAlignAttributeMap,
+ sCommonAttributeMap,
+ };
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLTextAreaElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+bool HTMLTextAreaElement::IsDisabledForEvents(WidgetEvent* aEvent) {
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(false);
+ nsIFrame* formFrame = do_QueryFrame(formControlFrame);
+ return IsElementDisabledForEvents(aEvent, formFrame);
+}
+
+void HTMLTextAreaElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
+ aVisitor.mCanHandle = false;
+ if (IsDisabledForEvents(aVisitor.mEvent)) {
+ return;
+ }
+
+ // Don't dispatch a second select event if we are already handling
+ // one.
+ if (aVisitor.mEvent->mMessage == eFormSelect) {
+ if (mHandlingSelect) {
+ return;
+ }
+ mHandlingSelect = true;
+ }
+
+ if (aVisitor.mEvent->mMessage == eBlur) {
+ // Set mWantsPreHandleEvent and fire change event in PreHandleEvent to
+ // prevent it breaks event target chain creation.
+ aVisitor.mWantsPreHandleEvent = true;
+ }
+
+ nsGenericHTMLFormControlElementWithState::GetEventTargetParent(aVisitor);
+}
+
+nsresult HTMLTextAreaElement::PreHandleEvent(EventChainVisitor& aVisitor) {
+ if (aVisitor.mEvent->mMessage == eBlur) {
+ // Fire onchange (if necessary), before we do the blur, bug 370521.
+ FireChangeEventIfNeeded();
+ }
+ return nsGenericHTMLFormControlElementWithState::PreHandleEvent(aVisitor);
+}
+
+void HTMLTextAreaElement::FireChangeEventIfNeeded() {
+ nsString value;
+ GetValueInternal(value, true);
+
+ // NOTE(emilio): This is not quite on the spec, but matches <input>, see
+ // https://github.com/whatwg/html/issues/10011 and
+ // https://github.com/whatwg/html/issues/10013
+ if (mValueChanged) {
+ SetUserInteracted(true);
+ }
+
+ if (mFocusedValue.Equals(value)) {
+ return;
+ }
+
+ // Dispatch the change event.
+ mFocusedValue = value;
+ nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, u"change"_ns,
+ CanBubble::eYes, Cancelable::eNo);
+}
+
+nsresult HTMLTextAreaElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
+ if (aVisitor.mEvent->mMessage == eFormSelect) {
+ mHandlingSelect = false;
+ }
+ if (aVisitor.mEvent->mMessage == eFocus) {
+ GetValueInternal(mFocusedValue, true);
+ }
+ return NS_OK;
+}
+
+void HTMLTextAreaElement::DoneAddingChildren(bool aHaveNotified) {
+ if (!mValueChanged) {
+ if (!mDoneAddingChildren) {
+ // Reset now that we're done adding children if the content sink tried to
+ // sneak some text in without calling AppendChildTo.
+ Reset();
+ }
+
+ if (!mInhibitStateRestoration) {
+ GenerateStateKey();
+ RestoreFormControlState();
+ }
+ }
+
+ mDoneAddingChildren = true;
+}
+
+// Controllers Methods
+
+nsIControllers* HTMLTextAreaElement::GetControllers(ErrorResult& aError) {
+ if (!mControllers) {
+ mControllers = new nsXULControllers();
+ if (!mControllers) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<nsBaseCommandController> commandController =
+ nsBaseCommandController::CreateEditorController();
+ if (!commandController) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ mControllers->AppendController(commandController);
+
+ commandController = nsBaseCommandController::CreateEditingController();
+ if (!commandController) {
+ aError.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ mControllers->AppendController(commandController);
+ }
+
+ return mControllers;
+}
+
+nsresult HTMLTextAreaElement::GetControllers(nsIControllers** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ ErrorResult error;
+ *aResult = GetControllers(error);
+ NS_IF_ADDREF(*aResult);
+
+ return error.StealNSResult();
+}
+
+uint32_t HTMLTextAreaElement::GetTextLength() {
+ nsAutoString val;
+ GetValue(val);
+ return val.Length();
+}
+
+Nullable<uint32_t> HTMLTextAreaElement::GetSelectionStart(ErrorResult& aError) {
+ uint32_t selStart, selEnd;
+ GetSelectionRange(&selStart, &selEnd, aError);
+ return Nullable<uint32_t>(selStart);
+}
+
+void HTMLTextAreaElement::SetSelectionStart(
+ const Nullable<uint32_t>& aSelectionStart, ErrorResult& aError) {
+ MOZ_ASSERT(mState);
+ mState->SetSelectionStart(aSelectionStart, aError);
+}
+
+Nullable<uint32_t> HTMLTextAreaElement::GetSelectionEnd(ErrorResult& aError) {
+ uint32_t selStart, selEnd;
+ GetSelectionRange(&selStart, &selEnd, aError);
+ return Nullable<uint32_t>(selEnd);
+}
+
+void HTMLTextAreaElement::SetSelectionEnd(
+ const Nullable<uint32_t>& aSelectionEnd, ErrorResult& aError) {
+ MOZ_ASSERT(mState);
+ mState->SetSelectionEnd(aSelectionEnd, aError);
+}
+
+void HTMLTextAreaElement::GetSelectionRange(uint32_t* aSelectionStart,
+ uint32_t* aSelectionEnd,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mState);
+ return mState->GetSelectionRange(aSelectionStart, aSelectionEnd, aRv);
+}
+
+void HTMLTextAreaElement::GetSelectionDirection(nsAString& aDirection,
+ ErrorResult& aError) {
+ MOZ_ASSERT(mState);
+ mState->GetSelectionDirectionString(aDirection, aError);
+}
+
+void HTMLTextAreaElement::SetSelectionDirection(const nsAString& aDirection,
+ ErrorResult& aError) {
+ MOZ_ASSERT(mState);
+ mState->SetSelectionDirection(aDirection, aError);
+}
+
+void HTMLTextAreaElement::SetSelectionRange(
+ uint32_t aSelectionStart, uint32_t aSelectionEnd,
+ const Optional<nsAString>& aDirection, ErrorResult& aError) {
+ MOZ_ASSERT(mState);
+ mState->SetSelectionRange(aSelectionStart, aSelectionEnd, aDirection, aError);
+}
+
+void HTMLTextAreaElement::SetRangeText(const nsAString& aReplacement,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mState);
+ mState->SetRangeText(aReplacement, aRv);
+}
+
+void HTMLTextAreaElement::SetRangeText(const nsAString& aReplacement,
+ uint32_t aStart, uint32_t aEnd,
+ SelectionMode aSelectMode,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mState);
+ mState->SetRangeText(aReplacement, aStart, aEnd, aSelectMode, aRv);
+}
+
+void HTMLTextAreaElement::GetValueFromSetRangeText(nsAString& aValue) {
+ GetValueInternal(aValue, false);
+}
+
+nsresult HTMLTextAreaElement::SetValueFromSetRangeText(
+ const nsAString& aValue) {
+ return SetValueInternal(aValue, {ValueSetterOption::ByContentAPI,
+ ValueSetterOption::BySetRangeTextAPI,
+ ValueSetterOption::SetValueChanged});
+}
+
+void HTMLTextAreaElement::SetDirectionFromValue(bool aNotify,
+ const nsAString* aKnownValue) {
+ nsAutoString value;
+ if (!aKnownValue) {
+ GetValue(value);
+ aKnownValue = &value;
+ }
+ SetDirectionalityFromValue(this, *aKnownValue, aNotify);
+}
+
+nsresult HTMLTextAreaElement::Reset() {
+ nsAutoString resetVal;
+ GetDefaultValue(resetVal, IgnoreErrors());
+ SetValueChanged(false);
+ SetUserInteracted(false);
+
+ nsresult rv = SetValueInternal(resetVal, ValueSetterOption::ByInternalAPI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLTextAreaElement::SubmitNamesValues(FormData* aFormData) {
+ //
+ // Get the name (if no name, no submit)
+ //
+ nsAutoString name;
+ GetAttr(nsGkAtoms::name, name);
+ if (name.IsEmpty()) {
+ return NS_OK;
+ }
+
+ //
+ // Get the value
+ //
+ nsAutoString value;
+ GetValueInternal(value, false);
+
+ //
+ // Submit name=value
+ //
+ const nsresult rv = aFormData->AddNameValuePair(name, value);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Submit dirname=dir
+ return SubmitDirnameDir(aFormData);
+}
+
+void HTMLTextAreaElement::SaveState() {
+ // Only save if value != defaultValue (bug 62713)
+ PresState* state = nullptr;
+ if (mValueChanged) {
+ state = GetPrimaryPresState();
+ if (state) {
+ nsAutoString value;
+ GetValueInternal(value, true);
+
+ if (NS_FAILED(nsLinebreakConverter::ConvertStringLineBreaks(
+ value, nsLinebreakConverter::eLinebreakPlatform,
+ nsLinebreakConverter::eLinebreakContent))) {
+ NS_ERROR("Converting linebreaks failed!");
+ return;
+ }
+
+ state->contentData() =
+ TextContentData(value, mLastValueChangeWasInteractive);
+ }
+ }
+
+ if (mDisabledChanged) {
+ if (!state) {
+ state = GetPrimaryPresState();
+ }
+ if (state) {
+ // We do not want to save the real disabled state but the disabled
+ // attribute.
+ state->disabled() = HasAttr(nsGkAtoms::disabled);
+ state->disabledSet() = true;
+ }
+ }
+}
+
+bool HTMLTextAreaElement::RestoreState(PresState* aState) {
+ const PresContentData& state = aState->contentData();
+
+ if (state.type() == PresContentData::TTextContentData) {
+ ErrorResult rv;
+ SetValue(state.get_TextContentData().value(), rv);
+ ENSURE_SUCCESS(rv, false);
+ if (state.get_TextContentData().lastValueChangeWasInteractive()) {
+ SetLastValueChangeWasInteractive(true);
+ }
+ }
+ if (aState->disabledSet() && !aState->disabled()) {
+ SetDisabled(false, IgnoreErrors());
+ }
+
+ return false;
+}
+
+void HTMLTextAreaElement::UpdateValidityElementStates(bool aNotify) {
+ AutoStateChangeNotifier notifier(*this, aNotify);
+ RemoveStatesSilently(ElementState::VALIDITY_STATES);
+ if (!IsCandidateForConstraintValidation()) {
+ return;
+ }
+ ElementState state;
+ if (IsValid()) {
+ state |= ElementState::VALID;
+ if (mUserInteracted) {
+ state |= ElementState::USER_VALID;
+ }
+ } else {
+ state |= ElementState::INVALID;
+ if (mUserInteracted) {
+ state |= ElementState::USER_INVALID;
+ }
+ }
+ AddStatesSilently(state);
+}
+
+nsresult HTMLTextAreaElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv =
+ nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set direction based on value if dir=auto
+ if (HasDirAuto()) {
+ SetDirectionFromValue(false);
+ }
+
+ // If there is a disabled fieldset in the parent chain, the element is now
+ // barred from constraint validation and can't suffer from value missing.
+ UpdateValueMissingValidityState();
+ UpdateBarredFromConstraintValidation();
+
+ // And now make sure our state is up to date
+ UpdateValidityElementStates(false);
+
+ return rv;
+}
+
+void HTMLTextAreaElement::UnbindFromTree(bool aNullParent) {
+ nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent);
+
+ // We might be no longer disabled because of parent chain changed.
+ UpdateValueMissingValidityState();
+ UpdateBarredFromConstraintValidation();
+
+ // And now make sure our state is up to date
+ UpdateValidityElementStates(false);
+}
+
+void HTMLTextAreaElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ bool aNotify) {
+ if (aNotify && aName == nsGkAtoms::disabled &&
+ aNameSpaceID == kNameSpaceID_None) {
+ mDisabledChanged = true;
+ }
+
+ return nsGenericHTMLFormControlElementWithState::BeforeSetAttr(
+ aNameSpaceID, aName, aValue, aNotify);
+}
+
+void HTMLTextAreaElement::CharacterDataChanged(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {
+ ContentChanged(aContent);
+}
+
+void HTMLTextAreaElement::ContentAppended(nsIContent* aFirstNewContent) {
+ ContentChanged(aFirstNewContent);
+}
+
+void HTMLTextAreaElement::ContentInserted(nsIContent* aChild) {
+ ContentChanged(aChild);
+}
+
+void HTMLTextAreaElement::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ ContentChanged(aChild);
+}
+
+void HTMLTextAreaElement::ContentChanged(nsIContent* aContent) {
+ if (!mValueChanged && mDoneAddingChildren &&
+ nsContentUtils::IsInSameAnonymousTree(this, aContent)) {
+ if (mState->IsSelectionCached()) {
+ // In case the content is *replaced*, i.e. by calling
+ // `.textContent = "foo";`,
+ // firstly the old content is removed, then the new content is added.
+ // As per wpt, this must collapse the selection to 0.
+ // Removing and adding of an element is routed through here, but due to
+ // the script runner `Reset()` is only invoked after the append operation.
+ // Therefore, `Reset()` would adjust the Selection to the new value, not
+ // to 0.
+ // By forcing a selection update here, the selection is reset in order to
+ // comply with the wpt.
+ auto& props = mState->GetSelectionProperties();
+ nsAutoString resetVal;
+ GetDefaultValue(resetVal, IgnoreErrors());
+ props.SetMaxLength(resetVal.Length());
+ props.SetStart(props.GetStart());
+ props.SetEnd(props.GetEnd());
+ }
+ // We should wait all ranges finish handling the mutation before updating
+ // the anonymous subtree with a call of Reset.
+ nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+ "ResetHTMLTextAreaElementIfValueHasNotChangedYet",
+ [self = RefPtr{this}]() {
+ // However, if somebody has already changed the value, we don't need
+ // to keep doing this.
+ if (!self->mValueChanged) {
+ self->Reset();
+ }
+ }));
+ }
+}
+
+void HTMLTextAreaElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::required || aName == nsGkAtoms::disabled ||
+ aName == nsGkAtoms::readonly) {
+ if (aName == nsGkAtoms::disabled) {
+ // This *has* to be called *before* validity state check because
+ // UpdateBarredFromConstraintValidation and
+ // UpdateValueMissingValidityState depend on our disabled state.
+ UpdateDisabledState(aNotify);
+ }
+
+ if (aName == nsGkAtoms::required) {
+ // This *has* to be called *before* UpdateValueMissingValidityState
+ // because UpdateValueMissingValidityState depends on our required
+ // state.
+ UpdateRequiredState(!!aValue, aNotify);
+ }
+
+ if (aName == nsGkAtoms::readonly && !!aValue != !!aOldValue) {
+ UpdateReadOnlyState(aNotify);
+ }
+
+ UpdateValueMissingValidityState();
+
+ // This *has* to be called *after* validity has changed.
+ if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) {
+ UpdateBarredFromConstraintValidation();
+ }
+ UpdateValidityElementStates(aNotify);
+ } else if (aName == nsGkAtoms::autocomplete) {
+ // Clear the cached @autocomplete attribute state.
+ mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown;
+ } else if (aName == nsGkAtoms::maxlength) {
+ UpdateTooLongValidityState();
+ UpdateValidityElementStates(aNotify);
+ } else if (aName == nsGkAtoms::minlength) {
+ UpdateTooShortValidityState();
+ UpdateValidityElementStates(aNotify);
+ } else if (aName == nsGkAtoms::placeholder) {
+ if (nsTextControlFrame* f = do_QueryFrame(GetPrimaryFrame())) {
+ f->PlaceholderChanged(aOldValue, aValue);
+ }
+ UpdatePlaceholderShownState();
+ } else if (aName == nsGkAtoms::dir && aValue &&
+ aValue->Equals(nsGkAtoms::_auto, eIgnoreCase)) {
+ SetDirectionFromValue(aNotify);
+ }
+ }
+
+ return nsGenericHTMLFormControlElementWithState::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
+}
+
+nsresult HTMLTextAreaElement::CopyInnerTo(Element* aDest) {
+ nsresult rv = nsGenericHTMLFormControlElementWithState::CopyInnerTo(aDest);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mValueChanged || aDest->OwnerDoc()->IsStaticDocument()) {
+ // Set our value on the clone.
+ auto* dest = static_cast<HTMLTextAreaElement*>(aDest);
+
+ nsAutoString value;
+ GetValueInternal(value, true);
+
+ // SetValueInternal handles setting mValueChanged for us. dest is a fresh
+ // element so setting its value can't really run script.
+ if (NS_WARN_IF(
+ NS_FAILED(rv = MOZ_KnownLive(dest)->SetValueInternal(
+ value, {ValueSetterOption::SetValueChanged})))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+bool HTMLTextAreaElement::IsMutable() const { return !IsDisabledOrReadOnly(); }
+
+void HTMLTextAreaElement::SetCustomValidity(const nsAString& aError) {
+ ConstraintValidation::SetCustomValidity(aError);
+ UpdateValidityElementStates(true);
+}
+
+bool HTMLTextAreaElement::IsTooLong() {
+ if (!mValueChanged || !mLastValueChangeWasInteractive ||
+ !HasAttr(nsGkAtoms::maxlength)) {
+ return false;
+ }
+
+ int32_t maxLength = MaxLength();
+
+ // Maxlength of -1 means parsing error.
+ if (maxLength == -1) {
+ return false;
+ }
+
+ int32_t textLength = GetTextLength();
+
+ return textLength > maxLength;
+}
+
+bool HTMLTextAreaElement::IsTooShort() {
+ if (!mValueChanged || !mLastValueChangeWasInteractive ||
+ !HasAttr(nsGkAtoms::minlength)) {
+ return false;
+ }
+
+ int32_t minLength = MinLength();
+
+ // Minlength of -1 means parsing error.
+ if (minLength == -1) {
+ return false;
+ }
+
+ int32_t textLength = GetTextLength();
+
+ return textLength && textLength < minLength;
+}
+
+bool HTMLTextAreaElement::IsValueMissing() const {
+ if (!Required() || !IsMutable()) {
+ return false;
+ }
+ return IsValueEmpty();
+}
+
+void HTMLTextAreaElement::UpdateTooLongValidityState() {
+ SetValidityState(VALIDITY_STATE_TOO_LONG, IsTooLong());
+}
+
+void HTMLTextAreaElement::UpdateTooShortValidityState() {
+ SetValidityState(VALIDITY_STATE_TOO_SHORT, IsTooShort());
+}
+
+void HTMLTextAreaElement::UpdateValueMissingValidityState() {
+ SetValidityState(VALIDITY_STATE_VALUE_MISSING, IsValueMissing());
+}
+
+void HTMLTextAreaElement::UpdateBarredFromConstraintValidation() {
+ SetBarredFromConstraintValidation(
+ HasAttr(nsGkAtoms::readonly) ||
+ HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR) || IsDisabled());
+}
+
+nsresult HTMLTextAreaElement::GetValidationMessage(
+ nsAString& aValidationMessage, ValidityStateType aType) {
+ nsresult rv = NS_OK;
+
+ switch (aType) {
+ case VALIDITY_STATE_TOO_LONG: {
+ nsAutoString message;
+ int32_t maxLength = MaxLength();
+ int32_t textLength = GetTextLength();
+ nsAutoString strMaxLength;
+ nsAutoString strTextLength;
+
+ strMaxLength.AppendInt(maxLength);
+ strTextLength.AppendInt(textLength);
+
+ rv = nsContentUtils::FormatMaybeLocalizedString(
+ message, nsContentUtils::eDOM_PROPERTIES, "FormValidationTextTooLong",
+ OwnerDoc(), strMaxLength, strTextLength);
+ aValidationMessage = message;
+ } break;
+ case VALIDITY_STATE_TOO_SHORT: {
+ nsAutoString message;
+ int32_t minLength = MinLength();
+ int32_t textLength = GetTextLength();
+ nsAutoString strMinLength;
+ nsAutoString strTextLength;
+
+ strMinLength.AppendInt(minLength);
+ strTextLength.AppendInt(textLength);
+
+ rv = nsContentUtils::FormatMaybeLocalizedString(
+ message, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationTextTooShort", OwnerDoc(), strMinLength,
+ strTextLength);
+ aValidationMessage = message;
+ } break;
+ case VALIDITY_STATE_VALUE_MISSING: {
+ nsAutoString message;
+ rv = nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationValueMissing",
+ OwnerDoc(), message);
+ aValidationMessage = message;
+ } break;
+ default:
+ rv =
+ ConstraintValidation::GetValidationMessage(aValidationMessage, aType);
+ }
+
+ return rv;
+}
+
+bool HTMLTextAreaElement::IsSingleLineTextControl() const { return false; }
+
+bool HTMLTextAreaElement::IsTextArea() const { return true; }
+
+bool HTMLTextAreaElement::IsPasswordTextControl() const { return false; }
+
+int32_t HTMLTextAreaElement::GetCols() { return Cols(); }
+
+int32_t HTMLTextAreaElement::GetWrapCols() {
+ nsHTMLTextWrap wrapProp;
+ TextControlElement::GetWrapPropertyEnum(this, wrapProp);
+ if (wrapProp == TextControlElement::eHTMLTextWrap_Off) {
+ // do not wrap when wrap=off
+ return 0;
+ }
+
+ // Otherwise we just wrap at the given number of columns
+ return GetCols();
+}
+
+int32_t HTMLTextAreaElement::GetRows() {
+ const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::rows);
+ if (attr && attr->Type() == nsAttrValue::eInteger) {
+ int32_t rows = attr->GetIntegerValue();
+ return (rows <= 0) ? DEFAULT_ROWS_TEXTAREA : rows;
+ }
+
+ return DEFAULT_ROWS_TEXTAREA;
+}
+
+void HTMLTextAreaElement::GetDefaultValueFromContent(nsAString& aValue, bool) {
+ GetDefaultValue(aValue, IgnoreErrors());
+}
+
+bool HTMLTextAreaElement::ValueChanged() const { return mValueChanged; }
+
+void HTMLTextAreaElement::GetTextEditorValue(nsAString& aValue) const {
+ MOZ_ASSERT(mState);
+ mState->GetValue(aValue, /* aIgnoreWrap = */ true, /* aForDisplay = */ true);
+}
+
+void HTMLTextAreaElement::InitializeKeyboardEventListeners() {
+ MOZ_ASSERT(mState);
+ mState->InitializeKeyboardEventListeners();
+}
+
+void HTMLTextAreaElement::UpdatePlaceholderShownState() {
+ SetStates(ElementState::PLACEHOLDER_SHOWN,
+ IsValueEmpty() && HasAttr(nsGkAtoms::placeholder));
+}
+
+void HTMLTextAreaElement::OnValueChanged(ValueChangeKind aKind,
+ bool aNewValueEmpty,
+ const nsAString* aKnownNewValue) {
+ if (aKind != ValueChangeKind::Internal) {
+ mLastValueChangeWasInteractive = aKind == ValueChangeKind::UserInteraction;
+ }
+
+ if (aNewValueEmpty != IsValueEmpty()) {
+ SetStates(ElementState::VALUE_EMPTY, aNewValueEmpty);
+ UpdatePlaceholderShownState();
+ }
+
+ // Update the validity state
+ const bool validBefore = IsValid();
+ UpdateTooLongValidityState();
+ UpdateTooShortValidityState();
+ UpdateValueMissingValidityState();
+
+ if (HasDirAuto()) {
+ SetDirectionFromValue(true, aKnownNewValue);
+ }
+
+ if (validBefore != IsValid()) {
+ UpdateValidityElementStates(true);
+ }
+}
+
+bool HTMLTextAreaElement::HasCachedSelection() {
+ MOZ_ASSERT(mState);
+ return mState->IsSelectionCached();
+}
+
+void HTMLTextAreaElement::SetUserInteracted(bool aInteracted) {
+ if (mUserInteracted == aInteracted) {
+ return;
+ }
+ mUserInteracted = aInteracted;
+ UpdateValidityElementStates(true);
+}
+
+void HTMLTextAreaElement::FieldSetDisabledChanged(bool aNotify) {
+ // This *has* to be called before UpdateBarredFromConstraintValidation and
+ // UpdateValueMissingValidityState because these two functions depend on our
+ // disabled state.
+ nsGenericHTMLFormControlElementWithState::FieldSetDisabledChanged(aNotify);
+
+ UpdateValueMissingValidityState();
+ UpdateBarredFromConstraintValidation();
+ UpdateValidityElementStates(true);
+}
+
+JSObject* HTMLTextAreaElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTextAreaElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void HTMLTextAreaElement::GetAutocomplete(DOMString& aValue) {
+ const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete);
+
+ mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute(
+ attributeVal, aValue, mAutocompleteAttrState);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTextAreaElement.h b/dom/html/HTMLTextAreaElement.h
new file mode 100644
index 0000000000..ac3eb8bbf5
--- /dev/null
+++ b/dom/html/HTMLTextAreaElement.h
@@ -0,0 +1,382 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLTextAreaElement_h
+#define mozilla_dom_HTMLTextAreaElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/TextControlElement.h"
+#include "mozilla/TextControlState.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/dom/ConstraintValidation.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "mozilla/dom/HTMLInputElementBinding.h"
+#include "nsIControllers.h"
+#include "nsCOMPtr.h"
+#include "nsGenericHTMLElement.h"
+#include "nsStubMutationObserver.h"
+#include "nsGkAtoms.h"
+
+class nsIControllers;
+class nsPresContext;
+
+namespace mozilla {
+
+class EventChainPostVisitor;
+class EventChainPreVisitor;
+class PresState;
+
+namespace dom {
+
+class FormData;
+
+class HTMLTextAreaElement final : public TextControlElement,
+ public nsStubMutationObserver,
+ public ConstraintValidation {
+ public:
+ using ConstraintValidation::GetValidationMessage;
+ using ValueSetterOption = TextControlState::ValueSetterOption;
+ using ValueSetterOptions = TextControlState::ValueSetterOptions;
+
+ explicit HTMLTextAreaElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser = NOT_FROM_PARSER);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTextAreaElement, textarea)
+
+ int32_t TabIndexDefault() override;
+
+ // Element
+ bool IsInteractiveHTMLContent() const override { return true; }
+
+ // nsGenericHTMLElement
+ bool IsDisabledForEvents(WidgetEvent* aEvent) override;
+
+ // nsGenericHTMLFormElement
+ void SaveState() override;
+
+ // FIXME: Shouldn't be a CAN_RUN_SCRIPT_BOUNDARY probably?
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool RestoreState(PresState*) override;
+
+ // nsIFormControl
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD Reset() override;
+ NS_IMETHOD SubmitNamesValues(FormData* aFormData) override;
+
+ void FieldSetDisabledChanged(bool aNotify) override;
+
+ void SetLastValueChangeWasInteractive(bool);
+
+ // TextControlElement
+ bool IsSingleLineTextControlOrTextArea() const override { return true; }
+ void SetValueChanged(bool aValueChanged) override;
+ bool IsSingleLineTextControl() const override;
+ bool IsTextArea() const override;
+ bool IsPasswordTextControl() const override;
+ int32_t GetCols() override;
+ int32_t GetWrapCols() override;
+ int32_t GetRows() override;
+ void GetDefaultValueFromContent(nsAString& aValue, bool aForDisplay) override;
+ bool ValueChanged() const override;
+ void GetTextEditorValue(nsAString& aValue) const override;
+ MOZ_CAN_RUN_SCRIPT TextEditor* GetTextEditor() override;
+ TextEditor* GetTextEditorWithoutCreation() const override;
+ nsISelectionController* GetSelectionController() override;
+ nsFrameSelection* GetConstFrameSelection() override;
+ TextControlState* GetTextControlState() const override { return mState; }
+ nsresult BindToFrame(nsTextControlFrame* aFrame) override;
+ MOZ_CAN_RUN_SCRIPT void UnbindFromFrame(nsTextControlFrame* aFrame) override;
+ MOZ_CAN_RUN_SCRIPT nsresult CreateEditor() override;
+ void SetPreviewValue(const nsAString& aValue) override;
+ void GetPreviewValue(nsAString& aValue) override;
+ void EnablePreview() override;
+ bool IsPreviewEnabled() override;
+ void InitializeKeyboardEventListeners() override;
+ void UpdatePlaceholderShownState();
+ void OnValueChanged(ValueChangeKind, bool aNewValueEmpty,
+ const nsAString* aKnownNewValue) override;
+ void GetValueFromSetRangeText(nsAString& aValue) override;
+ MOZ_CAN_RUN_SCRIPT nsresult
+ SetValueFromSetRangeText(const nsAString& aValue) override;
+ bool HasCachedSelection() override;
+
+ // nsIContent
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+ nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute,
+ int32_t aModType) const override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+
+ void GetEventTargetParent(EventChainPreVisitor& aVisitor) override;
+ nsresult PreHandleEvent(EventChainVisitor& aVisitor) override;
+ nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
+
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+
+ void DoneAddingChildren(bool aHaveNotified) override;
+
+ nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsresult CopyInnerTo(Element* aDest);
+
+ /**
+ * Called when an attribute is about to be changed
+ */
+ void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+
+ // nsIMutationObserver
+ NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTextAreaElement,
+ TextControlElement)
+
+ // nsIConstraintValidation
+ bool IsTooLong();
+ bool IsTooShort();
+ bool IsValueMissing() const;
+ void UpdateTooLongValidityState();
+ void UpdateTooShortValidityState();
+ void UpdateValueMissingValidityState();
+ void UpdateBarredFromConstraintValidation();
+
+ // ConstraintValidation
+ nsresult GetValidationMessage(nsAString& aValidationMessage,
+ ValidityStateType aType) override;
+
+ // Web IDL binding methods
+ void GetAutocomplete(DOMString& aValue);
+ void SetAutocomplete(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::autocomplete, aValue, aRv);
+ }
+ uint32_t Cols() { return GetUnsignedIntAttr(nsGkAtoms::cols, DEFAULT_COLS); }
+ void SetCols(uint32_t aCols, ErrorResult& aError) {
+ uint32_t cols = aCols ? aCols : DEFAULT_COLS;
+ SetUnsignedIntAttr(nsGkAtoms::cols, cols, DEFAULT_COLS, aError);
+ }
+ void GetDirName(nsAString& aValue) {
+ GetHTMLAttr(nsGkAtoms::dirname, aValue);
+ }
+ void SetDirName(const nsAString& aValue, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::dirname, aValue, aError);
+ }
+ bool Disabled() { return GetBoolAttr(nsGkAtoms::disabled); }
+ void SetDisabled(bool aDisabled, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::disabled, aDisabled, aError);
+ }
+ // nsGenericHTMLFormControlElementWithState::GetForm is fine
+ using nsGenericHTMLFormControlElementWithState::GetForm;
+ int32_t MaxLength() const { return GetIntAttr(nsGkAtoms::maxlength, -1); }
+ int32_t UsedMaxLength() const final { return MaxLength(); }
+ void SetMaxLength(int32_t aMaxLength, ErrorResult& aError) {
+ int32_t minLength = MinLength();
+ if (aMaxLength < 0 || (minLength >= 0 && aMaxLength < minLength)) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ } else {
+ SetHTMLIntAttr(nsGkAtoms::maxlength, aMaxLength, aError);
+ }
+ }
+ int32_t MinLength() const { return GetIntAttr(nsGkAtoms::minlength, -1); }
+ void SetMinLength(int32_t aMinLength, ErrorResult& aError) {
+ int32_t maxLength = MaxLength();
+ if (aMinLength < 0 || (maxLength >= 0 && aMinLength > maxLength)) {
+ aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ } else {
+ SetHTMLIntAttr(nsGkAtoms::minlength, aMinLength, aError);
+ }
+ }
+ void GetName(nsAString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); }
+ void SetName(const nsAString& aName, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::name, aName, aError);
+ }
+ void GetPlaceholder(nsAString& aPlaceholder) {
+ GetHTMLAttr(nsGkAtoms::placeholder, aPlaceholder);
+ }
+ void SetPlaceholder(const nsAString& aPlaceholder, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::placeholder, aPlaceholder, aError);
+ }
+ bool ReadOnly() { return GetBoolAttr(nsGkAtoms::readonly); }
+ void SetReadOnly(bool aReadOnly, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::readonly, aReadOnly, aError);
+ }
+ bool Required() const { return State().HasState(ElementState::REQUIRED); }
+
+ MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement,
+ ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement,
+ uint32_t aStart, uint32_t aEnd,
+ SelectionMode aSelectMode,
+ ErrorResult& aRv);
+
+ void SetRequired(bool aRequired, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::required, aRequired, aError);
+ }
+ uint32_t Rows() {
+ return GetUnsignedIntAttr(nsGkAtoms::rows, DEFAULT_ROWS_TEXTAREA);
+ }
+ void SetRows(uint32_t aRows, ErrorResult& aError) {
+ uint32_t rows = aRows ? aRows : DEFAULT_ROWS_TEXTAREA;
+ SetUnsignedIntAttr(nsGkAtoms::rows, rows, DEFAULT_ROWS_TEXTAREA, aError);
+ }
+ void GetWrap(nsAString& aWrap) { GetHTMLAttr(nsGkAtoms::wrap, aWrap); }
+ void SetWrap(const nsAString& aWrap, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::wrap, aWrap, aError);
+ }
+ void GetType(nsAString& aType);
+ void GetDefaultValue(nsAString& aDefaultValue, ErrorResult& aError) const;
+ void SetDefaultValue(const nsAString& aDefaultValue, ErrorResult& aError);
+ void GetValue(nsAString& aValue);
+ MOZ_CAN_RUN_SCRIPT void SetValue(const nsAString&, ErrorResult&);
+
+ uint32_t GetTextLength();
+
+ // Override SetCustomValidity so we update our state properly when it's called
+ // via bindings.
+ void SetCustomValidity(const nsAString& aError);
+
+ MOZ_CAN_RUN_SCRIPT void Select();
+ Nullable<uint32_t> GetSelectionStart(ErrorResult& aError);
+ MOZ_CAN_RUN_SCRIPT void SetSelectionStart(
+ const Nullable<uint32_t>& aSelectionStart, ErrorResult& aError);
+ Nullable<uint32_t> GetSelectionEnd(ErrorResult& aError);
+ MOZ_CAN_RUN_SCRIPT void SetSelectionEnd(
+ const Nullable<uint32_t>& aSelectionEnd, ErrorResult& aError);
+ void GetSelectionDirection(nsAString& aDirection, ErrorResult& aError);
+ MOZ_CAN_RUN_SCRIPT void SetSelectionDirection(const nsAString& aDirection,
+ ErrorResult& aError);
+ MOZ_CAN_RUN_SCRIPT void SetSelectionRange(
+ uint32_t aSelectionStart, uint32_t aSelectionEnd,
+ const Optional<nsAString>& aDirecton, ErrorResult& aError);
+ nsIControllers* GetControllers(ErrorResult& aError);
+ // XPCOM adapter function widely used throughout code, leaving it as is.
+ nsresult GetControllers(nsIControllers** aResult);
+
+ MOZ_CAN_RUN_SCRIPT nsIEditor* GetEditorForBindings();
+ bool HasEditor() const {
+ MOZ_ASSERT(mState);
+ return !!mState->GetTextEditorWithoutCreation();
+ }
+
+ bool IsInputEventTarget() const { return true; }
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void SetUserInput(
+ const nsAString& aValue, nsIPrincipal& aSubjectPrincipal);
+
+ protected:
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual ~HTMLTextAreaElement();
+
+ // get rid of the compiler warning
+ using nsGenericHTMLFormControlElementWithState::IsSingleLineTextControl;
+
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ nsCOMPtr<nsIControllers> mControllers;
+ /** https://html.spec.whatwg.org/#user-interacted */
+ bool mUserInteracted = false;
+ /** Whether or not the value has changed since its default value was given. */
+ bool mValueChanged = false;
+ /** Whether or not the last change to the value was made interactively by the
+ * user. */
+ bool mLastValueChangeWasInteractive = false;
+ /** Whether or not we are already handling select event. */
+ bool mHandlingSelect = false;
+ /** Whether or not we are done adding children (always true if not
+ created by a parser */
+ bool mDoneAddingChildren;
+ /** Whether state restoration should be inhibited in DoneAddingChildren. */
+ bool mInhibitStateRestoration;
+ /** Whether our disabled state has changed from the default **/
+ bool mDisabledChanged = false;
+ bool mIsPreviewEnabled = false;
+
+ nsContentUtils::AutocompleteAttrState mAutocompleteAttrState;
+
+ void FireChangeEventIfNeeded();
+
+ nsString mFocusedValue;
+
+ /** The state of the text editor (selection controller and the editor) **/
+ TextControlState* mState;
+
+ NS_IMETHOD SelectAll(nsPresContext* aPresContext);
+ /**
+ * Get the value, whether it is from the content or the frame.
+ * @param aValue the value [out]
+ * @param aIgnoreWrap whether to ignore the wrap attribute when getting the
+ * value. If this is true, linebreaks will not be inserted even if
+ * wrap=hard.
+ */
+ void GetValueInternal(nsAString& aValue, bool aIgnoreWrap) const;
+
+ /**
+ * Setting the value.
+ *
+ * @param aValue String to set.
+ * @param aOptions See TextControlState::ValueSetterOption.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ SetValueInternal(const nsAString& aValue, const ValueSetterOptions& aOptions);
+
+ /**
+ * Common method to call from the various mutation observer methods.
+ * aContent is a content node that's either the one that changed or its
+ * parent; we should only respond to the change if aContent is non-anonymous.
+ */
+ void ContentChanged(nsIContent* aContent);
+
+ void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal, bool aNotify) override;
+
+ void SetDirectionFromValue(bool aNotify,
+ const nsAString* aKnownValue = nullptr);
+
+ /**
+ * Get the mutable state of the element.
+ */
+ bool IsMutable() const;
+
+ /**
+ * Returns whether the current value is the empty string.
+ *
+ * @return whether the current value is the empty string.
+ */
+ bool IsValueEmpty() const {
+ return State().HasState(ElementState::VALUE_EMPTY);
+ }
+
+ /**
+ * A helper to get the current selection range. Will throw on the ErrorResult
+ * if we have no editor state.
+ */
+ void GetSelectionRange(uint32_t* aSelectionStart, uint32_t* aSelectionEnd,
+ ErrorResult& aRv);
+
+ void SetUserInteracted(bool) final;
+ void UpdateValidityElementStates(bool aNotify);
+
+ private:
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/html/HTMLTimeElement.cpp b/dom/html/HTMLTimeElement.cpp
new file mode 100644
index 0000000000..fc3003ce9f
--- /dev/null
+++ b/dom/html/HTMLTimeElement.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HTMLTimeElement.h"
+#include "mozilla/dom/HTMLTimeElementBinding.h"
+#include "nsGenericHTMLElement.h"
+#include "nsVariant.h"
+#include "nsGkAtoms.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Time)
+
+namespace mozilla::dom {
+
+HTMLTimeElement::HTMLTimeElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+HTMLTimeElement::~HTMLTimeElement() = default;
+
+NS_IMPL_ELEMENT_CLONE(HTMLTimeElement)
+
+JSObject* HTMLTimeElement::WrapNode(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTimeElement_Binding::Wrap(cx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTimeElement.h b/dom/html/HTMLTimeElement.h
new file mode 100644
index 0000000000..ccf1dca2a9
--- /dev/null
+++ b/dom/html/HTMLTimeElement.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLTimeElement_h
+#define mozilla_dom_HTMLTimeElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+
+namespace mozilla::dom {
+
+class HTMLTimeElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLTimeElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+ virtual ~HTMLTimeElement();
+
+ // HTMLTimeElement WebIDL
+ void GetDateTime(DOMString& aDateTime) {
+ GetHTMLAttr(nsGkAtoms::datetime, aDateTime);
+ }
+
+ void SetDateTime(const nsAString& aDateTime, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::datetime, aDateTime, aError);
+ }
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLTimeElement_h
diff --git a/dom/html/HTMLTitleElement.cpp b/dom/html/HTMLTitleElement.cpp
new file mode 100644
index 0000000000..776c76f7e5
--- /dev/null
+++ b/dom/html/HTMLTitleElement.cpp
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTitleElement.h"
+
+#include "mozilla/dom/HTMLTitleElementBinding.h"
+#include "mozilla/ErrorResult.h"
+#include "nsStyleConsts.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Title)
+
+namespace mozilla::dom {
+
+HTMLTitleElement::HTMLTitleElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ AddMutationObserver(this);
+}
+
+HTMLTitleElement::~HTMLTitleElement() = default;
+
+NS_IMPL_ISUPPORTS_INHERITED(HTMLTitleElement, nsGenericHTMLElement,
+ nsIMutationObserver)
+
+NS_IMPL_ELEMENT_CLONE(HTMLTitleElement)
+
+JSObject* HTMLTitleElement::WrapNode(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTitleElement_Binding::Wrap(cx, this, aGivenProto);
+}
+
+void HTMLTitleElement::GetText(DOMString& aText, ErrorResult& aError) const {
+ if (!nsContentUtils::GetNodeTextContent(this, false, aText, fallible)) {
+ aError = NS_ERROR_OUT_OF_MEMORY;
+ }
+}
+
+void HTMLTitleElement::SetText(const nsAString& aText, ErrorResult& aError) {
+ aError = nsContentUtils::SetNodeTextContent(this, aText, true);
+}
+
+void HTMLTitleElement::CharacterDataChanged(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {
+ SendTitleChangeEvent(false);
+}
+
+void HTMLTitleElement::ContentAppended(nsIContent* aFirstNewContent) {
+ SendTitleChangeEvent(false);
+}
+
+void HTMLTitleElement::ContentInserted(nsIContent* aChild) {
+ SendTitleChangeEvent(false);
+}
+
+void HTMLTitleElement::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ SendTitleChangeEvent(false);
+}
+
+nsresult HTMLTitleElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ // Let this fall through.
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SendTitleChangeEvent(true);
+
+ return NS_OK;
+}
+
+void HTMLTitleElement::UnbindFromTree(bool aNullParent) {
+ SendTitleChangeEvent(false);
+
+ // Let this fall through.
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+void HTMLTitleElement::DoneAddingChildren(bool aHaveNotified) {
+ if (!aHaveNotified) {
+ SendTitleChangeEvent(false);
+ }
+}
+
+void HTMLTitleElement::SendTitleChangeEvent(bool aBound) {
+ Document* doc = GetUncomposedDoc();
+ if (doc) {
+ doc->NotifyPossibleTitleChange(aBound);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTitleElement.h b/dom/html/HTMLTitleElement.h
new file mode 100644
index 0000000000..63cafa75a2
--- /dev/null
+++ b/dom/html/HTMLTitleElement.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLTITLEElement_h_
+#define mozilla_dom_HTMLTITLEElement_h_
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+#include "nsStubMutationObserver.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class HTMLTitleElement final : public nsGenericHTMLElement,
+ public nsStubMutationObserver {
+ public:
+ using Element::GetText;
+
+ explicit HTMLTitleElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // HTMLTitleElement
+ void GetText(DOMString& aText, ErrorResult& aError) const;
+ void SetText(const nsAString& aText, ErrorResult& aError);
+
+ // nsIMutationObserver
+ NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ virtual nsresult BindToTree(BindContext&, nsINode& aParent) override;
+
+ virtual void UnbindFromTree(bool aNullParent = true) override;
+
+ virtual void DoneAddingChildren(bool aHaveNotified) override;
+
+ protected:
+ virtual ~HTMLTitleElement();
+
+ JSObject* WrapNode(JSContext* cx, JS::Handle<JSObject*> aGivenProto) final;
+
+ private:
+ void SendTitleChangeEvent(bool aBound);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_HTMLTitleElement_h_
diff --git a/dom/html/HTMLTrackElement.cpp b/dom/html/HTMLTrackElement.cpp
new file mode 100644
index 0000000000..5363d3e399
--- /dev/null
+++ b/dom/html/HTMLTrackElement.cpp
@@ -0,0 +1,517 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLTrackElement.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/WebVTTListener.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/dom/HTMLTrackElementBinding.h"
+#include "mozilla/dom/HTMLUnknownElement.h"
+#include "nsAttrValueInlines.h"
+#include "nsCOMPtr.h"
+#include "nsContentPolicyUtils.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "nsIContentPolicy.h"
+#include "mozilla/dom/Document.h"
+#include "nsILoadGroup.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIScriptError.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsPrimitives.h"
+#include "nsNetUtil.h"
+#include "nsStyleConsts.h"
+#include "nsThreadUtils.h"
+#include "nsVideoFrame.h"
+
+extern mozilla::LazyLogModule gTextTrackLog;
+#define LOG(msg, ...) \
+ MOZ_LOG(gTextTrackLog, LogLevel::Verbose, \
+ ("TextTrackElement=%p, " msg, this, ##__VA_ARGS__))
+
+// Replace the usual NS_IMPL_NS_NEW_HTML_ELEMENT(Track) so
+// we can return an UnknownElement instead when pref'd off.
+nsGenericHTMLElement* NS_NewHTMLTrackElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
+ auto* nim = nodeInfo->NodeInfoManager();
+ return new (nim) mozilla::dom::HTMLTrackElement(nodeInfo.forget());
+}
+
+namespace mozilla::dom {
+
+// Map html attribute string values to TextTrackKind enums.
+static constexpr nsAttrValue::EnumTable kKindTable[] = {
+ {"subtitles", static_cast<int16_t>(TextTrackKind::Subtitles)},
+ {"captions", static_cast<int16_t>(TextTrackKind::Captions)},
+ {"descriptions", static_cast<int16_t>(TextTrackKind::Descriptions)},
+ {"chapters", static_cast<int16_t>(TextTrackKind::Chapters)},
+ {"metadata", static_cast<int16_t>(TextTrackKind::Metadata)},
+ {nullptr, 0}};
+
+// Invalid values are treated as "metadata" in ParseAttribute, but if no value
+// at all is specified, it's treated as "subtitles" in GetKind
+static constexpr const nsAttrValue::EnumTable* kKindTableInvalidValueDefault =
+ &kKindTable[4];
+
+class WindowDestroyObserver final : public nsIObserver {
+ NS_DECL_ISUPPORTS
+
+ public:
+ explicit WindowDestroyObserver(HTMLTrackElement* aElement, uint64_t aWinID)
+ : mTrackElement(aElement), mInnerID(aWinID) {
+ RegisterWindowDestroyObserver();
+ }
+ void RegisterWindowDestroyObserver() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, "inner-window-destroyed", false);
+ }
+ }
+ void UnRegisterWindowDestroyObserver() {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "inner-window-destroyed");
+ }
+ mTrackElement = nullptr;
+ }
+ NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (strcmp(aTopic, "inner-window-destroyed") == 0) {
+ nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
+ NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);
+ uint64_t innerID;
+ nsresult rv = wrapper->GetData(&innerID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (innerID == mInnerID) {
+ if (mTrackElement) {
+ mTrackElement->CancelChannelAndListener();
+ }
+ UnRegisterWindowDestroyObserver();
+ }
+ }
+ return NS_OK;
+ }
+
+ private:
+ ~WindowDestroyObserver() = default;
+
+ HTMLTrackElement* mTrackElement;
+ uint64_t mInnerID;
+};
+NS_IMPL_ISUPPORTS(WindowDestroyObserver, nsIObserver);
+
+/** HTMLTrackElement */
+HTMLTrackElement::HTMLTrackElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)),
+ mLoadResourceDispatched(false),
+ mWindowDestroyObserver(nullptr) {
+ nsISupports* parentObject = OwnerDoc()->GetParentObject();
+ NS_ENSURE_TRUE_VOID(parentObject);
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject);
+ if (window) {
+ mWindowDestroyObserver =
+ new WindowDestroyObserver(this, window->WindowID());
+ }
+}
+
+HTMLTrackElement::~HTMLTrackElement() {
+ if (mWindowDestroyObserver) {
+ mWindowDestroyObserver->UnRegisterWindowDestroyObserver();
+ }
+ CancelChannelAndListener();
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLTrackElement)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTrackElement, nsGenericHTMLElement,
+ mTrack, mMediaParent, mListener)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTrackElement,
+ nsGenericHTMLElement)
+
+void HTMLTrackElement::GetKind(DOMString& aKind) const {
+ GetEnumAttr(nsGkAtoms::kind, kKindTable[0].tag, aKind);
+}
+
+void HTMLTrackElement::OnChannelRedirect(nsIChannel* aChannel,
+ nsIChannel* aNewChannel,
+ uint32_t aFlags) {
+ NS_ASSERTION(aChannel == mChannel, "Channels should match!");
+ mChannel = aNewChannel;
+}
+
+JSObject* HTMLTrackElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLTrackElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+TextTrack* HTMLTrackElement::GetTrack() {
+ if (!mTrack) {
+ CreateTextTrack();
+ }
+ return mTrack;
+}
+
+void HTMLTrackElement::CreateTextTrack() {
+ nsISupports* parentObject = OwnerDoc()->GetParentObject();
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject);
+ if (!parentObject) {
+ nsContentUtils::ReportToConsole(
+ nsIScriptError::errorFlag, "Media"_ns, OwnerDoc(),
+ nsContentUtils::eDOM_PROPERTIES,
+ "Using track element in non-window context");
+ return;
+ }
+
+ nsString label, srcLang;
+ GetSrclang(srcLang);
+ GetLabel(label);
+
+ TextTrackKind kind;
+ if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::kind)) {
+ kind = static_cast<TextTrackKind>(value->GetEnumValue());
+ } else {
+ kind = TextTrackKind::Subtitles;
+ }
+
+ MOZ_ASSERT(!mTrack, "No need to recreate a text track!");
+ mTrack =
+ new TextTrack(window, kind, label, srcLang, TextTrackMode::Disabled,
+ TextTrackReadyState::NotLoaded, TextTrackSource::Track);
+ mTrack->SetTrackElement(this);
+}
+
+bool HTMLTrackElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::kind) {
+ // Case-insensitive lookup, with the first element as the default.
+ return aResult.ParseEnumValue(aValue, kKindTable, false,
+ kKindTableInvalidValueDefault);
+ }
+
+ // Otherwise call the generic implementation.
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLTrackElement::SetSrc(const nsAString& aSrc, ErrorResult& aError) {
+ LOG("Set src=%s", NS_ConvertUTF16toUTF8(aSrc).get());
+
+ nsAutoString src;
+ if (GetAttr(nsGkAtoms::src, src) && src == aSrc) {
+ LOG("No need to reload for same src url");
+ return;
+ }
+
+ SetHTMLAttr(nsGkAtoms::src, aSrc, aError);
+ SetReadyState(TextTrackReadyState::NotLoaded);
+ if (!mMediaParent) {
+ return;
+ }
+
+ // Stop WebVTTListener.
+ mListener = nullptr;
+ if (mChannel) {
+ mChannel->CancelWithReason(NS_BINDING_ABORTED,
+ "HTMLTrackElement::SetSrc"_ns);
+ mChannel = nullptr;
+ }
+
+ MaybeDispatchLoadResource();
+}
+
+void HTMLTrackElement::MaybeClearAllCues() {
+ // Empty track's cue list whenever the track element's `src` attribute set,
+ // changed, or removed,
+ // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:attr-track-src
+ if (!mTrack) {
+ return;
+ }
+ mTrack->ClearAllCues();
+}
+
+// This function will run partial steps from `start-the-track-processing-model`
+// and finish the rest of steps in `LoadResource()` during the stable state.
+// https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model
+void HTMLTrackElement::MaybeDispatchLoadResource() {
+ MOZ_ASSERT(mTrack, "Should have already created text track!");
+
+ // step2, if the text track's text track mode is not set to one of hidden or
+ // showing, then return.
+ if (mTrack->Mode() == TextTrackMode::Disabled) {
+ LOG("Do not load resource for disable track");
+ return;
+ }
+
+ // step3, if the text track's track element does not have a media element as a
+ // parent, return.
+ if (!mMediaParent) {
+ LOG("Do not load resource for track without media element");
+ return;
+ }
+
+ if (ReadyState() == TextTrackReadyState::Loaded) {
+ LOG("Has already loaded resource");
+ return;
+ }
+
+ // step5, await a stable state and run the rest of steps.
+ if (!mLoadResourceDispatched) {
+ RefPtr<WebVTTListener> listener = new WebVTTListener(this);
+ RefPtr<Runnable> r = NewRunnableMethod<RefPtr<WebVTTListener>>(
+ "dom::HTMLTrackElement::LoadResource", this,
+ &HTMLTrackElement::LoadResource, std::move(listener));
+ nsContentUtils::RunInStableState(r.forget());
+ mLoadResourceDispatched = true;
+ }
+}
+
+void HTMLTrackElement::LoadResource(RefPtr<WebVTTListener>&& aWebVTTListener) {
+ LOG("LoadResource");
+ mLoadResourceDispatched = false;
+
+ nsAutoString src;
+ if (!GetAttr(nsGkAtoms::src, src) || src.IsEmpty()) {
+ LOG("Fail to load because no src");
+ SetReadyState(TextTrackReadyState::FailedToLoad);
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NewURIFromString(src, getter_AddRefs(uri));
+ NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv));
+ LOG("Trying to load from src=%s", NS_ConvertUTF16toUTF8(src).get());
+
+ CancelChannelAndListener();
+
+ // According to
+ // https://www.w3.org/TR/html5/embedded-content-0.html#sourcing-out-of-band-text-tracks
+ //
+ // "8: If the track element's parent is a media element then let CORS mode
+ // be the state of the parent media element's crossorigin content attribute.
+ // Otherwise, let CORS mode be No CORS."
+ //
+ CORSMode corsMode =
+ mMediaParent ? AttrValueToCORSMode(
+ mMediaParent->GetParsedAttr(nsGkAtoms::crossorigin))
+ : CORS_NONE;
+
+ // Determine the security flag based on corsMode.
+ nsSecurityFlags secFlags;
+ if (CORS_NONE == corsMode) {
+ // Same-origin is required for track element.
+ secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT;
+ } else {
+ secFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
+ if (CORS_ANONYMOUS == corsMode) {
+ secFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
+ } else if (CORS_USE_CREDENTIALS == corsMode) {
+ secFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+ } else {
+ NS_WARNING("Unknown CORS mode.");
+ secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT;
+ }
+ }
+
+ mListener = std::move(aWebVTTListener);
+ // This will do 6. Set the text track readiness state to loading.
+ rv = mListener->LoadResource();
+ NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv));
+
+ Document* doc = OwnerDoc();
+ if (!doc) {
+ return;
+ }
+
+ // 9. End the synchronous section, continuing the remaining steps in parallel.
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "dom::HTMLTrackElement::LoadResource",
+ [self = RefPtr<HTMLTrackElement>(this), this, uri, secFlags]() {
+ if (!mListener) {
+ // Shutdown got called, abort.
+ return;
+ }
+ nsCOMPtr<nsIChannel> channel;
+ nsCOMPtr<nsILoadGroup> loadGroup = OwnerDoc()->GetDocumentLoadGroup();
+ nsresult rv = NS_NewChannel(getter_AddRefs(channel), uri,
+ static_cast<Element*>(this), secFlags,
+ nsIContentPolicy::TYPE_INTERNAL_TRACK,
+ nullptr, // PerformanceStorage
+ loadGroup);
+
+ if (NS_FAILED(rv)) {
+ LOG("create channel failed.");
+ SetReadyState(TextTrackReadyState::FailedToLoad);
+ return;
+ }
+
+ channel->SetNotificationCallbacks(mListener);
+
+ LOG("opening webvtt channel");
+ rv = channel->AsyncOpen(mListener);
+
+ if (NS_FAILED(rv)) {
+ SetReadyState(TextTrackReadyState::FailedToLoad);
+ return;
+ }
+ mChannel = channel;
+ });
+ doc->Dispatch(runnable.forget());
+}
+
+nsresult HTMLTrackElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ LOG("Track Element bound to tree.");
+ auto* parent = HTMLMediaElement::FromNode(aParent);
+ if (!parent) {
+ return NS_OK;
+ }
+
+ // Store our parent so we can look up its frame for display.
+ if (!mMediaParent) {
+ mMediaParent = parent;
+
+ // TODO: separate notification for 'alternate' tracks?
+ mMediaParent->NotifyAddedSource();
+ LOG("Track element sent notification to parent.");
+
+ // We may already have a TextTrack at this point if GetTrack() has already
+ // been called. This happens, for instance, if script tries to get the
+ // TextTrack before its mTrackElement has been bound to the DOM tree.
+ if (!mTrack) {
+ CreateTextTrack();
+ }
+ // As `CreateTextTrack()` might fail, so we have to check it again.
+ if (mTrack) {
+ LOG("Add text track to media parent");
+ mMediaParent->AddTextTrack(mTrack);
+ }
+ MaybeDispatchLoadResource();
+ }
+
+ return NS_OK;
+}
+
+void HTMLTrackElement::UnbindFromTree(bool aNullParent) {
+ if (mMediaParent && aNullParent) {
+ // mTrack can be null if HTMLTrackElement::LoadResource has never been
+ // called.
+ if (mTrack) {
+ mMediaParent->RemoveTextTrack(mTrack);
+ mMediaParent->UpdateReadyState();
+ }
+ mMediaParent = nullptr;
+ }
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+TextTrackReadyState HTMLTrackElement::ReadyState() const {
+ if (!mTrack) {
+ return TextTrackReadyState::NotLoaded;
+ }
+
+ return mTrack->ReadyState();
+}
+
+void HTMLTrackElement::SetReadyState(TextTrackReadyState aReadyState) {
+ if (ReadyState() == aReadyState) {
+ return;
+ }
+
+ if (mTrack) {
+ switch (aReadyState) {
+ case TextTrackReadyState::Loaded:
+ LOG("dispatch 'load' event");
+ DispatchTrackRunnable(u"load"_ns);
+ break;
+ case TextTrackReadyState::FailedToLoad:
+ LOG("dispatch 'error' event");
+ DispatchTrackRunnable(u"error"_ns);
+ break;
+ default:
+ break;
+ }
+ mTrack->SetReadyState(aReadyState);
+ }
+}
+
+void HTMLTrackElement::DispatchTrackRunnable(const nsString& aEventName) {
+ Document* doc = OwnerDoc();
+ if (!doc) {
+ return;
+ }
+ nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<const nsString>(
+ "dom::HTMLTrackElement::DispatchTrustedEvent", this,
+ &HTMLTrackElement::DispatchTrustedEvent, aEventName);
+ doc->Dispatch(runnable.forget());
+}
+
+void HTMLTrackElement::DispatchTrustedEvent(const nsAString& aName) {
+ Document* doc = OwnerDoc();
+ if (!doc) {
+ return;
+ }
+ nsContentUtils::DispatchTrustedEvent(doc, this, aName, CanBubble::eNo,
+ Cancelable::eNo);
+}
+
+void HTMLTrackElement::CancelChannelAndListener() {
+ if (mChannel) {
+ mChannel->CancelWithReason(NS_BINDING_ABORTED,
+ "HTMLTrackElement::CancelChannelAndListener"_ns);
+ mChannel->SetNotificationCallbacks(nullptr);
+ mChannel = nullptr;
+ }
+
+ if (mListener) {
+ mListener->Cancel();
+ mListener = nullptr;
+ }
+}
+
+void HTMLTrackElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::src) {
+ MaybeClearAllCues();
+ // In spec, `start the track processing model` step10, while fetching is
+ // ongoing, if the track URL changes, then we have to set the `FailedToLoad`
+ // state.
+ // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:text-track-failed-to-load-3
+ if (ReadyState() == TextTrackReadyState::Loading && aValue != aOldValue) {
+ SetReadyState(TextTrackReadyState::FailedToLoad);
+ }
+ }
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void HTMLTrackElement::DispatchTestEvent(const nsAString& aName) {
+ if (!StaticPrefs::media_webvtt_testing_events()) {
+ return;
+ }
+ DispatchTrustedEvent(aName);
+}
+
+#undef LOG
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLTrackElement.h b/dom/html/HTMLTrackElement.h
new file mode 100644
index 0000000000..20a239778d
--- /dev/null
+++ b/dom/html/HTMLTrackElement.h
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLTrackElement_h
+#define mozilla_dom_HTMLTrackElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/TextTrack.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+
+class nsIContent;
+
+namespace mozilla::dom {
+
+class WebVTTListener;
+class WindowDestroyObserver;
+enum class TextTrackReadyState : uint8_t;
+
+class HTMLTrackElement final : public nsGenericHTMLElement {
+ public:
+ explicit HTMLTrackElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTrackElement,
+ nsGenericHTMLElement)
+
+ // HTMLTrackElement WebIDL
+ void GetKind(DOMString& aKind) const;
+ void SetKind(const nsAString& aKind, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::kind, aKind, aError);
+ }
+
+ void GetSrc(DOMString& aSrc) const { GetHTMLURIAttr(nsGkAtoms::src, aSrc); }
+
+ void SetSrc(const nsAString& aSrc, ErrorResult& aError);
+
+ void GetSrclang(DOMString& aSrclang) const {
+ GetHTMLAttr(nsGkAtoms::srclang, aSrclang);
+ }
+ void GetSrclang(nsAString& aSrclang) const {
+ GetHTMLAttr(nsGkAtoms::srclang, aSrclang);
+ }
+ void SetSrclang(const nsAString& aSrclang, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::srclang, aSrclang, aError);
+ }
+
+ void GetLabel(DOMString& aLabel) const {
+ GetHTMLAttr(nsGkAtoms::label, aLabel);
+ }
+ void GetLabel(nsAString& aLabel) const {
+ GetHTMLAttr(nsGkAtoms::label, aLabel);
+ }
+ void SetLabel(const nsAString& aLabel, ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::label, aLabel, aError);
+ }
+
+ bool Default() const { return GetBoolAttr(nsGkAtoms::_default); }
+ void SetDefault(bool aDefault, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::_default, aDefault, aError);
+ }
+
+ TextTrackReadyState ReadyState() const;
+ uint16_t ReadyStateForBindings() const {
+ return static_cast<uint16_t>(ReadyState());
+ }
+ void SetReadyState(TextTrackReadyState);
+
+ TextTrack* GetTrack();
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ // Override ParseAttribute() to convert kind strings to enum values.
+ virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ // Override BindToTree() so that we can trigger a load when we become
+ // the child of a media element.
+ virtual nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ virtual void UnbindFromTree(bool aNullParent) override;
+
+ virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+
+ void DispatchTrackRunnable(const nsString& aEventName);
+ void DispatchTrustedEvent(const nsAString& aName);
+ void DispatchTestEvent(const nsAString& aName);
+
+ void CancelChannelAndListener();
+
+ // Only load resource for the non-disabled track with media parent.
+ void MaybeDispatchLoadResource();
+
+ protected:
+ virtual ~HTMLTrackElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ void OnChannelRedirect(nsIChannel* aChannel, nsIChannel* aNewChannel,
+ uint32_t aFlags);
+
+ friend class TextTrackCue;
+ friend class WebVTTListener;
+
+ RefPtr<TextTrack> mTrack;
+ nsCOMPtr<nsIChannel> mChannel;
+ RefPtr<HTMLMediaElement> mMediaParent;
+ RefPtr<WebVTTListener> mListener;
+
+ void CreateTextTrack();
+
+ private:
+ // Open a new channel to the HTMLTrackElement's src attribute and call
+ // mListener's LoadResource().
+ void LoadResource(RefPtr<WebVTTListener>&& aWebVTTListener);
+ bool mLoadResourceDispatched;
+
+ void MaybeClearAllCues();
+
+ RefPtr<WindowDestroyObserver> mWindowDestroyObserver;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_HTMLTrackElement_h
diff --git a/dom/html/HTMLUnknownElement.cpp b/dom/html/HTMLUnknownElement.cpp
new file mode 100644
index 0000000000..875ad514e2
--- /dev/null
+++ b/dom/html/HTMLUnknownElement.cpp
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLUnknownElement.h"
+#include "mozilla/dom/HTMLElementBinding.h"
+#include "jsapi.h"
+
+NS_IMPL_NS_NEW_HTML_ELEMENT(Unknown)
+
+namespace mozilla::dom {
+
+NS_IMPL_ISUPPORTS_INHERITED(HTMLUnknownElement, nsGenericHTMLElement,
+ HTMLUnknownElement)
+
+JSObject* HTMLUnknownElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLUnknownElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_ELEMENT_CLONE(HTMLUnknownElement)
+
+} // namespace mozilla::dom
diff --git a/dom/html/HTMLUnknownElement.h b/dom/html/HTMLUnknownElement.h
new file mode 100644
index 0000000000..3bed35a4f6
--- /dev/null
+++ b/dom/html/HTMLUnknownElement.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_HTMLUnknownElement_h
+#define mozilla_dom_HTMLUnknownElement_h
+
+#include "mozilla/Attributes.h"
+#include "nsGenericHTMLElement.h"
+
+namespace mozilla::dom {
+
+#define NS_HTMLUNKNOWNELEMENT_IID \
+ { \
+ 0xc09e665b, 0x3876, 0x40dd, { \
+ 0x85, 0x28, 0x44, 0xc2, 0x3f, 0xd4, 0x58, 0xf2 \
+ } \
+ }
+
+class HTMLUnknownElement final : public nsGenericHTMLElement {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTMLUNKNOWNELEMENT_IID)
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ explicit HTMLUnknownElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {}
+
+ virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+
+ protected:
+ virtual ~HTMLUnknownElement() = default;
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(HTMLUnknownElement, NS_HTMLUNKNOWNELEMENT_IID)
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HTMLUnknownElement_h */
diff --git a/dom/html/HTMLVideoElement.cpp b/dom/html/HTMLVideoElement.cpp
new file mode 100644
index 0000000000..effb21706c
--- /dev/null
+++ b/dom/html/HTMLVideoElement.cpp
@@ -0,0 +1,681 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLVideoElement.h"
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/HTMLVideoElementBinding.h"
+#include "nsGenericHTMLElement.h"
+#include "nsGkAtoms.h"
+#include "nsSize.h"
+#include "nsError.h"
+#include "nsIHttpChannel.h"
+#include "nsNodeInfoManager.h"
+#include "plbase64.h"
+#include "prlock.h"
+#include "nsRFPService.h"
+#include "nsThreadUtils.h"
+#include "ImageContainer.h"
+#include "VideoFrameContainer.h"
+#include "VideoOutput.h"
+
+#include "FrameStatistics.h"
+#include "MediaError.h"
+#include "MediaDecoder.h"
+#include "MediaDecoderStateMachine.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/WakeLock.h"
+#include "mozilla/dom/power/PowerManagerService.h"
+#include "mozilla/dom/Performance.h"
+#include "mozilla/dom/TimeRanges.h"
+#include "mozilla/dom/VideoPlaybackQuality.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Unused.h"
+
+#include <algorithm>
+#include <limits>
+
+extern mozilla::LazyLogModule gMediaElementLog;
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaElementLog, LogLevel::Debug, \
+ ("HTMLVideoElement=%p, " msg, this, ##__VA_ARGS__))
+
+nsGenericHTMLElement* NS_NewHTMLVideoElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
+ auto* nim = nodeInfo->NodeInfoManager();
+ mozilla::dom::HTMLVideoElement* element =
+ new (nim) mozilla::dom::HTMLVideoElement(nodeInfo.forget());
+ element->Init();
+ return element;
+}
+
+namespace mozilla::dom {
+
+nsresult HTMLVideoElement::Clone(mozilla::dom::NodeInfo* aNodeInfo,
+ nsINode** aResult) const {
+ *aResult = nullptr;
+ RefPtr<mozilla::dom::NodeInfo> ni(aNodeInfo);
+ auto* nim = ni->NodeInfoManager();
+ HTMLVideoElement* it = new (nim) HTMLVideoElement(ni.forget());
+ it->Init();
+ nsCOMPtr<nsINode> kungFuDeathGrip = it;
+ nsresult rv = const_cast<HTMLVideoElement*>(this)->CopyInnerTo(it);
+ if (NS_SUCCEEDED(rv)) {
+ kungFuDeathGrip.swap(*aResult);
+ }
+ return rv;
+}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLVideoElement,
+ HTMLMediaElement)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLVideoElement)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HTMLVideoElement)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualCloneTarget)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualCloneTargetPromise)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualCloneSource)
+ tmp->mSecondaryVideoOutput = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(HTMLMediaElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLVideoElement,
+ HTMLMediaElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualCloneTarget)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualCloneTargetPromise)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualCloneSource)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+HTMLVideoElement::HTMLVideoElement(already_AddRefed<NodeInfo>&& aNodeInfo)
+ : HTMLMediaElement(std::move(aNodeInfo)),
+ mVideoWatchManager(this, AbstractThread::MainThread()) {
+ DecoderDoctorLogger::LogConstruction(this);
+}
+
+HTMLVideoElement::~HTMLVideoElement() {
+ mVideoWatchManager.Shutdown();
+ DecoderDoctorLogger::LogDestruction(this);
+}
+
+void HTMLVideoElement::UpdateMediaSize(const nsIntSize& aSize) {
+ HTMLMediaElement::UpdateMediaSize(aSize);
+ // If we have a clone target, we should update its size as well.
+ if (mVisualCloneTarget) {
+ Maybe<nsIntSize> newSize = Some(aSize);
+ mVisualCloneTarget->Invalidate(ImageSizeChanged::Yes, newSize,
+ ForceInvalidate::Yes);
+ }
+}
+
+Maybe<CSSIntSize> HTMLVideoElement::GetVideoSize() const {
+ if (!mMediaInfo.HasVideo()) {
+ return Nothing();
+ }
+
+ if (mDisableVideo) {
+ return Nothing();
+ }
+
+ CSSIntSize size;
+ switch (mMediaInfo.mVideo.mRotation) {
+ case VideoRotation::kDegree_90:
+ case VideoRotation::kDegree_270: {
+ size.width = mMediaInfo.mVideo.mDisplay.height;
+ size.height = mMediaInfo.mVideo.mDisplay.width;
+ break;
+ }
+ case VideoRotation::kDegree_0:
+ case VideoRotation::kDegree_180:
+ default: {
+ size.height = mMediaInfo.mVideo.mDisplay.height;
+ size.width = mMediaInfo.mVideo.mDisplay.width;
+ break;
+ }
+ }
+ return Some(size);
+}
+
+void HTMLVideoElement::Invalidate(ImageSizeChanged aImageSizeChanged,
+ const Maybe<nsIntSize>& aNewIntrinsicSize,
+ ForceInvalidate aForceInvalidate) {
+ HTMLMediaElement::Invalidate(aImageSizeChanged, aNewIntrinsicSize,
+ aForceInvalidate);
+ if (mVisualCloneTarget) {
+ VideoFrameContainer* container =
+ mVisualCloneTarget->GetVideoFrameContainer();
+ if (container) {
+ container->Invalidate();
+ }
+ }
+}
+
+bool HTMLVideoElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height) {
+ return aResult.ParseHTMLDimension(aValue);
+ }
+
+ return HTMLMediaElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ aMaybeScriptedPrincipal, aResult);
+}
+
+void HTMLVideoElement::MapAttributesIntoRule(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapImageSizeAttributesInto(aBuilder, MapAspectRatio::Yes);
+ MapCommonAttributesInto(aBuilder);
+}
+
+NS_IMETHODIMP_(bool)
+HTMLVideoElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry attributes[] = {
+ {nsGkAtoms::width}, {nsGkAtoms::height}, {nullptr}};
+
+ static const MappedAttributeEntry* const map[] = {attributes,
+ sCommonAttributeMap};
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc HTMLVideoElement::GetAttributeMappingFunction()
+ const {
+ return &MapAttributesIntoRule;
+}
+
+void HTMLVideoElement::UnbindFromTree(bool aNullParent) {
+ if (mVisualCloneSource) {
+ mVisualCloneSource->EndCloningVisually();
+ } else if (mVisualCloneTarget) {
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *this, u"MozStopPictureInPicture"_ns, CanBubble::eNo,
+ ChromeOnlyDispatch::eYes);
+ EndCloningVisually();
+ }
+
+ HTMLMediaElement::UnbindFromTree(aNullParent);
+}
+
+nsresult HTMLVideoElement::SetAcceptHeader(nsIHttpChannel* aChannel) {
+ nsAutoCString value(
+ "video/webm,"
+ "video/ogg,"
+ "video/*;q=0.9,"
+ "application/ogg;q=0.7,"
+ "audio/*;q=0.6,*/*;q=0.5");
+
+ return aChannel->SetRequestHeader("Accept"_ns, value, false);
+}
+
+bool HTMLVideoElement::IsInteractiveHTMLContent() const {
+ return HasAttr(nsGkAtoms::controls) ||
+ HTMLMediaElement::IsInteractiveHTMLContent();
+}
+
+gfx::IntSize HTMLVideoElement::GetVideoIntrinsicDimensions() {
+ const auto& sz = mMediaInfo.mVideo.mDisplay;
+
+ // Prefer the size of the container as it's more up to date.
+ return ToMaybeRef(mVideoFrameContainer.get())
+ .map([&](auto& aVFC) { return aVFC.CurrentIntrinsicSize().valueOr(sz); })
+ .valueOr(sz);
+}
+
+uint32_t HTMLVideoElement::VideoWidth() {
+ if (!HasVideo()) {
+ return 0;
+ }
+ gfx::IntSize size = GetVideoIntrinsicDimensions();
+ if (mMediaInfo.mVideo.mRotation == VideoRotation::kDegree_90 ||
+ mMediaInfo.mVideo.mRotation == VideoRotation::kDegree_270) {
+ return size.height;
+ }
+ return size.width;
+}
+
+uint32_t HTMLVideoElement::VideoHeight() {
+ if (!HasVideo()) {
+ return 0;
+ }
+ gfx::IntSize size = GetVideoIntrinsicDimensions();
+ if (mMediaInfo.mVideo.mRotation == VideoRotation::kDegree_90 ||
+ mMediaInfo.mVideo.mRotation == VideoRotation::kDegree_270) {
+ return size.width;
+ }
+ return size.height;
+}
+
+uint32_t HTMLVideoElement::MozParsedFrames() const {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
+ if (!IsVideoStatsEnabled()) {
+ return 0;
+ }
+
+ if (OwnerDoc()->ShouldResistFingerprinting(
+ RFPTarget::VideoElementMozFrames)) {
+ return nsRFPService::GetSpoofedTotalFrames(TotalPlayTime());
+ }
+
+ return mDecoder ? mDecoder->GetFrameStatistics().GetParsedFrames() : 0;
+}
+
+uint32_t HTMLVideoElement::MozDecodedFrames() const {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
+ if (!IsVideoStatsEnabled()) {
+ return 0;
+ }
+
+ if (OwnerDoc()->ShouldResistFingerprinting(
+ RFPTarget::VideoElementMozFrames)) {
+ return nsRFPService::GetSpoofedTotalFrames(TotalPlayTime());
+ }
+
+ return mDecoder ? mDecoder->GetFrameStatistics().GetDecodedFrames() : 0;
+}
+
+uint32_t HTMLVideoElement::MozPresentedFrames() {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
+ if (!IsVideoStatsEnabled()) {
+ return 0;
+ }
+
+ if (OwnerDoc()->ShouldResistFingerprinting(
+ RFPTarget::VideoElementMozFrames)) {
+ return nsRFPService::GetSpoofedPresentedFrames(TotalPlayTime(),
+ VideoWidth(), VideoHeight());
+ }
+
+ return mDecoder ? mDecoder->GetFrameStatistics().GetPresentedFrames() : 0;
+}
+
+uint32_t HTMLVideoElement::MozPaintedFrames() {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
+ if (!IsVideoStatsEnabled()) {
+ return 0;
+ }
+
+ if (OwnerDoc()->ShouldResistFingerprinting(
+ RFPTarget::VideoElementMozFrames)) {
+ return nsRFPService::GetSpoofedPresentedFrames(TotalPlayTime(),
+ VideoWidth(), VideoHeight());
+ }
+
+ layers::ImageContainer* container = GetImageContainer();
+ return container ? container->GetPaintCount() : 0;
+}
+
+double HTMLVideoElement::MozFrameDelay() {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
+
+ if (!IsVideoStatsEnabled() || OwnerDoc()->ShouldResistFingerprinting(
+ RFPTarget::VideoElementMozFrameDelay)) {
+ return 0.0;
+ }
+
+ VideoFrameContainer* container = GetVideoFrameContainer();
+ // Hide negative delays. Frame timing tweaks in the compositor (e.g.
+ // adding a bias value to prevent multiple dropped/duped frames when
+ // frame times are aligned with composition times) may produce apparent
+ // negative delay, but we shouldn't report that.
+ return container ? std::max(0.0, container->GetFrameDelay()) : 0.0;
+}
+
+bool HTMLVideoElement::MozHasAudio() const {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
+ return HasAudio();
+}
+
+JSObject* HTMLVideoElement::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLVideoElement_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<VideoPlaybackQuality>
+HTMLVideoElement::GetVideoPlaybackQuality() {
+ DOMHighResTimeStamp creationTime = 0;
+ uint32_t totalFrames = 0;
+ uint32_t droppedFrames = 0;
+
+ if (IsVideoStatsEnabled()) {
+ if (nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow()) {
+ Performance* perf = window->GetPerformance();
+ if (perf) {
+ creationTime = perf->Now();
+ }
+ }
+
+ if (mDecoder) {
+ if (OwnerDoc()->ShouldResistFingerprinting(
+ RFPTarget::VideoElementPlaybackQuality)) {
+ totalFrames = nsRFPService::GetSpoofedTotalFrames(TotalPlayTime());
+ droppedFrames = nsRFPService::GetSpoofedDroppedFrames(
+ TotalPlayTime(), VideoWidth(), VideoHeight());
+ } else {
+ FrameStatistics* stats = &mDecoder->GetFrameStatistics();
+ if (sizeof(totalFrames) >= sizeof(stats->GetParsedFrames())) {
+ totalFrames = stats->GetTotalFrames();
+ droppedFrames = stats->GetDroppedFrames();
+ } else {
+ uint64_t total = stats->GetTotalFrames();
+ const auto maxNumber = std::numeric_limits<uint32_t>::max();
+ if (total <= maxNumber) {
+ totalFrames = uint32_t(total);
+ droppedFrames = uint32_t(stats->GetDroppedFrames());
+ } else {
+ // Too big number(s) -> Resize everything to fit in 32 bits.
+ double ratio = double(maxNumber) / double(total);
+ totalFrames = maxNumber; // === total * ratio
+ droppedFrames = uint32_t(double(stats->GetDroppedFrames()) * ratio);
+ }
+ }
+ }
+ if (!StaticPrefs::media_video_dropped_frame_stats_enabled()) {
+ droppedFrames = 0;
+ }
+ }
+ }
+
+ RefPtr<VideoPlaybackQuality> playbackQuality =
+ new VideoPlaybackQuality(this, creationTime, totalFrames, droppedFrames);
+ return playbackQuality.forget();
+}
+
+void HTMLVideoElement::WakeLockRelease() {
+ HTMLMediaElement::WakeLockRelease();
+ ReleaseVideoWakeLockIfExists();
+}
+
+void HTMLVideoElement::UpdateWakeLock() {
+ HTMLMediaElement::UpdateWakeLock();
+ if (!mPaused) {
+ CreateVideoWakeLockIfNeeded();
+ } else {
+ ReleaseVideoWakeLockIfExists();
+ }
+}
+
+bool HTMLVideoElement::ShouldCreateVideoWakeLock() const {
+ if (!StaticPrefs::media_video_wakelock()) {
+ return false;
+ }
+ // Only request wake lock for video with audio or video from media
+ // stream, because non-stream video without audio is often used as a
+ // background image.
+ //
+ // Some web conferencing sites route audio outside the video element,
+ // and would not be detected unless we check for media stream, so do
+ // that below.
+ //
+ // Media streams generally aren't used as background images, though if
+ // they were we'd get false positives. If this is an issue, we could
+ // check for media stream AND document has audio playing (but that was
+ // tricky to do).
+ return HasVideo() && (mSrcStream || HasAudio());
+}
+
+void HTMLVideoElement::CreateVideoWakeLockIfNeeded() {
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ return;
+ }
+ if (!mScreenWakeLock && ShouldCreateVideoWakeLock()) {
+ RefPtr<power::PowerManagerService> pmService =
+ power::PowerManagerService::GetInstance();
+ NS_ENSURE_TRUE_VOID(pmService);
+
+ ErrorResult rv;
+ mScreenWakeLock = pmService->NewWakeLock(u"video-playing"_ns,
+ OwnerDoc()->GetInnerWindow(), rv);
+ }
+}
+
+void HTMLVideoElement::ReleaseVideoWakeLockIfExists() {
+ if (mScreenWakeLock) {
+ ErrorResult rv;
+ mScreenWakeLock->Unlock(rv);
+ rv.SuppressException();
+ mScreenWakeLock = nullptr;
+ return;
+ }
+}
+
+bool HTMLVideoElement::SetVisualCloneTarget(
+ RefPtr<HTMLVideoElement> aVisualCloneTarget,
+ RefPtr<Promise> aVisualCloneTargetPromise) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !aVisualCloneTarget || aVisualCloneTarget->IsInComposedDoc(),
+ "Can't set the clone target to a disconnected video "
+ "element.");
+ MOZ_DIAGNOSTIC_ASSERT(!mVisualCloneSource,
+ "Can't clone a video element that is already a clone.");
+ if (!aVisualCloneTarget ||
+ (aVisualCloneTarget->IsInComposedDoc() && !mVisualCloneSource)) {
+ mVisualCloneTarget = std::move(aVisualCloneTarget);
+ mVisualCloneTargetPromise = std::move(aVisualCloneTargetPromise);
+ return true;
+ }
+ return false;
+}
+
+bool HTMLVideoElement::SetVisualCloneSource(
+ RefPtr<HTMLVideoElement> aVisualCloneSource) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !aVisualCloneSource || aVisualCloneSource->IsInComposedDoc(),
+ "Can't set the clone source to a disconnected video "
+ "element.");
+ MOZ_DIAGNOSTIC_ASSERT(!mVisualCloneTarget,
+ "Can't clone a video element that is already a "
+ "clone.");
+ if (!aVisualCloneSource ||
+ (aVisualCloneSource->IsInComposedDoc() && !mVisualCloneTarget)) {
+ mVisualCloneSource = std::move(aVisualCloneSource);
+ return true;
+ }
+ return false;
+}
+
+/* static */
+bool HTMLVideoElement::IsVideoStatsEnabled() {
+ return StaticPrefs::media_video_stats_enabled();
+}
+
+double HTMLVideoElement::TotalPlayTime() const {
+ double total = 0.0;
+
+ if (mPlayed) {
+ uint32_t timeRangeCount = mPlayed->Length();
+
+ for (uint32_t i = 0; i < timeRangeCount; i++) {
+ double begin = mPlayed->Start(i);
+ double end = mPlayed->End(i);
+ total += end - begin;
+ }
+
+ if (mCurrentPlayRangeStart != -1.0) {
+ double now = CurrentTime();
+ if (mCurrentPlayRangeStart != now) {
+ total += now - mCurrentPlayRangeStart;
+ }
+ }
+ }
+
+ return total;
+}
+
+already_AddRefed<Promise> HTMLVideoElement::CloneElementVisually(
+ HTMLVideoElement& aTargetVideo, ErrorResult& aRv) {
+ MOZ_ASSERT(IsInComposedDoc(),
+ "Can't clone a video that's not bound to a DOM tree.");
+ MOZ_ASSERT(aTargetVideo.IsInComposedDoc(),
+ "Can't clone to a video that's not bound to a DOM tree.");
+ if (!IsInComposedDoc() || !aTargetVideo.IsInComposedDoc()) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow();
+ if (!win) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = Promise::Create(win->AsGlobal(), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // Do we already have a visual clone target? If so, shut it down.
+ if (mVisualCloneTarget) {
+ EndCloningVisually();
+ }
+
+ // If there's a poster set on the target video, clear it, otherwise
+ // it'll display over top of the cloned frames.
+ aTargetVideo.UnsetHTMLAttr(nsGkAtoms::poster, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!SetVisualCloneTarget(&aTargetVideo, promise)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ if (!aTargetVideo.SetVisualCloneSource(this)) {
+ mVisualCloneTarget = nullptr;
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ aTargetVideo.SetMediaInfo(mMediaInfo);
+
+ if (IsInComposedDoc() && !StaticPrefs::media_cloneElementVisually_testing()) {
+ NotifyUAWidgetSetupOrChange();
+ }
+
+ MaybeBeginCloningVisually();
+
+ return promise.forget();
+}
+
+void HTMLVideoElement::StopCloningElementVisually() {
+ if (mVisualCloneTarget) {
+ EndCloningVisually();
+ }
+}
+
+void HTMLVideoElement::MaybeBeginCloningVisually() {
+ if (!mVisualCloneTarget) {
+ return;
+ }
+
+ if (mDecoder) {
+ mDecoder->SetSecondaryVideoContainer(
+ mVisualCloneTarget->GetVideoFrameContainer());
+ NotifyDecoderActivityChanges();
+ UpdateMediaControlAfterPictureInPictureModeChanged();
+ } else if (mSrcStream) {
+ VideoFrameContainer* container =
+ mVisualCloneTarget->GetVideoFrameContainer();
+ if (container) {
+ mSecondaryVideoOutput = MakeRefPtr<FirstFrameVideoOutput>(
+ container, AbstractThread::MainThread());
+ mVideoWatchManager.Watch(
+ mSecondaryVideoOutput->mFirstFrameRendered,
+ &HTMLVideoElement::OnSecondaryVideoOutputFirstFrameRendered);
+ SetSecondaryMediaStreamRenderer(container, mSecondaryVideoOutput);
+ }
+ UpdateMediaControlAfterPictureInPictureModeChanged();
+ }
+}
+
+void HTMLVideoElement::EndCloningVisually() {
+ MOZ_ASSERT(mVisualCloneTarget);
+
+ if (mDecoder) {
+ mDecoder->SetSecondaryVideoContainer(nullptr);
+ NotifyDecoderActivityChanges();
+ } else if (mSrcStream) {
+ if (mSecondaryVideoOutput) {
+ mVideoWatchManager.Unwatch(
+ mSecondaryVideoOutput->mFirstFrameRendered,
+ &HTMLVideoElement::OnSecondaryVideoOutputFirstFrameRendered);
+ mSecondaryVideoOutput = nullptr;
+ }
+ SetSecondaryMediaStreamRenderer(nullptr);
+ }
+
+ Unused << mVisualCloneTarget->SetVisualCloneSource(nullptr);
+ Unused << SetVisualCloneTarget(nullptr);
+
+ UpdateMediaControlAfterPictureInPictureModeChanged();
+
+ if (IsInComposedDoc() && !StaticPrefs::media_cloneElementVisually_testing()) {
+ NotifyUAWidgetSetupOrChange();
+ }
+}
+
+void HTMLVideoElement::OnSecondaryVideoContainerInstalled(
+ const RefPtr<VideoFrameContainer>& aSecondaryContainer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT_IF(mVisualCloneTargetPromise, mVisualCloneTarget);
+ if (!mVisualCloneTargetPromise) {
+ // Clone target was unset.
+ return;
+ }
+
+ VideoFrameContainer* container = mVisualCloneTarget->GetVideoFrameContainer();
+ if (NS_WARN_IF(container != aSecondaryContainer)) {
+ // Not the right container.
+ return;
+ }
+
+ NS_DispatchToCurrentThread(NewRunnableMethod(
+ "Promise::MaybeResolveWithUndefined", mVisualCloneTargetPromise,
+ &Promise::MaybeResolveWithUndefined));
+ mVisualCloneTargetPromise = nullptr;
+}
+
+void HTMLVideoElement::OnSecondaryVideoOutputFirstFrameRendered() {
+ OnSecondaryVideoContainerInstalled(
+ mVisualCloneTarget->GetVideoFrameContainer());
+}
+
+void HTMLVideoElement::OnVisibilityChange(Visibility aNewVisibility) {
+ HTMLMediaElement::OnVisibilityChange(aNewVisibility);
+
+ // See the alternative part after step 4, but we only pause/resume invisible
+ // autoplay for non-audible video, which is different from the spec. This
+ // behavior seems aiming to reduce the power consumption without interering
+ // users, and Chrome and Safari also chose to do that only for non-audible
+ // video, so we want to match them in order to reduce webcompat issue.
+ // https://html.spec.whatwg.org/multipage/media.html#ready-states:eligible-for-autoplay-2
+ if (!HasAttr(nsGkAtoms::autoplay) || IsAudible()) {
+ return;
+ }
+
+ if (aNewVisibility == Visibility::ApproximatelyVisible && mPaused &&
+ IsEligibleForAutoplay() && AllowedToPlay()) {
+ LOG("resume invisible paused autoplay video");
+ RunAutoplay();
+ }
+
+ // We need to consider the Pip window as well, which won't reflect in the
+ // visibility event.
+ if ((aNewVisibility == Visibility::ApproximatelyNonVisible &&
+ !IsCloningElementVisually()) &&
+ mCanAutoplayFlag) {
+ LOG("pause non-audible autoplay video when it's invisible");
+ PauseInternal();
+ mCanAutoplayFlag = true;
+ return;
+ }
+}
+
+} // namespace mozilla::dom
+
+#undef LOG
diff --git a/dom/html/HTMLVideoElement.h b/dom/html/HTMLVideoElement.h
new file mode 100644
index 0000000000..eda62d759a
--- /dev/null
+++ b/dom/html/HTMLVideoElement.h
@@ -0,0 +1,203 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HTMLVideoElement_h
+#define mozilla_dom_HTMLVideoElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "Units.h"
+
+namespace mozilla {
+
+class FrameStatistics;
+
+namespace dom {
+
+class WakeLock;
+class VideoPlaybackQuality;
+
+class HTMLVideoElement final : public HTMLMediaElement {
+ class SecondaryVideoOutput;
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLVideoElement, HTMLMediaElement)
+
+ typedef mozilla::dom::NodeInfo NodeInfo;
+
+ explicit HTMLVideoElement(already_AddRefed<NodeInfo>&& aNodeInfo);
+
+ NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLVideoElement, video)
+
+ using HTMLMediaElement::GetPaused;
+
+ void Invalidate(ImageSizeChanged aImageSizeChanged,
+ const Maybe<nsIntSize>& aNewIntrinsicSize,
+ ForceInvalidate aForceInvalidate) override;
+
+ virtual bool IsVideo() const override { return true; }
+
+ virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+
+ nsresult Clone(NodeInfo*, nsINode** aResult) const override;
+
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ mozilla::Maybe<mozilla::CSSIntSize> GetVideoSize() const;
+
+ void UpdateMediaSize(const nsIntSize& aSize) override;
+
+ nsresult SetAcceptHeader(nsIHttpChannel* aChannel) override;
+
+ // Element
+ bool IsInteractiveHTMLContent() const override;
+
+ // WebIDL
+
+ uint32_t Width() const {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::width, 0);
+ }
+
+ void SetWidth(uint32_t aValue, ErrorResult& aRv) {
+ SetUnsignedIntAttr(nsGkAtoms::width, aValue, 0, aRv);
+ }
+
+ uint32_t Height() const {
+ return GetDimensionAttrAsUnsignedInt(nsGkAtoms::height, 0);
+ }
+
+ void SetHeight(uint32_t aValue, ErrorResult& aRv) {
+ SetUnsignedIntAttr(nsGkAtoms::height, aValue, 0, aRv);
+ }
+
+ uint32_t VideoWidth();
+
+ uint32_t VideoHeight();
+
+ VideoRotation RotationDegrees() const { return mMediaInfo.mVideo.mRotation; }
+
+ bool HasAlpha() const { return mMediaInfo.mVideo.HasAlpha(); }
+
+ void GetPoster(nsAString& aValue) {
+ GetURIAttr(nsGkAtoms::poster, nullptr, aValue);
+ }
+ void SetPoster(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::poster, aValue, aRv);
+ }
+
+ uint32_t MozParsedFrames() const;
+
+ uint32_t MozDecodedFrames() const;
+
+ uint32_t MozPresentedFrames();
+
+ uint32_t MozPaintedFrames();
+
+ double MozFrameDelay();
+
+ bool MozHasAudio() const;
+
+ already_AddRefed<VideoPlaybackQuality> GetVideoPlaybackQuality();
+
+ already_AddRefed<Promise> CloneElementVisually(HTMLVideoElement& aTarget,
+ ErrorResult& rv);
+
+ void StopCloningElementVisually();
+
+ bool IsCloningElementVisually() const { return !!mVisualCloneTarget; }
+
+ void OnSecondaryVideoContainerInstalled(
+ const RefPtr<VideoFrameContainer>& aSecondaryContainer) override;
+
+ void OnSecondaryVideoOutputFirstFrameRendered();
+
+ void OnVisibilityChange(Visibility aNewVisibility) override;
+
+ bool DisablePictureInPicture() const {
+ return GetBoolAttr(nsGkAtoms::disablepictureinpicture);
+ }
+
+ void SetDisablePictureInPicture(bool aValue, ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::disablepictureinpicture, aValue, aError);
+ }
+
+ protected:
+ virtual ~HTMLVideoElement();
+
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ /**
+ * We create video wakelock when the video is playing and release it when
+ * video pauses. Note, the actual platform wakelock will automatically be
+ * released when the page is in the background, so we don't need to check the
+ * video's visibility by ourselves.
+ */
+ void WakeLockRelease() override;
+ void UpdateWakeLock() override;
+
+ bool ShouldCreateVideoWakeLock() const;
+ void CreateVideoWakeLockIfNeeded();
+ void ReleaseVideoWakeLockIfExists();
+
+ gfx::IntSize GetVideoIntrinsicDimensions();
+
+ RefPtr<WakeLock> mScreenWakeLock;
+
+ WatchManager<HTMLVideoElement> mVideoWatchManager;
+
+ private:
+ bool SetVisualCloneTarget(
+ RefPtr<HTMLVideoElement> aVisualCloneTarget,
+ RefPtr<Promise> aVisualCloneTargetPromise = nullptr);
+ bool SetVisualCloneSource(RefPtr<HTMLVideoElement> aVisualCloneSource);
+
+ // For video elements, we can clone the frames being played to
+ // a secondary video element. If we're doing that, we hold a
+ // reference to the video element we're cloning to in
+ // mVisualCloneSource.
+ //
+ // Please don't set this to non-nullptr values directly - use
+ // SetVisualCloneTarget() instead.
+ RefPtr<HTMLVideoElement> mVisualCloneTarget;
+ // Set when mVisualCloneTarget is set, and resolved (and unset) when the
+ // secondary container has been applied to the underlying resource.
+ RefPtr<Promise> mVisualCloneTargetPromise;
+ // Set when beginning to clone visually and we are playing a MediaStream.
+ // This is the output wrapping the VideoFrameContainer of mVisualCloneTarget,
+ // so we can render its first frame, and resolve mVisualCloneTargetPromise as
+ // we do.
+ RefPtr<FirstFrameVideoOutput> mSecondaryVideoOutput;
+ // If this video is the clone target of another video element,
+ // then mVisualCloneSource points to that originating video
+ // element.
+ //
+ // Please don't set this to non-nullptr values directly - use
+ // SetVisualCloneTarget() instead.
+ RefPtr<HTMLVideoElement> mVisualCloneSource;
+
+ static void MapAttributesIntoRule(MappedDeclarationsBuilder&);
+
+ static bool IsVideoStatsEnabled();
+ double TotalPlayTime() const;
+
+ virtual void MaybeBeginCloningVisually() override;
+ void EndCloningVisually();
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_HTMLVideoElement_h
diff --git a/dom/html/ImageDocument.cpp b/dom/html/ImageDocument.cpp
new file mode 100644
index 0000000000..2e037284a6
--- /dev/null
+++ b/dom/html/ImageDocument.cpp
@@ -0,0 +1,795 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ImageDocument.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/ImageDocumentBinding.h"
+#include "mozilla/dom/HTMLImageElement.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "nsICSSDeclaration.h"
+#include "nsObjectLoadingContent.h"
+#include "nsRect.h"
+#include "nsIImageLoadingContent.h"
+#include "nsGenericHTMLElement.h"
+#include "nsDocShell.h"
+#include "DocumentInlines.h"
+#include "ImageBlocker.h"
+#include "nsDOMTokenList.h"
+#include "nsIDOMEventListener.h"
+#include "nsIFrame.h"
+#include "nsGkAtoms.h"
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "imgINotificationObserver.h"
+#include "nsPresContext.h"
+#include "nsIChannel.h"
+#include "nsIContentPolicy.h"
+#include "nsContentPolicyUtils.h"
+#include "nsPIDOMWindow.h"
+#include "nsError.h"
+#include "nsURILoader.h"
+#include "nsIDocShell.h"
+#include "nsIDocumentViewer.h"
+#include "nsThreadUtils.h"
+#include "nsIScrollableFrame.h"
+#include "nsContentUtils.h"
+#include "mozilla/Preferences.h"
+#include <algorithm>
+
+namespace mozilla::dom {
+
+class ImageListener : public MediaDocumentStreamListener {
+ public:
+ // NS_DECL_NSIREQUESTOBSERVER
+ // We only implement OnStartRequest; OnStopRequest is
+ // implemented by MediaDocumentStreamListener
+ NS_IMETHOD OnStartRequest(nsIRequest* aRequest) override;
+
+ explicit ImageListener(ImageDocument* aDocument);
+ virtual ~ImageListener();
+};
+
+ImageListener::ImageListener(ImageDocument* aDocument)
+ : MediaDocumentStreamListener(aDocument) {}
+
+ImageListener::~ImageListener() = default;
+
+NS_IMETHODIMP
+ImageListener::OnStartRequest(nsIRequest* request) {
+ NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE);
+
+ ImageDocument* imgDoc = static_cast<ImageDocument*>(mDocument.get());
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
+ if (!channel) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> domWindow = imgDoc->GetWindow();
+ NS_ENSURE_TRUE(domWindow, NS_ERROR_UNEXPECTED);
+
+ // This is an image being loaded as a document, so it's not going to be
+ // detected by the ImageBlocker. However we don't want to call
+ // NS_CheckContentLoadPolicy (with an TYPE_INTERNAL_IMAGE) here, as it would
+ // e.g. make this image load be detectable by CSP.
+ nsCOMPtr<nsIURI> channelURI;
+ channel->GetURI(getter_AddRefs(channelURI));
+ if (image::ImageBlocker::ShouldBlock(channelURI)) {
+ request->Cancel(NS_ERROR_CONTENT_BLOCKED);
+ return NS_OK;
+ }
+
+ if (!imgDoc->mObservingImageLoader) {
+ NS_ENSURE_TRUE(imgDoc->mImageContent, NS_ERROR_UNEXPECTED);
+ imgDoc->mImageContent->AddNativeObserver(imgDoc);
+ imgDoc->mObservingImageLoader = true;
+ imgDoc->mImageContent->LoadImageWithChannel(channel,
+ getter_AddRefs(mNextStream));
+ }
+
+ return MediaDocumentStreamListener::OnStartRequest(request);
+}
+
+ImageDocument::ImageDocument()
+ : mVisibleWidth(0.0),
+ mVisibleHeight(0.0),
+ mImageWidth(0),
+ mImageHeight(0),
+ mImageIsResized(false),
+ mShouldResize(false),
+ mFirstResize(false),
+ mObservingImageLoader(false),
+ mTitleUpdateInProgress(false),
+ mHasCustomTitle(false),
+ mIsInObjectOrEmbed(false),
+ mOriginalZoomLevel(1.0),
+ mOriginalResolution(1.0) {}
+
+ImageDocument::~ImageDocument() = default;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ImageDocument, MediaDocument, mImageContent)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(ImageDocument, MediaDocument,
+ imgINotificationObserver,
+ nsIDOMEventListener)
+
+nsresult ImageDocument::Init(nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) {
+ nsresult rv = MediaDocument::Init(aPrincipal, aPartitionedPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mShouldResize = StaticPrefs::browser_enable_automatic_image_resizing();
+ mFirstResize = true;
+
+ return NS_OK;
+}
+
+JSObject* ImageDocument::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ImageDocument_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsresult ImageDocument::StartDocumentLoad(
+ const char* aCommand, nsIChannel* aChannel, nsILoadGroup* aLoadGroup,
+ nsISupports* aContainer, nsIStreamListener** aDocListener, bool aReset) {
+ nsresult rv = MediaDocument::StartDocumentLoad(
+ aCommand, aChannel, aLoadGroup, aContainer, aDocListener, aReset);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mOriginalZoomLevel = IsSiteSpecific() ? 1.0 : GetZoomLevel();
+ CheckFullZoom();
+ mOriginalResolution = GetResolution();
+
+ if (BrowsingContext* context = GetBrowsingContext()) {
+ mIsInObjectOrEmbed = context->IsEmbedderTypeObjectOrEmbed();
+ }
+
+ NS_ASSERTION(aDocListener, "null aDocListener");
+ *aDocListener = new ImageListener(this);
+ NS_ADDREF(*aDocListener);
+
+ return NS_OK;
+}
+
+void ImageDocument::Destroy() {
+ if (RefPtr<HTMLImageElement> img = std::move(mImageContent)) {
+ // Remove our event listener from the image content.
+ img->RemoveEventListener(u"load"_ns, this, false);
+ img->RemoveEventListener(u"click"_ns, this, false);
+
+ // Break reference cycle with mImageContent, if we have one
+ if (mObservingImageLoader) {
+ img->RemoveNativeObserver(this);
+ }
+ }
+
+ MediaDocument::Destroy();
+}
+
+void ImageDocument::SetScriptGlobalObject(
+ nsIScriptGlobalObject* aScriptGlobalObject) {
+ // If the script global object is changing, we need to unhook our event
+ // listeners on the window.
+ nsCOMPtr<EventTarget> target;
+ if (mScriptGlobalObject && aScriptGlobalObject != mScriptGlobalObject) {
+ target = do_QueryInterface(mScriptGlobalObject);
+ target->RemoveEventListener(u"resize"_ns, this, false);
+ target->RemoveEventListener(u"keypress"_ns, this, false);
+ }
+
+ // Set the script global object on the superclass before doing
+ // anything that might require it....
+ MediaDocument::SetScriptGlobalObject(aScriptGlobalObject);
+
+ if (aScriptGlobalObject) {
+ if (!InitialSetupHasBeenDone()) {
+ MOZ_ASSERT(!GetRootElement(), "Where did the root element come from?");
+ // Create synthetic document
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ CreateSyntheticDocument();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create synthetic document");
+
+ target = mImageContent;
+ target->AddEventListener(u"load"_ns, this, false);
+ target->AddEventListener(u"click"_ns, this, false);
+ }
+
+ target = do_QueryInterface(aScriptGlobalObject);
+ target->AddEventListener(u"resize"_ns, this, false);
+ target->AddEventListener(u"keypress"_ns, this, false);
+
+ if (!InitialSetupHasBeenDone()) {
+ LinkStylesheet(u"resource://content-accessible/ImageDocument.css"_ns);
+ if (!nsContentUtils::IsChildOfSameType(this)) {
+ LinkStylesheet(nsLiteralString(
+ u"resource://content-accessible/TopLevelImageDocument.css"));
+ }
+ InitialSetupDone();
+ }
+ }
+}
+
+void ImageDocument::OnPageShow(bool aPersisted,
+ EventTarget* aDispatchStartTarget,
+ bool aOnlySystemGroup) {
+ if (aPersisted) {
+ mOriginalZoomLevel = IsSiteSpecific() ? 1.0 : GetZoomLevel();
+ CheckFullZoom();
+ mOriginalResolution = GetResolution();
+ }
+ RefPtr<ImageDocument> kungFuDeathGrip(this);
+ UpdateSizeFromLayout();
+
+ MediaDocument::OnPageShow(aPersisted, aDispatchStartTarget, aOnlySystemGroup);
+}
+
+void ImageDocument::ShrinkToFit() {
+ if (!mImageContent) {
+ return;
+ }
+ if (GetZoomLevel() != mOriginalZoomLevel && mImageIsResized &&
+ !nsContentUtils::IsChildOfSameType(this)) {
+ // If we're zoomed, so that we don't maintain the invariant that
+ // mImageIsResized if and only if its displayed width/height fit in
+ // mVisibleWidth/mVisibleHeight, then we may need to switch to/from the
+ // overflowingVertical class here, because our viewport size may have
+ // changed and we don't plan to adjust the image size to compensate. Since
+ // mImageIsResized it has a "height" attribute set, and we can just get the
+ // displayed image height by getting .height on the HTMLImageElement.
+ //
+ // Hold strong ref, because Height() can run script.
+ RefPtr<HTMLImageElement> img = mImageContent;
+ uint32_t imageHeight = img->Height();
+ nsDOMTokenList* classList = img->ClassList();
+ if (imageHeight > mVisibleHeight) {
+ classList->Add(u"overflowingVertical"_ns, IgnoreErrors());
+ } else {
+ classList->Remove(u"overflowingVertical"_ns, IgnoreErrors());
+ }
+ return;
+ }
+ if (GetResolution() != mOriginalResolution && mImageIsResized) {
+ // Don't resize if resolution has changed, e.g., through pinch-zooming on
+ // Android.
+ return;
+ }
+
+ // Keep image content alive while changing the attributes.
+ RefPtr<HTMLImageElement> image = mImageContent;
+
+ uint32_t newWidth = std::max(1, NSToCoordFloor(GetRatio() * mImageWidth));
+ uint32_t newHeight = std::max(1, NSToCoordFloor(GetRatio() * mImageHeight));
+ image->SetWidth(newWidth, IgnoreErrors());
+ image->SetHeight(newHeight, IgnoreErrors());
+
+ // The view might have been scrolled when zooming in, scroll back to the
+ // origin now that we're showing a shrunk-to-window version.
+ ScrollImageTo(0, 0);
+
+ if (!mImageContent) {
+ // ScrollImageTo flush destroyed our content.
+ return;
+ }
+
+ SetModeClass(eShrinkToFit);
+
+ mImageIsResized = true;
+
+ UpdateTitleAndCharset();
+}
+
+void ImageDocument::ScrollImageTo(int32_t aX, int32_t aY) {
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (!presShell) {
+ return;
+ }
+
+ nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
+ if (!sf) {
+ return;
+ }
+
+ float ratio = GetRatio();
+ // Don't try to scroll image if the document is not visible (mVisibleWidth or
+ // mVisibleHeight is zero).
+ if (ratio <= 0.0) {
+ return;
+ }
+ nsRect portRect = sf->GetScrollPortRect();
+ sf->ScrollTo(
+ nsPoint(
+ nsPresContext::CSSPixelsToAppUnits(aX / ratio) - portRect.width / 2,
+ nsPresContext::CSSPixelsToAppUnits(aY / ratio) - portRect.height / 2),
+ ScrollMode::Instant);
+}
+
+void ImageDocument::RestoreImage() {
+ if (!mImageContent) {
+ return;
+ }
+ // Keep image content alive while changing the attributes.
+ RefPtr<HTMLImageElement> imageContent = mImageContent;
+ imageContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::width, true);
+ imageContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::height, true);
+
+ if (mIsInObjectOrEmbed) {
+ SetModeClass(eIsInObjectOrEmbed);
+ } else if (ImageIsOverflowing()) {
+ if (!ImageIsOverflowingVertically()) {
+ SetModeClass(eOverflowingHorizontalOnly);
+ } else {
+ SetModeClass(eOverflowingVertical);
+ }
+ } else {
+ SetModeClass(eNone);
+ }
+
+ mImageIsResized = false;
+
+ UpdateTitleAndCharset();
+}
+
+void ImageDocument::NotifyPossibleTitleChange(bool aBoundTitleElement) {
+ if (!mHasCustomTitle && !mTitleUpdateInProgress) {
+ mHasCustomTitle = true;
+ }
+
+ Document::NotifyPossibleTitleChange(aBoundTitleElement);
+}
+
+void ImageDocument::Notify(imgIRequest* aRequest, int32_t aType,
+ const nsIntRect* aData) {
+ if (aType == imgINotificationObserver::SIZE_AVAILABLE) {
+ nsCOMPtr<imgIContainer> image;
+ aRequest->GetImage(getter_AddRefs(image));
+ return OnSizeAvailable(aRequest, image);
+ }
+
+ // Run this using a script runner because HAS_TRANSPARENCY notifications can
+ // come during painting and this will trigger invalidation.
+ if (aType == imgINotificationObserver::HAS_TRANSPARENCY) {
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod("dom::ImageDocument::OnHasTransparency", this,
+ &ImageDocument::OnHasTransparency);
+ nsContentUtils::AddScriptRunner(runnable);
+ }
+
+ if (aType == imgINotificationObserver::LOAD_COMPLETE) {
+ uint32_t reqStatus;
+ aRequest->GetImageStatus(&reqStatus);
+ nsresult status =
+ reqStatus & imgIRequest::STATUS_ERROR ? NS_ERROR_FAILURE : NS_OK;
+ return OnLoadComplete(aRequest, status);
+ }
+}
+
+void ImageDocument::OnHasTransparency() {
+ if (!mImageContent || nsContentUtils::IsChildOfSameType(this)) {
+ return;
+ }
+
+ nsDOMTokenList* classList = mImageContent->ClassList();
+ classList->Add(u"transparent"_ns, IgnoreErrors());
+}
+
+void ImageDocument::SetModeClass(eModeClasses mode) {
+ nsDOMTokenList* classList = mImageContent->ClassList();
+
+ if (mode == eShrinkToFit) {
+ classList->Add(u"shrinkToFit"_ns, IgnoreErrors());
+ } else {
+ classList->Remove(u"shrinkToFit"_ns, IgnoreErrors());
+ }
+
+ if (mode == eOverflowingVertical) {
+ classList->Add(u"overflowingVertical"_ns, IgnoreErrors());
+ } else {
+ classList->Remove(u"overflowingVertical"_ns, IgnoreErrors());
+ }
+
+ if (mode == eOverflowingHorizontalOnly) {
+ classList->Add(u"overflowingHorizontalOnly"_ns, IgnoreErrors());
+ } else {
+ classList->Remove(u"overflowingHorizontalOnly"_ns, IgnoreErrors());
+ }
+
+ if (mode == eIsInObjectOrEmbed) {
+ classList->Add(u"isInObjectOrEmbed"_ns, IgnoreErrors());
+ }
+}
+
+void ImageDocument::OnSizeAvailable(imgIRequest* aRequest,
+ imgIContainer* aImage) {
+ int32_t oldWidth = mImageWidth;
+ int32_t oldHeight = mImageHeight;
+
+ // Styles have not yet been applied, so we don't know the final size. For now,
+ // default to the image's intrinsic size.
+ aImage->GetWidth(&mImageWidth);
+ aImage->GetHeight(&mImageHeight);
+
+ // Multipart images send size available for each part; ignore them if it
+ // doesn't change our size. (We may not even support changing size in
+ // multipart images in the future.)
+ if (oldWidth == mImageWidth && oldHeight == mImageHeight) {
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod("dom::ImageDocument::DefaultCheckOverflowing", this,
+ &ImageDocument::DefaultCheckOverflowing);
+ nsContentUtils::AddScriptRunner(runnable);
+ UpdateTitleAndCharset();
+}
+
+void ImageDocument::OnLoadComplete(imgIRequest* aRequest, nsresult aStatus) {
+ UpdateTitleAndCharset();
+
+ // mImageContent can be null if the document is already destroyed
+ if (NS_FAILED(aStatus) && mImageContent) {
+ nsAutoCString src;
+ mDocumentURI->GetSpec(src);
+ AutoTArray<nsString, 1> formatString;
+ CopyUTF8toUTF16(src, *formatString.AppendElement());
+ nsAutoString errorMsg;
+ FormatStringFromName("InvalidImage", formatString, errorMsg);
+
+ mImageContent->SetAttr(kNameSpaceID_None, nsGkAtoms::alt, errorMsg, false);
+ }
+
+ MaybeSendResultToEmbedder(aStatus);
+}
+
+NS_IMETHODIMP
+ImageDocument::HandleEvent(Event* aEvent) {
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("resize")) {
+ CheckOverflowing(false);
+ CheckFullZoom();
+ } else if (eventType.EqualsLiteral("click") &&
+ StaticPrefs::browser_enable_click_image_resizing() &&
+ !mIsInObjectOrEmbed) {
+ ResetZoomLevel();
+ mShouldResize = true;
+ if (mImageIsResized) {
+ int32_t x = 0, y = 0;
+ MouseEvent* event = aEvent->AsMouseEvent();
+ if (event) {
+ RefPtr<HTMLImageElement> img = mImageContent;
+ x = event->ClientX() - img->OffsetLeft();
+ y = event->ClientY() - img->OffsetTop();
+ }
+ mShouldResize = false;
+ RestoreImage();
+ FlushPendingNotifications(FlushType::Layout);
+ ScrollImageTo(x, y);
+ } else if (ImageIsOverflowing()) {
+ ShrinkToFit();
+ }
+ } else if (eventType.EqualsLiteral("load")) {
+ UpdateSizeFromLayout();
+ }
+
+ return NS_OK;
+}
+
+void ImageDocument::UpdateSizeFromLayout() {
+ // Pull an updated size from the content frame to account for any size
+ // change due to CSS properties like |image-orientation|.
+ if (!mImageContent) {
+ return;
+ }
+
+ // Need strong ref, because GetPrimaryFrame can run script.
+ RefPtr<HTMLImageElement> imageContent = mImageContent;
+ nsIFrame* contentFrame = imageContent->GetPrimaryFrame(FlushType::Frames);
+ if (!contentFrame) {
+ return;
+ }
+
+ nsIntSize oldSize(mImageWidth, mImageHeight);
+ IntrinsicSize newSize = contentFrame->GetIntrinsicSize();
+
+ if (newSize.width) {
+ mImageWidth = nsPresContext::AppUnitsToFloatCSSPixels(*newSize.width);
+ }
+ if (newSize.height) {
+ mImageHeight = nsPresContext::AppUnitsToFloatCSSPixels(*newSize.height);
+ }
+
+ // Ensure that our information about overflow is up-to-date if needed.
+ if (mImageWidth != oldSize.width || mImageHeight != oldSize.height) {
+ CheckOverflowing(false);
+ }
+}
+
+void ImageDocument::UpdateRemoteStyle(StyleImageRendering aImageRendering) {
+ if (!mImageContent) {
+ return;
+ }
+
+ // Using ScriptRunner to avoid doing DOM mutation at an unexpected time.
+ if (!nsContentUtils::IsSafeToRunScript()) {
+ return nsContentUtils::AddScriptRunner(
+ NewRunnableMethod<StyleImageRendering>(
+ "UpdateRemoteStyle", this, &ImageDocument::UpdateRemoteStyle,
+ aImageRendering));
+ }
+
+ nsCOMPtr<nsICSSDeclaration> style = mImageContent->Style();
+ switch (aImageRendering) {
+ case StyleImageRendering::Auto:
+ case StyleImageRendering::Smooth:
+ case StyleImageRendering::Optimizequality:
+ style->SetProperty("image-rendering"_ns, "auto"_ns, ""_ns,
+ IgnoreErrors());
+ break;
+ case StyleImageRendering::Optimizespeed:
+ case StyleImageRendering::Pixelated:
+ style->SetProperty("image-rendering"_ns, "pixelated"_ns, ""_ns,
+ IgnoreErrors());
+ break;
+ case StyleImageRendering::CrispEdges:
+ style->SetProperty("image-rendering"_ns, "crisp-edges"_ns, ""_ns,
+ IgnoreErrors());
+ break;
+ }
+}
+
+nsresult ImageDocument::CreateSyntheticDocument() {
+ // Synthesize an html document that refers to the image
+ nsresult rv = MediaDocument::CreateSyntheticDocument();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add the image element
+ RefPtr<Element> body = GetBodyElement();
+ if (!body) {
+ NS_WARNING("no body on image document!");
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::img, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ RefPtr<Element> image = NS_NewHTMLImageElement(nodeInfo.forget());
+ mImageContent = HTMLImageElement::FromNodeOrNull(image);
+ if (!mImageContent) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsAutoCString src;
+ mDocumentURI->GetSpec(src);
+
+ NS_ConvertUTF8toUTF16 srcString(src);
+ // Make sure not to start the image load from here...
+ mImageContent->SetLoadingEnabled(false);
+ mImageContent->SetAttr(kNameSpaceID_None, nsGkAtoms::src, srcString, false);
+ mImageContent->SetAttr(kNameSpaceID_None, nsGkAtoms::alt, srcString, false);
+
+ if (mIsInObjectOrEmbed) {
+ SetModeClass(eIsInObjectOrEmbed);
+ }
+
+ body->AppendChildTo(mImageContent, false, IgnoreErrors());
+ mImageContent->SetLoadingEnabled(true);
+
+ return NS_OK;
+}
+
+void ImageDocument::DefaultCheckOverflowing() {
+ CheckOverflowing(StaticPrefs::browser_enable_automatic_image_resizing());
+}
+
+nsresult ImageDocument::CheckOverflowing(bool changeState) {
+ const bool imageWasOverflowing = ImageIsOverflowing();
+ const bool imageWasOverflowingVertically = ImageIsOverflowingVertically();
+
+ {
+ nsPresContext* context = GetPresContext();
+ if (!context) {
+ return NS_OK;
+ }
+
+ nsRect visibleArea = context->GetVisibleArea();
+
+ mVisibleWidth = nsPresContext::AppUnitsToFloatCSSPixels(visibleArea.width);
+ mVisibleHeight =
+ nsPresContext::AppUnitsToFloatCSSPixels(visibleArea.height);
+ }
+
+ const bool windowBecameBigEnough =
+ imageWasOverflowing && !ImageIsOverflowing();
+ const bool verticalOverflowChanged =
+ imageWasOverflowingVertically != ImageIsOverflowingVertically();
+
+ if (changeState || mShouldResize || mFirstResize || windowBecameBigEnough ||
+ verticalOverflowChanged) {
+ if (mIsInObjectOrEmbed) {
+ SetModeClass(eIsInObjectOrEmbed);
+ } else if (ImageIsOverflowing() && (changeState || mShouldResize)) {
+ ShrinkToFit();
+ } else if (mImageIsResized || mFirstResize || windowBecameBigEnough) {
+ RestoreImage();
+ } else if (!mImageIsResized && verticalOverflowChanged) {
+ if (ImageIsOverflowingVertically()) {
+ SetModeClass(eOverflowingVertical);
+ } else {
+ SetModeClass(eOverflowingHorizontalOnly);
+ }
+ }
+ }
+ mFirstResize = false;
+ return NS_OK;
+}
+
+void ImageDocument::UpdateTitleAndCharset() {
+ if (mHasCustomTitle) {
+ return;
+ }
+
+ AutoRestore<bool> restore(mTitleUpdateInProgress);
+ mTitleUpdateInProgress = true;
+
+ nsAutoCString typeStr;
+ nsCOMPtr<imgIRequest> imageRequest;
+ if (mImageContent) {
+ mImageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
+ getter_AddRefs(imageRequest));
+ }
+
+ if (imageRequest) {
+ nsCString mimeType;
+ imageRequest->GetMimeType(getter_Copies(mimeType));
+ ToUpperCase(mimeType);
+ nsCString::const_iterator start, end;
+ mimeType.BeginReading(start);
+ mimeType.EndReading(end);
+ nsCString::const_iterator iter = end;
+ if (FindInReadable("IMAGE/"_ns, start, iter) && iter != end) {
+ // strip out "X-" if any
+ if (*iter == 'X') {
+ ++iter;
+ if (iter != end && *iter == '-') {
+ ++iter;
+ if (iter == end) {
+ // looks like "IMAGE/X-" is the type?? Bail out of here.
+ mimeType.BeginReading(iter);
+ }
+ } else {
+ --iter;
+ }
+ }
+ typeStr = Substring(iter, end);
+ } else {
+ typeStr = mimeType;
+ }
+ }
+
+ nsAutoString status;
+ if (mImageIsResized) {
+ AutoTArray<nsString, 1> formatString;
+ formatString.AppendElement()->AppendInt(NSToCoordFloor(GetRatio() * 100));
+
+ FormatStringFromName("ScaledImage", formatString, status);
+ }
+
+ static const char* const formatNames[4] = {
+ "ImageTitleWithNeitherDimensionsNorFile",
+ "ImageTitleWithoutDimensions",
+ "ImageTitleWithDimensions2",
+ "ImageTitleWithDimensions2AndFile",
+ };
+
+ MediaDocument::UpdateTitleAndCharset(typeStr, mChannel, formatNames,
+ mImageWidth, mImageHeight, status);
+}
+
+bool ImageDocument::IsSiteSpecific() {
+ return !ShouldResistFingerprinting(RFPTarget::SiteSpecificZoom) &&
+ StaticPrefs::browser_zoom_siteSpecific();
+}
+
+void ImageDocument::ResetZoomLevel() {
+ if (nsContentUtils::IsChildOfSameType(this)) {
+ return;
+ }
+
+ if (RefPtr<BrowsingContext> bc = GetBrowsingContext()) {
+ // Resetting the zoom level on a discarded browsing context has no effect.
+ Unused << bc->SetFullZoom(mOriginalZoomLevel);
+ }
+}
+
+float ImageDocument::GetZoomLevel() {
+ if (BrowsingContext* bc = GetBrowsingContext()) {
+ return bc->FullZoom();
+ }
+ return mOriginalZoomLevel;
+}
+
+void ImageDocument::CheckFullZoom() {
+ nsDOMTokenList* classList =
+ mImageContent ? mImageContent->ClassList() : nullptr;
+
+ if (!classList) {
+ return;
+ }
+
+ classList->Toggle(u"fullZoomOut"_ns,
+ dom::Optional<bool>(GetZoomLevel() > mOriginalZoomLevel),
+ IgnoreErrors());
+ classList->Toggle(u"fullZoomIn"_ns,
+ dom::Optional<bool>(GetZoomLevel() < mOriginalZoomLevel),
+ IgnoreErrors());
+}
+
+float ImageDocument::GetResolution() {
+ if (PresShell* presShell = GetPresShell()) {
+ return presShell->GetResolution();
+ }
+ return mOriginalResolution;
+}
+
+void ImageDocument::MaybeSendResultToEmbedder(nsresult aResult) {
+ if (!mIsInObjectOrEmbed) {
+ return;
+ }
+
+ BrowsingContext* context = GetBrowsingContext();
+
+ if (!context) {
+ return;
+ }
+
+ if (context->GetParent() && context->GetParent()->IsInProcess()) {
+ if (Element* embedder = context->GetEmbedderElement()) {
+ if (nsCOMPtr<nsIObjectLoadingContent> objectLoadingContent =
+ do_QueryInterface(embedder)) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "nsObjectLoadingContent::SubdocumentImageLoadComplete",
+ [objectLoadingContent, aResult]() {
+ static_cast<nsObjectLoadingContent*>(objectLoadingContent.get())
+ ->SubdocumentImageLoadComplete(aResult);
+ }));
+ }
+ return;
+ }
+ }
+
+ if (BrowserChild* browserChild =
+ BrowserChild::GetFrom(context->GetDocShell())) {
+ browserChild->SendImageLoadComplete(aResult);
+ }
+}
+} // namespace mozilla::dom
+
+nsresult NS_NewImageDocument(mozilla::dom::Document** aResult,
+ nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) {
+ auto* doc = new mozilla::dom::ImageDocument();
+ NS_ADDREF(doc);
+
+ nsresult rv = doc->Init(aPrincipal, aPartitionedPrincipal);
+ if (NS_FAILED(rv)) {
+ NS_RELEASE(doc);
+ }
+
+ *aResult = doc;
+
+ return rv;
+}
diff --git a/dom/html/ImageDocument.h b/dom/html/ImageDocument.h
new file mode 100644
index 0000000000..891658264c
--- /dev/null
+++ b/dom/html/ImageDocument.h
@@ -0,0 +1,160 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_dom_ImageDocument_h
+#define mozilla_dom_ImageDocument_h
+
+#include "mozilla/Attributes.h"
+#include "imgINotificationObserver.h"
+#include "mozilla/dom/MediaDocument.h"
+#include "nsIDOMEventListener.h"
+
+namespace mozilla {
+enum class StyleImageRendering : uint8_t;
+struct IntrinsicSize;
+} // namespace mozilla
+
+namespace mozilla::dom {
+class HTMLImageElement;
+
+class ImageDocument final : public MediaDocument,
+ public imgINotificationObserver,
+ public nsIDOMEventListener {
+ public:
+ ImageDocument();
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ enum MediaDocumentKind MediaDocumentKind() const override {
+ return MediaDocumentKind::Image;
+ }
+
+ nsresult Init(nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) override;
+
+ nsresult StartDocumentLoad(const char* aCommand, nsIChannel* aChannel,
+ nsILoadGroup* aLoadGroup, nsISupports* aContainer,
+ nsIStreamListener** aDocListener,
+ bool aReset = true) override;
+
+ void SetScriptGlobalObject(nsIScriptGlobalObject*) override;
+ void Destroy() override;
+ void OnPageShow(bool aPersisted, EventTarget* aDispatchStartTarget,
+ bool aOnlySystemGroup = false) override;
+
+ NS_DECL_IMGINOTIFICATIONOBSERVER
+
+ // nsIDOMEventListener
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ImageDocument, MediaDocument)
+
+ friend class ImageListener;
+
+ void DefaultCheckOverflowing();
+
+ // WebIDL API
+ JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override;
+
+ bool ImageIsOverflowing() const {
+ return ImageIsOverflowingHorizontally() || ImageIsOverflowingVertically();
+ }
+
+ bool ImageIsOverflowingVertically() const {
+ return mImageHeight > mVisibleHeight;
+ }
+
+ bool ImageIsOverflowingHorizontally() const {
+ return mImageWidth > mVisibleWidth;
+ }
+
+ bool ImageIsResized() const { return mImageIsResized; }
+ // ShrinkToFit is called from xpidl methods and we don't have a good
+ // way to mark those MOZ_CAN_RUN_SCRIPT yet.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void ShrinkToFit();
+ void RestoreImage();
+
+ void NotifyPossibleTitleChange(bool aBoundTitleElement) override;
+
+ void UpdateRemoteStyle(StyleImageRendering aImageRendering);
+
+ protected:
+ virtual ~ImageDocument();
+
+ nsresult CreateSyntheticDocument() override;
+
+ nsresult CheckOverflowing(bool changeState);
+
+ void UpdateTitleAndCharset();
+
+ void ScrollImageTo(int32_t aX, int32_t aY);
+
+ float GetRatio() const {
+ return std::min(mVisibleWidth / mImageWidth, mVisibleHeight / mImageHeight);
+ }
+
+ bool IsSiteSpecific();
+
+ void ResetZoomLevel();
+ float GetZoomLevel();
+ void CheckFullZoom();
+ float GetResolution();
+
+ void UpdateSizeFromLayout();
+
+ enum eModeClasses {
+ eNone,
+ eShrinkToFit,
+ eOverflowingVertical, // And maybe horizontal too.
+ eOverflowingHorizontalOnly,
+ eIsInObjectOrEmbed
+ };
+ void SetModeClass(eModeClasses mode);
+
+ void OnSizeAvailable(imgIRequest* aRequest, imgIContainer* aImage);
+ void OnLoadComplete(imgIRequest* aRequest, nsresult aStatus);
+ void OnHasTransparency();
+
+ void MaybeSendResultToEmbedder(nsresult aResult);
+
+ RefPtr<HTMLImageElement> mImageContent;
+
+ float mVisibleWidth;
+ float mVisibleHeight;
+ int32_t mImageWidth;
+ int32_t mImageHeight;
+
+ // mImageIsResized is true if the image is currently resized
+ bool mImageIsResized;
+ // mShouldResize is true if the image should be resized when it doesn't fit
+ // mImageIsResized cannot be true when this is false, but mImageIsResized
+ // can be false when this is true
+ bool mShouldResize;
+ bool mFirstResize;
+ // mObservingImageLoader is true while the observer is set.
+ bool mObservingImageLoader;
+ bool mTitleUpdateInProgress;
+ bool mHasCustomTitle;
+
+ // True iff embedder is either <object> or <embed>.
+ bool mIsInObjectOrEmbed;
+
+ float mOriginalZoomLevel;
+ float mOriginalResolution;
+};
+
+inline ImageDocument* Document::AsImageDocument() {
+ MOZ_ASSERT(IsImageDocument());
+ return static_cast<ImageDocument*>(this);
+}
+
+inline const ImageDocument* Document::AsImageDocument() const {
+ MOZ_ASSERT(IsImageDocument());
+ return static_cast<const ImageDocument*>(this);
+}
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_ImageDocument_h */
diff --git a/dom/html/MediaDocument.cpp b/dom/html/MediaDocument.cpp
new file mode 100644
index 0000000000..40662e6d4c
--- /dev/null
+++ b/dom/html/MediaDocument.cpp
@@ -0,0 +1,411 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDocument.h"
+#include "nsGkAtoms.h"
+#include "nsRect.h"
+#include "nsPresContext.h"
+#include "nsViewManager.h"
+#include "nsITextToSubURI.h"
+#include "nsIURL.h"
+#include "nsIDocShell.h"
+#include "nsCharsetSource.h" // kCharsetFrom* macro definition
+#include "nsNodeInfoManager.h"
+#include "nsContentUtils.h"
+#include "nsDocElementCreatedNotificationRunner.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Components.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIPrincipal.h"
+#include "nsIMultiPartChannel.h"
+#include "nsProxyRelease.h"
+
+namespace mozilla::dom {
+
+MediaDocumentStreamListener::MediaDocumentStreamListener(
+ MediaDocument* aDocument)
+ : mDocument(aDocument) {}
+
+MediaDocumentStreamListener::~MediaDocumentStreamListener() {
+ if (mDocument && !NS_IsMainThread()) {
+ nsCOMPtr<nsIEventTarget> mainTarget(do_GetMainThread());
+ NS_ProxyRelease("MediaDocumentStreamListener::mDocument", mainTarget,
+ mDocument.forget());
+ }
+}
+
+NS_IMPL_ISUPPORTS(MediaDocumentStreamListener, nsIRequestObserver,
+ nsIStreamListener, nsIThreadRetargetableStreamListener)
+
+NS_IMETHODIMP
+MediaDocumentStreamListener::OnStartRequest(nsIRequest* request) {
+ NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE);
+
+ mDocument->StartLayout();
+
+ if (mNextStream) {
+ return mNextStream->OnStartRequest(request);
+ }
+
+ return NS_ERROR_PARSED_DATA_CACHED;
+}
+
+NS_IMETHODIMP
+MediaDocumentStreamListener::OnStopRequest(nsIRequest* request,
+ nsresult status) {
+ nsresult rv = NS_OK;
+ if (mNextStream) {
+ rv = mNextStream->OnStopRequest(request, status);
+ }
+
+ // Don't release mDocument here if we're in the middle of a multipart
+ // response.
+ bool lastPart = true;
+ nsCOMPtr<nsIMultiPartChannel> mpchan(do_QueryInterface(request));
+ if (mpchan) {
+ mpchan->GetIsLastPart(&lastPart);
+ }
+
+ if (lastPart) {
+ mDocument = nullptr;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+MediaDocumentStreamListener::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset,
+ uint32_t count) {
+ if (mNextStream) {
+ return mNextStream->OnDataAvailable(request, inStr, sourceOffset, count);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaDocumentStreamListener::OnDataFinished(nsresult aStatus) {
+ if (!mNextStream) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable =
+ do_QueryInterface(mNextStream);
+ if (retargetable) {
+ return retargetable->OnDataFinished(aStatus);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaDocumentStreamListener::CheckListenerChain() {
+ nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable =
+ do_QueryInterface(mNextStream);
+ if (retargetable) {
+ return retargetable->CheckListenerChain();
+ }
+ return NS_ERROR_NO_INTERFACE;
+}
+
+// default format names for MediaDocument.
+const char* const MediaDocument::sFormatNames[4] = {
+ "MediaTitleWithNoInfo", // eWithNoInfo
+ "MediaTitleWithFile", // eWithFile
+ "", // eWithDim
+ "" // eWithDimAndFile
+};
+
+MediaDocument::MediaDocument() : mDidInitialDocumentSetup(false) {
+ mCompatMode = eCompatibility_FullStandards;
+}
+MediaDocument::~MediaDocument() = default;
+
+nsresult MediaDocument::Init(nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) {
+ nsresult rv = nsHTMLDocument::Init(aPrincipal, aPartitionedPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mIsSyntheticDocument = true;
+
+ return NS_OK;
+}
+
+nsresult MediaDocument::StartDocumentLoad(
+ const char* aCommand, nsIChannel* aChannel, nsILoadGroup* aLoadGroup,
+ nsISupports* aContainer, nsIStreamListener** aDocListener, bool aReset) {
+ nsresult rv = Document::StartDocumentLoad(aCommand, aChannel, aLoadGroup,
+ aContainer, aDocListener, aReset);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // We try to set the charset of the current document to that of the
+ // 'genuine' (as opposed to an intervening 'chrome') parent document
+ // that may be in a different window/tab. Even if we fail here,
+ // we just return NS_OK because another attempt is made in
+ // |UpdateTitleAndCharset| and the worst thing possible is a mangled
+ // filename in the titlebar and the file picker.
+
+ // Note that we
+ // exclude UTF-8 as 'invalid' because UTF-8 is likely to be the charset
+ // of a chrome document that has nothing to do with the actual content
+ // whose charset we want to know. Even if "the actual content" is indeed
+ // in UTF-8, we don't lose anything because the default empty value is
+ // considered synonymous with UTF-8.
+
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aContainer));
+
+ // not being able to set the charset is not critical.
+ NS_ENSURE_TRUE(docShell, NS_OK);
+
+ const Encoding* encoding;
+ int32_t source;
+ nsCOMPtr<nsIPrincipal> principal;
+ // opening in a new tab
+ docShell->GetParentCharset(encoding, &source, getter_AddRefs(principal));
+
+ if (encoding && encoding != UTF_8_ENCODING &&
+ NodePrincipal()->Equals(principal)) {
+ SetDocumentCharacterSetSource(source);
+ SetDocumentCharacterSet(WrapNotNull(encoding));
+ }
+
+ return NS_OK;
+}
+
+void MediaDocument::InitialSetupDone() {
+ MOZ_ASSERT(GetReadyStateEnum() == Document::READYSTATE_LOADING,
+ "Bad readyState: we should still be doing our initial load");
+ mDidInitialDocumentSetup = true;
+ nsContentUtils::AddScriptRunner(
+ new nsDocElementCreatedNotificationRunner(this));
+ SetReadyStateInternal(Document::READYSTATE_INTERACTIVE);
+}
+
+nsresult MediaDocument::CreateSyntheticDocument() {
+ MOZ_ASSERT(!InitialSetupHasBeenDone());
+
+ // Synthesize an empty html document
+
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::html, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ RefPtr<nsGenericHTMLElement> root = NS_NewHTMLHtmlElement(nodeInfo.forget());
+ NS_ENSURE_TRUE(root, NS_ERROR_OUT_OF_MEMORY);
+
+ NS_ASSERTION(GetChildCount() == 0, "Shouldn't have any kids");
+ ErrorResult rv;
+ AppendChildTo(root, false, rv);
+ if (rv.Failed()) {
+ return rv.StealNSResult();
+ }
+
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::head, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ // Create a <head> so our title has somewhere to live
+ RefPtr<nsGenericHTMLElement> head = NS_NewHTMLHeadElement(nodeInfo.forget());
+ NS_ENSURE_TRUE(head, NS_ERROR_OUT_OF_MEMORY);
+
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::meta, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ RefPtr<nsGenericHTMLElement> metaContent =
+ NS_NewHTMLMetaElement(nodeInfo.forget());
+ NS_ENSURE_TRUE(metaContent, NS_ERROR_OUT_OF_MEMORY);
+ metaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::name, u"viewport"_ns,
+ true);
+
+ metaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::content,
+ u"width=device-width; height=device-height;"_ns, true);
+ head->AppendChildTo(metaContent, false, IgnoreErrors());
+
+ root->AppendChildTo(head, false, IgnoreErrors());
+
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::body, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ RefPtr<nsGenericHTMLElement> body = NS_NewHTMLBodyElement(nodeInfo.forget());
+ NS_ENSURE_TRUE(body, NS_ERROR_OUT_OF_MEMORY);
+
+ root->AppendChildTo(body, false, IgnoreErrors());
+
+ return NS_OK;
+}
+
+nsresult MediaDocument::StartLayout() {
+ mMayStartLayout = true;
+ RefPtr<PresShell> presShell = GetPresShell();
+ // Don't mess with the presshell if someone has already handled
+ // its initial reflow.
+ if (presShell && !presShell->DidInitialize()) {
+ nsresult rv = presShell->Initialize();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+void MediaDocument::GetFileName(nsAString& aResult, nsIChannel* aChannel) {
+ aResult.Truncate();
+
+ if (aChannel) {
+ aChannel->GetContentDispositionFilename(aResult);
+ if (!aResult.IsEmpty()) return;
+ }
+
+ nsCOMPtr<nsIURL> url = do_QueryInterface(mDocumentURI);
+ if (!url) return;
+
+ nsAutoCString fileName;
+ url->GetFileName(fileName);
+ if (fileName.IsEmpty()) return;
+
+ // Now that the charset is set in |StartDocumentLoad| to the charset of
+ // the document viewer instead of a bogus value ("windows-1252" set in
+ // |Document|'s ctor), the priority is given to the current charset.
+ // This is necessary to deal with a media document being opened in a new
+ // window or a new tab.
+ if (mCharacterSetSource == kCharsetUninitialized) {
+ // resort to UTF-8
+ SetDocumentCharacterSet(UTF_8_ENCODING);
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsITextToSubURI> textToSubURI =
+ do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ // UnEscapeURIForUI always succeeds
+ textToSubURI->UnEscapeURIForUI(fileName, aResult);
+ } else {
+ CopyUTF8toUTF16(fileName, aResult);
+ }
+}
+
+nsresult MediaDocument::LinkStylesheet(const nsAString& aStylesheet) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::link, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ RefPtr<nsGenericHTMLElement> link = NS_NewHTMLLinkElement(nodeInfo.forget());
+ NS_ENSURE_TRUE(link, NS_ERROR_OUT_OF_MEMORY);
+
+ link->SetAttr(kNameSpaceID_None, nsGkAtoms::rel, u"stylesheet"_ns, true);
+
+ link->SetAttr(kNameSpaceID_None, nsGkAtoms::href, aStylesheet, true);
+
+ ErrorResult rv;
+ Element* head = GetHeadElement();
+ head->AppendChildTo(link, false, rv);
+ return rv.StealNSResult();
+}
+
+nsresult MediaDocument::LinkScript(const nsAString& aScript) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::script, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ RefPtr<nsGenericHTMLElement> script =
+ NS_NewHTMLScriptElement(nodeInfo.forget());
+ NS_ENSURE_TRUE(script, NS_ERROR_OUT_OF_MEMORY);
+
+ script->SetAttr(kNameSpaceID_None, nsGkAtoms::type, u"text/javascript"_ns,
+ true);
+
+ script->SetAttr(kNameSpaceID_None, nsGkAtoms::src, aScript, true);
+
+ ErrorResult rv;
+ Element* head = GetHeadElement();
+ head->AppendChildTo(script, false, rv);
+ return rv.StealNSResult();
+}
+
+void MediaDocument::FormatStringFromName(const char* aName,
+ const nsTArray<nsString>& aParams,
+ nsAString& aResult) {
+ bool spoofLocale = nsContentUtils::SpoofLocaleEnglish() && !AllowsL10n();
+ if (!spoofLocale) {
+ if (!mStringBundle) {
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (stringService) {
+ stringService->CreateBundle(NSMEDIADOCUMENT_PROPERTIES_URI,
+ getter_AddRefs(mStringBundle));
+ }
+ }
+ if (mStringBundle) {
+ mStringBundle->FormatStringFromName(aName, aParams, aResult);
+ }
+ } else {
+ if (!mStringBundleEnglish) {
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (stringService) {
+ stringService->CreateBundle(NSMEDIADOCUMENT_PROPERTIES_URI_en_US,
+ getter_AddRefs(mStringBundleEnglish));
+ }
+ }
+ if (mStringBundleEnglish) {
+ mStringBundleEnglish->FormatStringFromName(aName, aParams, aResult);
+ }
+ }
+}
+
+void MediaDocument::UpdateTitleAndCharset(const nsACString& aTypeStr,
+ nsIChannel* aChannel,
+ const char* const* aFormatNames,
+ int32_t aWidth, int32_t aHeight,
+ const nsAString& aStatus) {
+ nsAutoString fileStr;
+ GetFileName(fileStr, aChannel);
+
+ NS_ConvertASCIItoUTF16 typeStr(aTypeStr);
+ nsAutoString title;
+
+ // if we got a valid size (not all media have a size)
+ if (aWidth != 0 && aHeight != 0) {
+ nsAutoString widthStr;
+ nsAutoString heightStr;
+ widthStr.AppendInt(aWidth);
+ heightStr.AppendInt(aHeight);
+ // If we got a filename, display it
+ if (!fileStr.IsEmpty()) {
+ AutoTArray<nsString, 4> formatStrings = {fileStr, typeStr, widthStr,
+ heightStr};
+ FormatStringFromName(aFormatNames[eWithDimAndFile], formatStrings, title);
+ } else {
+ AutoTArray<nsString, 3> formatStrings = {typeStr, widthStr, heightStr};
+ FormatStringFromName(aFormatNames[eWithDim], formatStrings, title);
+ }
+ } else {
+ // If we got a filename, display it
+ if (!fileStr.IsEmpty()) {
+ AutoTArray<nsString, 2> formatStrings = {fileStr, typeStr};
+ FormatStringFromName(aFormatNames[eWithFile], formatStrings, title);
+ } else {
+ AutoTArray<nsString, 1> formatStrings = {typeStr};
+ FormatStringFromName(aFormatNames[eWithNoInfo], formatStrings, title);
+ }
+ }
+
+ // set it on the document
+ if (aStatus.IsEmpty()) {
+ IgnoredErrorResult ignored;
+ SetTitle(title, ignored);
+ } else {
+ nsAutoString titleWithStatus;
+ AutoTArray<nsString, 2> formatStrings;
+ formatStrings.AppendElement(title);
+ formatStrings.AppendElement(aStatus);
+ FormatStringFromName("TitleWithStatus", formatStrings, titleWithStatus);
+ SetTitle(titleWithStatus, IgnoreErrors());
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/MediaDocument.h b/dom/html/MediaDocument.h
new file mode 100644
index 0000000000..31c1658c97
--- /dev/null
+++ b/dom/html/MediaDocument.h
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaDocument_h
+#define mozilla_dom_MediaDocument_h
+
+#include "mozilla/Attributes.h"
+#include "nsHTMLDocument.h"
+#include "nsGenericHTMLElement.h"
+#include "nsIStreamListener.h"
+#include "nsIStringBundle.h"
+#include "nsIThreadRetargetableStreamListener.h"
+
+#define NSMEDIADOCUMENT_PROPERTIES_URI \
+ "chrome://global/locale/layout/MediaDocument.properties"
+
+#define NSMEDIADOCUMENT_PROPERTIES_URI_en_US \
+ "resource://gre/res/locale/layout/MediaDocument.properties"
+
+namespace mozilla::dom {
+
+class MediaDocument : public nsHTMLDocument {
+ public:
+ MediaDocument();
+ virtual ~MediaDocument();
+
+ // Subclasses need to override this.
+ enum MediaDocumentKind MediaDocumentKind() const override = 0;
+
+ virtual nsresult Init(nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) override;
+
+ virtual nsresult StartDocumentLoad(const char* aCommand, nsIChannel* aChannel,
+ nsILoadGroup* aLoadGroup,
+ nsISupports* aContainer,
+ nsIStreamListener** aDocListener,
+ bool aReset = true) override;
+
+ virtual bool WillIgnoreCharsetOverride() override { return true; }
+
+ protected:
+ // Hook to be called once our initial document setup is done. Subclasses
+ // should call this from SetScriptGlobalObject when setup hasn't been done
+ // yet, a non-null script global is being set, and they have finished whatever
+ // setup work they plan to do for an initial load.
+ void InitialSetupDone();
+
+ // Check whether initial setup has been done.
+ [[nodiscard]] bool InitialSetupHasBeenDone() const {
+ return mDidInitialDocumentSetup;
+ }
+
+ virtual nsresult CreateSyntheticDocument();
+
+ friend class MediaDocumentStreamListener;
+ virtual nsresult StartLayout();
+
+ void GetFileName(nsAString& aResult, nsIChannel* aChannel);
+
+ nsresult LinkStylesheet(const nsAString& aStylesheet);
+ nsresult LinkScript(const nsAString& aScript);
+
+ void FormatStringFromName(const char* aName,
+ const nsTArray<nsString>& aParams,
+ nsAString& aResult);
+
+ // |aFormatNames[]| needs to have four elements in the following order:
+ // a format name with neither dimension nor file, a format name with
+ // filename but w/o dimension, a format name with dimension but w/o filename,
+ // a format name with both of them. For instance, it can have
+ // "ImageTitleWithNeitherDimensionsNorFile", "ImageTitleWithoutDimensions",
+ // "ImageTitleWithDimesions2", "ImageTitleWithDimensions2AndFile".
+ //
+ // Also see MediaDocument.properties if you want to define format names
+ // for a new subclass. aWidth and aHeight are pixels for |ImageDocument|,
+ // but could be in other units for other 'media', in which case you have to
+ // define format names accordingly.
+ void UpdateTitleAndCharset(const nsACString& aTypeStr, nsIChannel* aChannel,
+ const char* const* aFormatNames = sFormatNames,
+ int32_t aWidth = 0, int32_t aHeight = 0,
+ const nsAString& aStatus = u""_ns);
+
+ nsCOMPtr<nsIStringBundle> mStringBundle;
+ nsCOMPtr<nsIStringBundle> mStringBundleEnglish;
+ static const char* const sFormatNames[4];
+
+ private:
+ enum { eWithNoInfo, eWithFile, eWithDim, eWithDimAndFile };
+
+ // A boolean that indicates whether we did our initial document setup. This
+ // will be false initially, become true when we finish setting up the document
+ // during initial load and stay true thereafter.
+ bool mDidInitialDocumentSetup;
+};
+
+class MediaDocumentStreamListener : public nsIThreadRetargetableStreamListener {
+ protected:
+ virtual ~MediaDocumentStreamListener();
+
+ public:
+ explicit MediaDocumentStreamListener(MediaDocument* aDocument);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIREQUESTOBSERVER
+
+ NS_DECL_NSISTREAMLISTENER
+
+ NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
+
+ void DropDocumentRef() { mDocument = nullptr; }
+
+ RefPtr<MediaDocument> mDocument;
+ nsCOMPtr<nsIStreamListener> mNextStream;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_MediaDocument_h */
diff --git a/dom/html/MediaError.cpp b/dom/html/MediaError.cpp
new file mode 100644
index 0000000000..819e1745c6
--- /dev/null
+++ b/dom/html/MediaError.cpp
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaError.h"
+
+#include <string>
+#include <unordered_set>
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MediaErrorBinding.h"
+#include "nsContentUtils.h"
+#include "nsIScriptError.h"
+#include "jsapi.h"
+#include "js/Warnings.h" // JS::WarnASCII
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaError, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaError)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaError)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaError)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+MediaError::MediaError(HTMLMediaElement* aParent, uint16_t aCode,
+ const nsACString& aMessage)
+ : mParent(aParent), mCode(aCode), mMessage(aMessage) {}
+
+void MediaError::GetMessage(nsAString& aResult) const {
+ // When fingerprinting resistance is enabled, only messages in this list
+ // can be returned to content script.
+ // FIXME: An unordered_set seems overkill for this.
+ static const std::unordered_set<std::string> whitelist = {
+ "404: Not Found"
+ // TODO
+ };
+
+ const bool shouldBlank = whitelist.find(mMessage.get()) == whitelist.end();
+
+ if (shouldBlank) {
+ // Print a warning message to JavaScript console to alert developers of
+ // a non-whitelisted error message.
+ nsAutoCString message =
+ nsLiteralCString(
+ "This error message will be blank when "
+ "privacy.resistFingerprinting = true."
+ " If it is really necessary, please add it to the whitelist in"
+ " MediaError::GetMessage: ") +
+ mMessage;
+ Document* ownerDoc = mParent->OwnerDoc();
+ AutoJSAPI api;
+ if (api.Init(ownerDoc->GetScopeObject())) {
+ // We prefer this API because it can also print to our debug log and
+ // try server's log viewer.
+ JS::WarnASCII(api.cx(), "%s", message.get());
+ } else {
+ // If failed to use JS::WarnASCII, fall back to
+ // nsContentUtils::ReportToConsoleNonLocalized, which can only print to
+ // JavaScript console.
+ nsContentUtils::ReportToConsoleNonLocalized(
+ NS_ConvertASCIItoUTF16(message), nsIScriptError::warningFlag,
+ "MediaError"_ns, ownerDoc);
+ }
+
+ if (!nsContentUtils::IsCallerChrome() &&
+ ownerDoc->ShouldResistFingerprinting(RFPTarget::MediaError)) {
+ aResult.Truncate();
+ return;
+ }
+ }
+
+ CopyUTF8toUTF16(mMessage, aResult);
+}
+
+JSObject* MediaError::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaError_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/MediaError.h b/dom/html/MediaError.h
new file mode 100644
index 0000000000..aac1df118d
--- /dev/null
+++ b/dom/html/MediaError.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaError_h
+#define mozilla_dom_MediaError_h
+
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "nsWrapperCache.h"
+#include "nsISupports.h"
+#include "mozilla/Attributes.h"
+
+namespace mozilla::dom {
+
+class MediaError final : public nsISupports, public nsWrapperCache {
+ ~MediaError() = default;
+
+ public:
+ MediaError(HTMLMediaElement* aParent, uint16_t aCode,
+ const nsACString& aMessage = nsCString());
+
+ // nsISupports
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaError)
+
+ HTMLMediaElement* GetParentObject() const { return mParent; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint16_t Code() const { return mCode; }
+
+ void GetMessage(nsAString& aResult) const;
+
+ private:
+ RefPtr<HTMLMediaElement> mParent;
+
+ // Error code
+ const uint16_t mCode;
+ // Error details;
+ const nsCString mMessage;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MediaError_h
diff --git a/dom/html/PlayPromise.cpp b/dom/html/PlayPromise.cpp
new file mode 100644
index 0000000000..ee33b3ad68
--- /dev/null
+++ b/dom/html/PlayPromise.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/PlayPromise.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Telemetry.h"
+
+extern mozilla::LazyLogModule gMediaElementLog;
+
+#define PLAY_PROMISE_LOG(msg, ...) \
+ MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+PlayPromise::PlayPromise(nsIGlobalObject* aGlobal) : Promise(aGlobal) {}
+
+PlayPromise::~PlayPromise() {
+ if (!mFulfilled && PromiseObj()) {
+ MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ }
+}
+
+/* static */
+already_AddRefed<PlayPromise> PlayPromise::Create(nsIGlobalObject* aGlobal,
+ ErrorResult& aRv) {
+ RefPtr<PlayPromise> promise = new PlayPromise(aGlobal);
+ promise->CreateWrapper(aRv);
+ return aRv.Failed() ? nullptr : promise.forget();
+}
+
+/* static */
+void PlayPromise::ResolvePromisesWithUndefined(
+ const PlayPromiseArr& aPromises) {
+ for (const auto& promise : aPromises) {
+ promise->MaybeResolveWithUndefined();
+ }
+}
+
+/* static */
+void PlayPromise::RejectPromises(const PlayPromiseArr& aPromises,
+ nsresult aError) {
+ for (const auto& promise : aPromises) {
+ promise->MaybeReject(aError);
+ }
+}
+
+void PlayPromise::MaybeResolveWithUndefined() {
+ if (mFulfilled) {
+ return;
+ }
+ mFulfilled = true;
+ PLAY_PROMISE_LOG("PlayPromise %p resolved with undefined", this);
+ Promise::MaybeResolveWithUndefined();
+}
+
+static const char* ToPlayResultStr(nsresult aReason) {
+ switch (aReason) {
+ case NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR:
+ return "NotAllowedErr";
+ case NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR:
+ return "SrcNotSupportedErr";
+ case NS_ERROR_DOM_MEDIA_ABORT_ERR:
+ return "PauseAbortErr";
+ case NS_ERROR_DOM_ABORT_ERR:
+ return "AbortErr";
+ default:
+ return "UnknownErr";
+ }
+}
+
+void PlayPromise::MaybeReject(nsresult aReason) {
+ if (mFulfilled) {
+ return;
+ }
+ mFulfilled = true;
+ PLAY_PROMISE_LOG("PlayPromise %p rejected with 0x%x (%s)", this,
+ static_cast<uint32_t>(aReason), ToPlayResultStr(aReason));
+ Promise::MaybeReject(aReason);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/PlayPromise.h b/dom/html/PlayPromise.h
new file mode 100644
index 0000000000..9684f926df
--- /dev/null
+++ b/dom/html/PlayPromise.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __PlayPromise_h__
+#define __PlayPromise_h__
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Telemetry.h"
+
+namespace mozilla::dom {
+
+// Decorates a DOM Promise to report telemetry as to whether it was resolved
+// or rejected and why.
+class PlayPromise : public Promise {
+ public:
+ static already_AddRefed<PlayPromise> Create(nsIGlobalObject* aGlobal,
+ ErrorResult& aRv);
+
+ using PlayPromiseArr = nsTArray<RefPtr<PlayPromise>>;
+ static void ResolvePromisesWithUndefined(const PlayPromiseArr& aPromises);
+ static void RejectPromises(const PlayPromiseArr& aPromises, nsresult aError);
+
+ ~PlayPromise();
+ void MaybeResolveWithUndefined();
+ void MaybeReject(nsresult aReason);
+
+ private:
+ explicit PlayPromise(nsIGlobalObject* aGlobal);
+ bool mFulfilled = false;
+};
+
+} // namespace mozilla::dom
+
+#endif // __PlayPromise_h__
diff --git a/dom/html/RadioNodeList.cpp b/dom/html/RadioNodeList.cpp
new file mode 100644
index 0000000000..a6c0d55929
--- /dev/null
+++ b/dom/html/RadioNodeList.cpp
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/RadioNodeList.h"
+
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/RadioNodeListBinding.h"
+#include "js/TypeDecls.h"
+
+#include "HTMLInputElement.h"
+
+namespace mozilla::dom {
+
+/* virtual */
+JSObject* RadioNodeList::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return RadioNodeList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+HTMLInputElement* GetAsRadio(nsIContent* node) {
+ auto* el = HTMLInputElement::FromNode(node);
+ if (el && el->ControlType() == FormControlType::InputRadio) {
+ return el;
+ }
+ return nullptr;
+}
+
+void RadioNodeList::GetValue(nsString& retval, CallerType aCallerType) {
+ for (uint32_t i = 0; i < Length(); i++) {
+ HTMLInputElement* maybeRadio = GetAsRadio(Item(i));
+ if (maybeRadio && maybeRadio->Checked()) {
+ maybeRadio->GetValue(retval, aCallerType);
+ return;
+ }
+ }
+ retval.Truncate();
+}
+
+void RadioNodeList::SetValue(const nsAString& value, CallerType aCallerType) {
+ for (uint32_t i = 0; i < Length(); i++) {
+ HTMLInputElement* maybeRadio = GetAsRadio(Item(i));
+ if (!maybeRadio) {
+ continue;
+ }
+
+ nsString curval = nsString();
+ maybeRadio->GetValue(curval, aCallerType);
+ if (curval.Equals(value)) {
+ maybeRadio->SetChecked(true);
+ return;
+ }
+ }
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(RadioNodeList, nsSimpleContentList, RadioNodeList)
+
+} // namespace mozilla::dom
diff --git a/dom/html/RadioNodeList.h b/dom/html/RadioNodeList.h
new file mode 100644
index 0000000000..965bbb007c
--- /dev/null
+++ b/dom/html/RadioNodeList.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_RadioNodeList_h
+#define mozilla_dom_RadioNodeList_h
+
+#include "nsContentList.h"
+#include "nsCOMPtr.h"
+#include "HTMLFormElement.h"
+#include "mozilla/dom/BindingDeclarations.h"
+
+#define MOZILLA_DOM_RADIONODELIST_IMPLEMENTATION_IID \
+ { \
+ 0xbba7f3e8, 0xf3b5, 0x42e5, { \
+ 0x82, 0x08, 0xa6, 0x8b, 0xe0, 0xbc, 0x22, 0x19 \
+ } \
+ }
+
+namespace mozilla::dom {
+
+class RadioNodeList final : public nsSimpleContentList {
+ public:
+ explicit RadioNodeList(HTMLFormElement* aForm) : nsSimpleContentList(aForm) {}
+
+ virtual JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ void GetValue(nsString& retval, CallerType aCallerType);
+ void SetValue(const nsAString& value, CallerType aCallerType);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECLARE_STATIC_IID_ACCESSOR(MOZILLA_DOM_RADIONODELIST_IMPLEMENTATION_IID)
+ private:
+ ~RadioNodeList() = default;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(RadioNodeList,
+ MOZILLA_DOM_RADIONODELIST_IMPLEMENTATION_IID)
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_RadioNodeList_h
diff --git a/dom/html/TextControlElement.h b/dom/html/TextControlElement.h
new file mode 100644
index 0000000000..abca471953
--- /dev/null
+++ b/dom/html/TextControlElement.h
@@ -0,0 +1,249 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_TextControlElement_h
+#define mozilla_TextControlElement_h
+
+#include "mozilla/dom/FromParser.h"
+#include "mozilla/dom/NodeInfo.h"
+#include "nsGenericHTMLElement.h"
+
+class nsIContent;
+class nsISelectionController;
+class nsFrameSelection;
+class nsTextControlFrame;
+
+namespace mozilla {
+
+class ErrorResult;
+class TextControlState;
+class TextEditor;
+
+/**
+ * This abstract class is used for the text control frame to get the editor and
+ * selection controller objects, and some helper properties.
+ */
+class TextControlElement : public nsGenericHTMLFormControlElementWithState {
+ public:
+ TextControlElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo,
+ dom::FromParser aFromParser, FormControlType aType)
+ : nsGenericHTMLFormControlElementWithState(std::move(aNodeInfo),
+ aFromParser, aType){};
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
+ TextControlElement, nsGenericHTMLFormControlElementWithState)
+
+ /**
+ * Return true always, i.e., even if this is an <input> but the type is not
+ * for a single line text control, this returns true. Use
+ * IsSingleLineTextControlOrTextArea() if you want to know whether this may
+ * work with a TextEditor.
+ */
+ bool IsTextControlElement() const final { return true; }
+
+ virtual bool IsSingleLineTextControlOrTextArea() const = 0;
+
+ NS_IMPL_FROMNODE_HELPER(TextControlElement, IsTextControlElement())
+
+ /**
+ * Tell the control that value has been deliberately changed (or not).
+ */
+ virtual void SetValueChanged(bool) = 0;
+
+ /**
+ * Find out whether this is a single line text control. (text or password)
+ * @return whether this is a single line text control
+ */
+ virtual bool IsSingleLineTextControl() const = 0;
+
+ /**
+ * Find out whether this control is a textarea.
+ * @return whether this is a textarea text control
+ */
+ virtual bool IsTextArea() const = 0;
+
+ /**
+ * Find out whether this is a password control (input type=password)
+ * @return whether this is a password ontrol
+ */
+ virtual bool IsPasswordTextControl() const = 0;
+
+ /**
+ * Get the cols attribute (if textarea) or a default
+ * @return the number of columns to use
+ */
+ virtual int32_t GetCols() = 0;
+
+ /**
+ * Get the column index to wrap at, or -1 if we shouldn't wrap
+ */
+ virtual int32_t GetWrapCols() = 0;
+
+ /**
+ * Get the rows attribute (if textarea) or a default
+ * @return the number of rows to use
+ */
+ virtual int32_t GetRows() = 0;
+
+ /**
+ * Get the default value of the text control
+ */
+ virtual void GetDefaultValueFromContent(nsAString& aValue,
+ bool aForDisplay) = 0;
+
+ /**
+ * Return true if the value of the control has been changed.
+ */
+ virtual bool ValueChanged() const = 0;
+
+ /**
+ * Returns the used maxlength attribute value.
+ */
+ virtual int32_t UsedMaxLength() const = 0;
+
+ /**
+ * Get the current value of the text editor.
+ *
+ * @param aValue the buffer to retrieve the value in
+ */
+ virtual void GetTextEditorValue(nsAString& aValue) const = 0;
+
+ /**
+ * Get the editor object associated with the text editor.
+ * The return value is null if the control does not support an editor
+ * (for example, if it is a checkbox.)
+ * Note that GetTextEditor() creates editor if it hasn't been created yet.
+ * If you need editor only when the editor is there, you should use
+ * GetTextEditorWithoutCreation().
+ */
+ MOZ_CAN_RUN_SCRIPT virtual TextEditor* GetTextEditor() = 0;
+ virtual TextEditor* GetTextEditorWithoutCreation() const = 0;
+
+ /**
+ * Get the selection controller object associated with the text editor.
+ * The return value is null if the control does not support an editor
+ * (for example, if it is a checkbox.)
+ */
+ virtual nsISelectionController* GetSelectionController() = 0;
+
+ virtual nsFrameSelection* GetConstFrameSelection() = 0;
+
+ virtual TextControlState* GetTextControlState() const = 0;
+
+ /**
+ * Binds a frame to the text control. This is performed when a frame
+ * is created for the content node.
+ * Be aware, this must be called with script blocker.
+ */
+ virtual nsresult BindToFrame(nsTextControlFrame* aFrame) = 0;
+
+ /**
+ * Unbinds a frame from the text control. This is performed when a frame
+ * belonging to a content node is destroyed.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual void UnbindFromFrame(
+ nsTextControlFrame* aFrame) = 0;
+
+ /**
+ * Creates an editor for the text control. This should happen when
+ * a frame has been created for the text control element, but the created
+ * editor may outlive the frame itself.
+ */
+ MOZ_CAN_RUN_SCRIPT virtual nsresult CreateEditor() = 0;
+
+ /**
+ * Update preview value for the text control.
+ */
+ virtual void SetPreviewValue(const nsAString& aValue) = 0;
+
+ /**
+ * Get the current preview value for text control.
+ */
+ virtual void GetPreviewValue(nsAString& aValue) = 0;
+
+ /**
+ * Enable preview for text control.
+ */
+ virtual void EnablePreview() = 0;
+
+ /**
+ * Find out whether this control enables preview for form autofoll.
+ */
+ virtual bool IsPreviewEnabled() = 0;
+
+ /**
+ * Initialize the keyboard event listeners.
+ */
+ virtual void InitializeKeyboardEventListeners() = 0;
+
+ enum class ValueChangeKind {
+ Internal,
+ Script,
+ UserInteraction,
+ };
+
+ /**
+ * Callback called whenever the value is changed.
+ *
+ * aKnownNewValue can be used to avoid value lookups if present (might be
+ * null, if the caller doesn't know the specific value that got set).
+ */
+ virtual void OnValueChanged(ValueChangeKind, bool aNewValueEmpty,
+ const nsAString* aKnownNewValue) = 0;
+
+ void OnValueChanged(ValueChangeKind aKind, const nsAString& aNewValue) {
+ return OnValueChanged(aKind, aNewValue.IsEmpty(), &aNewValue);
+ }
+
+ /**
+ * Helpers for value manipulation from SetRangeText.
+ */
+ virtual void GetValueFromSetRangeText(nsAString& aValue) = 0;
+ MOZ_CAN_RUN_SCRIPT virtual nsresult SetValueFromSetRangeText(
+ const nsAString& aValue) = 0;
+
+ static const int32_t DEFAULT_COLS = 20;
+ static const int32_t DEFAULT_ROWS = 1;
+ static const int32_t DEFAULT_ROWS_TEXTAREA = 2;
+ static const int32_t DEFAULT_UNDO_CAP = 1000;
+
+ // wrap can be one of these three values.
+ typedef enum {
+ eHTMLTextWrap_Off = 1, // "off"
+ eHTMLTextWrap_Hard = 2, // "hard"
+ eHTMLTextWrap_Soft = 3 // the default
+ } nsHTMLTextWrap;
+
+ static bool GetWrapPropertyEnum(nsIContent* aContent,
+ nsHTMLTextWrap& aWrapProp);
+
+ /**
+ * Does the editor have a selection cache?
+ *
+ * Note that this function has the side effect of making the editor for input
+ * elements be initialized eagerly.
+ */
+ virtual bool HasCachedSelection() = 0;
+
+ static already_AddRefed<TextControlElement>
+ GetTextControlElementFromEditingHost(nsIContent* aHost);
+
+ protected:
+ virtual ~TextControlElement() = default;
+
+ // The focusability state of this form control. eUnfocusable means that it
+ // shouldn't be focused at all, eInactiveWindow means it's in an inactive
+ // window, eActiveWindow means it's in an active window.
+ enum class FocusTristate { eUnfocusable, eInactiveWindow, eActiveWindow };
+
+ // Get our focus state.
+ FocusTristate FocusState();
+};
+
+} // namespace mozilla
+
+#endif // mozilla_TextControlElement_h
diff --git a/dom/html/TextControlState.cpp b/dom/html/TextControlState.cpp
new file mode 100644
index 0000000000..3e2c06c53a
--- /dev/null
+++ b/dom/html/TextControlState.cpp
@@ -0,0 +1,3087 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TextControlState.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/CaretAssociationHint.h"
+#include "mozilla/IMEContentObserver.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/TextInputListener.h"
+
+#include "nsCOMPtr.h"
+#include "nsView.h"
+#include "nsCaret.h"
+#include "nsLayoutCID.h"
+#include "nsITextControlFrame.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsTextControlFrame.h"
+#include "nsIControllers.h"
+#include "nsIControllerContext.h"
+#include "nsAttrValue.h"
+#include "nsAttrValueInlines.h"
+#include "nsGenericHTMLElement.h"
+#include "nsIDOMEventListener.h"
+#include "nsIWidget.h"
+#include "nsIDocumentEncoder.h"
+#include "nsPIDOMWindow.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/EventListenerManager.h"
+#include "nsContentUtils.h"
+#include "mozilla/Preferences.h"
+#include "nsTextNode.h"
+#include "nsIController.h"
+#include "nsIScrollableFrame.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/InputEventOptions.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/HTMLTextAreaElement.h"
+#include "mozilla/dom/Text.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "nsFrameSelection.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/ShortcutKeys.h"
+#include "mozilla/KeyEventHandler.h"
+#include "mozilla/dom/KeyboardEvent.h"
+#include "mozilla/ScrollTypes.h"
+
+namespace mozilla {
+
+using namespace dom;
+using ValueSetterOption = TextControlState::ValueSetterOption;
+using ValueSetterOptions = TextControlState::ValueSetterOptions;
+using SelectionDirection = nsITextControlFrame::SelectionDirection;
+
+/*****************************************************************************
+ * TextControlElement
+ *****************************************************************************/
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(TextControlElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(
+ TextControlElement, nsGenericHTMLFormControlElementWithState)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(
+ TextControlElement, nsGenericHTMLFormControlElementWithState)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(
+ TextControlElement, nsGenericHTMLFormControlElementWithState)
+
+/*static*/
+bool TextControlElement::GetWrapPropertyEnum(
+ nsIContent* aContent, TextControlElement::nsHTMLTextWrap& aWrapProp) {
+ // soft is the default; "physical" defaults to soft as well because all other
+ // browsers treat it that way and there is no real reason to maintain physical
+ // and virtual as separate entities if no one else does. Only hard and off
+ // do anything different.
+ aWrapProp = eHTMLTextWrap_Soft; // the default
+
+ if (!aContent->IsHTMLElement()) {
+ return false;
+ }
+
+ static mozilla::dom::Element::AttrValuesArray strings[] = {
+ nsGkAtoms::HARD, nsGkAtoms::OFF, nullptr};
+ switch (aContent->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::wrap, strings, eIgnoreCase)) {
+ case 0:
+ aWrapProp = eHTMLTextWrap_Hard;
+ break;
+ case 1:
+ aWrapProp = eHTMLTextWrap_Off;
+ break;
+ }
+
+ return true;
+}
+
+/*static*/
+already_AddRefed<TextControlElement>
+TextControlElement::GetTextControlElementFromEditingHost(nsIContent* aHost) {
+ if (!aHost) {
+ return nullptr;
+ }
+
+ RefPtr<TextControlElement> parent =
+ TextControlElement::FromNodeOrNull(aHost->GetParent());
+ return parent.forget();
+}
+
+TextControlElement::FocusTristate TextControlElement::FocusState() {
+ // We can't be focused if we aren't in a (composed) document
+ Document* doc = GetComposedDoc();
+ if (!doc) {
+ return FocusTristate::eUnfocusable;
+ }
+
+ // first see if we are disabled or not. If disabled then do nothing.
+ if (IsDisabled()) {
+ return FocusTristate::eUnfocusable;
+ }
+
+ return IsInActiveTab(doc) ? FocusTristate::eActiveWindow
+ : FocusTristate::eInactiveWindow;
+}
+
+using ValueChangeKind = TextControlElement::ValueChangeKind;
+
+MOZ_CAN_RUN_SCRIPT inline nsresult SetEditorFlagsIfNecessary(
+ EditorBase& aEditorBase, uint32_t aFlags) {
+ if (aEditorBase.Flags() == aFlags) {
+ return NS_OK;
+ }
+ return aEditorBase.SetFlags(aFlags);
+}
+
+/*****************************************************************************
+ * mozilla::AutoInputEventSuppresser
+ *****************************************************************************/
+
+class MOZ_STACK_CLASS AutoInputEventSuppresser final {
+ public:
+ explicit AutoInputEventSuppresser(TextEditor* aTextEditor)
+ : mTextEditor(aTextEditor),
+ // To protect against a reentrant call to SetValue, we check whether
+ // another SetValue is already happening for this editor. If it is,
+ // we must wait until we unwind to re-enable oninput events.
+ mOuterTransaction(aTextEditor->IsSuppressingDispatchingInputEvent()) {
+ MOZ_ASSERT(mTextEditor);
+ mTextEditor->SuppressDispatchingInputEvent(true);
+ }
+ ~AutoInputEventSuppresser() {
+ mTextEditor->SuppressDispatchingInputEvent(mOuterTransaction);
+ }
+
+ private:
+ RefPtr<TextEditor> mTextEditor;
+ bool mOuterTransaction;
+};
+
+/*****************************************************************************
+ * mozilla::RestoreSelectionState
+ *****************************************************************************/
+
+class RestoreSelectionState : public Runnable {
+ public:
+ RestoreSelectionState(TextControlState* aState, nsTextControlFrame* aFrame)
+ : Runnable("RestoreSelectionState"),
+ mFrame(aFrame),
+ mTextControlState(aState) {}
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ if (!mTextControlState) {
+ return NS_OK;
+ }
+
+ AutoHideSelectionChanges hideSelectionChanges(
+ mFrame->GetConstFrameSelection());
+
+ if (mFrame) {
+ // EnsureEditorInitialized and SetSelectionRange leads to
+ // Selection::AddRangeAndSelectFramesAndNotifyListeners which flushes
+ // Layout - need to block script to avoid nested PrepareEditor calls (bug
+ // 642800).
+ nsAutoScriptBlocker scriptBlocker;
+ mFrame->EnsureEditorInitialized();
+ TextControlState::SelectionProperties& properties =
+ mTextControlState->GetSelectionProperties();
+ if (properties.IsDirty()) {
+ mFrame->SetSelectionRange(properties.GetStart(), properties.GetEnd(),
+ properties.GetDirection());
+ }
+ }
+
+ if (mTextControlState) {
+ mTextControlState->FinishedRestoringSelection();
+ }
+ return NS_OK;
+ }
+
+ // Let the text editor tell us we're no longer relevant - avoids use of
+ // AutoWeakFrame
+ void Revoke() {
+ mFrame = nullptr;
+ mTextControlState = nullptr;
+ }
+
+ private:
+ nsTextControlFrame* mFrame;
+ TextControlState* mTextControlState;
+};
+
+/*****************************************************************************
+ * mozilla::AutoRestoreEditorState
+ *****************************************************************************/
+
+class MOZ_RAII AutoRestoreEditorState final {
+ public:
+ MOZ_CAN_RUN_SCRIPT explicit AutoRestoreEditorState(TextEditor* aTextEditor)
+ : mTextEditor(aTextEditor),
+ mSavedFlags(mTextEditor->Flags()),
+ mSavedMaxLength(mTextEditor->MaxTextLength()),
+ mSavedEchoingPasswordPrevented(
+ mTextEditor->EchoingPasswordPrevented()) {
+ MOZ_ASSERT(mTextEditor);
+
+ // EditorBase::SetFlags() is a virtual method. Even though it does nothing
+ // if new flags and current flags are same, the calling cost causes
+ // appearing the method in profile. So, this class should check if it's
+ // necessary to call.
+ uint32_t flags = mSavedFlags;
+ flags &= ~nsIEditor::eEditorReadonlyMask;
+ if (mSavedFlags != flags) {
+ // It's aTextEditor and whose lifetime must be guaranteed by the caller.
+ MOZ_KnownLive(mTextEditor)->SetFlags(flags);
+ }
+ mTextEditor->PreventToEchoPassword();
+ mTextEditor->SetMaxTextLength(-1);
+ }
+
+ MOZ_CAN_RUN_SCRIPT ~AutoRestoreEditorState() {
+ if (!mSavedEchoingPasswordPrevented) {
+ mTextEditor->AllowToEchoPassword();
+ }
+ mTextEditor->SetMaxTextLength(mSavedMaxLength);
+ // mTextEditor's lifetime must be guaranteed by owner of the instance
+ // since the constructor is marked as `MOZ_CAN_RUN_SCRIPT` and this is
+ // a stack only class.
+ SetEditorFlagsIfNecessary(MOZ_KnownLive(*mTextEditor), mSavedFlags);
+ }
+
+ private:
+ TextEditor* mTextEditor;
+ uint32_t mSavedFlags;
+ int32_t mSavedMaxLength;
+ bool mSavedEchoingPasswordPrevented;
+};
+
+/*****************************************************************************
+ * mozilla::AutoDisableUndo
+ *****************************************************************************/
+
+class MOZ_RAII AutoDisableUndo final {
+ public:
+ explicit AutoDisableUndo(TextEditor* aTextEditor)
+ : mTextEditor(aTextEditor), mNumberOfMaximumTransactions(0) {
+ MOZ_ASSERT(mTextEditor);
+
+ mNumberOfMaximumTransactions =
+ mTextEditor ? mTextEditor->NumberOfMaximumTransactions() : 0;
+ DebugOnly<bool> disabledUndoRedo = mTextEditor->DisableUndoRedo();
+ NS_WARNING_ASSERTION(disabledUndoRedo,
+ "Failed to disable undo/redo transactions");
+ }
+
+ ~AutoDisableUndo() {
+ // Don't change enable/disable of undo/redo if it's enabled after
+ // it's disabled by the constructor because we shouldn't change
+ // the maximum undo/redo count to the old value.
+ if (mTextEditor->IsUndoRedoEnabled()) {
+ return;
+ }
+ // If undo/redo was enabled, mNumberOfMaximumTransactions is -1 or lager
+ // than 0. Only when it's 0, it was disabled.
+ if (mNumberOfMaximumTransactions) {
+ DebugOnly<bool> enabledUndoRedo =
+ mTextEditor->EnableUndoRedo(mNumberOfMaximumTransactions);
+ NS_WARNING_ASSERTION(enabledUndoRedo,
+ "Failed to enable undo/redo transactions");
+ } else {
+ DebugOnly<bool> disabledUndoRedo = mTextEditor->DisableUndoRedo();
+ NS_WARNING_ASSERTION(disabledUndoRedo,
+ "Failed to disable undo/redo transactions");
+ }
+ }
+
+ private:
+ TextEditor* mTextEditor;
+ int32_t mNumberOfMaximumTransactions;
+};
+
+static bool SuppressEventHandlers(nsPresContext* aPresContext) {
+ bool suppressHandlers = false;
+
+ if (aPresContext) {
+ // Right now we only suppress event handlers and controller manipulation
+ // when in a print preview or print context!
+
+ // In the current implementation, we only paginate when
+ // printing or in print preview.
+
+ suppressHandlers = aPresContext->IsPaginated();
+ }
+
+ return suppressHandlers;
+}
+
+/*****************************************************************************
+ * mozilla::TextInputSelectionController
+ *****************************************************************************/
+
+class TextInputSelectionController final : public nsSupportsWeakReference,
+ public nsISelectionController {
+ ~TextInputSelectionController() = default;
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TextInputSelectionController,
+ nsISelectionController)
+
+ TextInputSelectionController(PresShell* aPresShell, nsIContent* aLimiter);
+
+ void SetScrollableFrame(nsIScrollableFrame* aScrollableFrame);
+ nsFrameSelection* GetConstFrameSelection() { return mFrameSelection; }
+ // Will return null if !mFrameSelection.
+ Selection* GetSelection(SelectionType aSelectionType);
+
+ // NSISELECTIONCONTROLLER INTERFACES
+ NS_IMETHOD SetDisplaySelection(int16_t toggle) override;
+ NS_IMETHOD GetDisplaySelection(int16_t* _retval) override;
+ NS_IMETHOD SetSelectionFlags(int16_t aInEnable) override;
+ NS_IMETHOD GetSelectionFlags(int16_t* aOutEnable) override;
+ NS_IMETHOD GetSelectionFromScript(RawSelectionType aRawSelectionType,
+ Selection** aSelection) override;
+ Selection* GetSelection(RawSelectionType aRawSelectionType) override;
+ NS_IMETHOD ScrollSelectionIntoView(RawSelectionType aRawSelectionType,
+ int16_t aRegion, int16_t aFlags) override;
+ NS_IMETHOD RepaintSelection(RawSelectionType aRawSelectionType) override;
+ nsresult RepaintSelection(nsPresContext* aPresContext,
+ SelectionType aSelectionType);
+ NS_IMETHOD SetCaretEnabled(bool enabled) override;
+ NS_IMETHOD SetCaretReadOnly(bool aReadOnly) override;
+ NS_IMETHOD GetCaretEnabled(bool* _retval) override;
+ NS_IMETHOD GetCaretVisible(bool* _retval) override;
+ NS_IMETHOD SetCaretVisibilityDuringSelection(bool aVisibility) override;
+ NS_IMETHOD PhysicalMove(int16_t aDirection, int16_t aAmount,
+ bool aExtend) override;
+ NS_IMETHOD CharacterMove(bool aForward, bool aExtend) override;
+ NS_IMETHOD WordMove(bool aForward, bool aExtend) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD LineMove(bool aForward,
+ bool aExtend) override;
+ NS_IMETHOD IntraLineMove(bool aForward, bool aExtend) override;
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD PageMove(bool aForward, bool aExtend) override;
+ NS_IMETHOD CompleteScroll(bool aForward) override;
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD CompleteMove(bool aForward,
+ bool aExtend) override;
+ NS_IMETHOD ScrollPage(bool aForward) override;
+ NS_IMETHOD ScrollLine(bool aForward) override;
+ NS_IMETHOD ScrollCharacter(bool aRight) override;
+ void SelectionWillTakeFocus() override;
+ void SelectionWillLoseFocus() override;
+
+ private:
+ RefPtr<nsFrameSelection> mFrameSelection;
+ nsIScrollableFrame* mScrollFrame;
+ nsWeakPtr mPresShellWeak;
+};
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TextInputSelectionController)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TextInputSelectionController)
+NS_INTERFACE_TABLE_HEAD(TextInputSelectionController)
+ NS_INTERFACE_TABLE(TextInputSelectionController, nsISelectionController,
+ nsISelectionDisplay, nsISupportsWeakReference)
+ NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(TextInputSelectionController)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_WEAK(TextInputSelectionController, mFrameSelection)
+
+TextInputSelectionController::TextInputSelectionController(
+ PresShell* aPresShell, nsIContent* aLimiter)
+ : mScrollFrame(nullptr) {
+ if (aPresShell) {
+ bool accessibleCaretEnabled =
+ PresShell::AccessibleCaretEnabled(aLimiter->OwnerDoc()->GetDocShell());
+ mFrameSelection =
+ new nsFrameSelection(aPresShell, aLimiter, accessibleCaretEnabled);
+ mPresShellWeak = do_GetWeakReference(aPresShell);
+ }
+}
+
+void TextInputSelectionController::SetScrollableFrame(
+ nsIScrollableFrame* aScrollableFrame) {
+ mScrollFrame = aScrollableFrame;
+ if (!mScrollFrame && mFrameSelection) {
+ mFrameSelection->DisconnectFromPresShell();
+ mFrameSelection = nullptr;
+ }
+}
+
+Selection* TextInputSelectionController::GetSelection(
+ SelectionType aSelectionType) {
+ if (!mFrameSelection) {
+ return nullptr;
+ }
+
+ return mFrameSelection->GetSelection(aSelectionType);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::SetDisplaySelection(int16_t aToggle) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ mFrameSelection->SetDisplaySelection(aToggle);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::GetDisplaySelection(int16_t* aToggle) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ *aToggle = mFrameSelection->GetDisplaySelection();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::SetSelectionFlags(int16_t aToggle) {
+ return NS_OK; // stub this out. not used in input
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::GetSelectionFlags(int16_t* aOutEnable) {
+ *aOutEnable = nsISelectionDisplay::DISPLAY_TEXT;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::GetSelectionFromScript(
+ RawSelectionType aRawSelectionType, Selection** aSelection) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ *aSelection =
+ mFrameSelection->GetSelection(ToSelectionType(aRawSelectionType));
+
+ // GetSelection() fails only when aRawSelectionType is invalid value.
+ if (!(*aSelection)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ NS_ADDREF(*aSelection);
+ return NS_OK;
+}
+
+Selection* TextInputSelectionController::GetSelection(
+ RawSelectionType aRawSelectionType) {
+ return GetSelection(ToSelectionType(aRawSelectionType));
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::ScrollSelectionIntoView(
+ RawSelectionType aRawSelectionType, int16_t aRegion, int16_t aFlags) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ return frameSelection->ScrollSelectionIntoView(
+ ToSelectionType(aRawSelectionType), aRegion, aFlags);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::RepaintSelection(
+ RawSelectionType aRawSelectionType) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ return frameSelection->RepaintSelection(ToSelectionType(aRawSelectionType));
+}
+
+nsresult TextInputSelectionController::RepaintSelection(
+ nsPresContext* aPresContext, SelectionType aSelectionType) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ return frameSelection->RepaintSelection(aSelectionType);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::SetCaretEnabled(bool enabled) {
+ if (!mPresShellWeak) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak);
+ if (!presShell) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // tell the pres shell to enable the caret, rather than settings its
+ // visibility directly. this way the presShell's idea of caret visibility is
+ // maintained.
+ presShell->SetCaretEnabled(enabled);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::SetCaretReadOnly(bool aReadOnly) {
+ if (!mPresShellWeak) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv;
+ RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak, &rv);
+ if (!presShell) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<nsCaret> caret = presShell->GetCaret();
+ if (!caret) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mFrameSelection) {
+ return NS_ERROR_FAILURE;
+ }
+
+ Selection* selection = mFrameSelection->GetSelection(SelectionType::eNormal);
+ if (selection) {
+ caret->SetCaretReadOnly(aReadOnly);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::GetCaretEnabled(bool* _retval) {
+ return GetCaretVisible(_retval);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::GetCaretVisible(bool* _retval) {
+ if (!mPresShellWeak) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv;
+ RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak, &rv);
+ if (!presShell) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<nsCaret> caret = presShell->GetCaret();
+ if (!caret) {
+ return NS_ERROR_FAILURE;
+ }
+ *_retval = caret->IsVisible();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::SetCaretVisibilityDuringSelection(
+ bool aVisibility) {
+ if (!mPresShellWeak) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv;
+ RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak, &rv);
+ if (!presShell) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<nsCaret> caret = presShell->GetCaret();
+ if (!caret) {
+ return NS_ERROR_FAILURE;
+ }
+ Selection* selection = mFrameSelection->GetSelection(SelectionType::eNormal);
+ if (selection) {
+ caret->SetVisibilityDuringSelection(aVisibility);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::PhysicalMove(int16_t aDirection, int16_t aAmount,
+ bool aExtend) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ return frameSelection->PhysicalMove(aDirection, aAmount, aExtend);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::CharacterMove(bool aForward, bool aExtend) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ return frameSelection->CharacterMove(aForward, aExtend);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::WordMove(bool aForward, bool aExtend) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ return frameSelection->WordMove(aForward, aExtend);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::LineMove(bool aForward, bool aExtend) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ nsresult result = frameSelection->LineMove(aForward, aExtend);
+ if (NS_FAILED(result)) {
+ result = CompleteMove(aForward, aExtend);
+ }
+ return result;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::IntraLineMove(bool aForward, bool aExtend) {
+ if (!mFrameSelection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ return frameSelection->IntraLineMove(aForward, aExtend);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::PageMove(bool aForward, bool aExtend) {
+ // expected behavior for PageMove is to scroll AND move the caret
+ // and to remain relative position of the caret in view. see Bug 4302.
+ if (mScrollFrame) {
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ nsIFrame* scrollFrame = do_QueryFrame(mScrollFrame);
+ // We won't scroll parent scrollable element of mScrollFrame. Therefore,
+ // this may be handled when mScrollFrame is completely outside of the view.
+ // In such case, user may be confused since they might have wanted to
+ // scroll a parent scrollable element. For making clearer which element
+ // handles PageDown/PageUp, we should move selection into view even if
+ // selection is not changed.
+ return frameSelection->PageMove(aForward, aExtend, scrollFrame,
+ nsFrameSelection::SelectionIntoView::Yes);
+ }
+ // Similarly, if there is no scrollable frame, we should move the editor
+ // frame into the view for making it clearer which element handles
+ // PageDown/PageUp.
+ return ScrollSelectionIntoView(
+ nsISelectionController::SELECTION_NORMAL,
+ nsISelectionController::SELECTION_FOCUS_REGION,
+ nsISelectionController::SCROLL_SYNCHRONOUS |
+ nsISelectionController::SCROLL_FOR_CARET_MOVE);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::CompleteScroll(bool aForward) {
+ if (!mScrollFrame) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mScrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), ScrollUnit::WHOLE,
+ ScrollMode::Instant);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::CompleteMove(bool aForward, bool aExtend) {
+ if (NS_WARN_IF(!mFrameSelection)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+
+ // grab the parent / root DIV for this text widget
+ nsIContent* parentDIV = frameSelection->GetLimiter();
+ if (!parentDIV) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // make the caret be either at the very beginning (0) or the very end
+ int32_t offset = 0;
+ CaretAssociationHint hint = CaretAssociationHint::Before;
+ if (aForward) {
+ offset = parentDIV->GetChildCount();
+
+ // Prevent the caret from being placed after the last
+ // BR node in the content tree!
+
+ if (offset > 0) {
+ nsIContent* child = parentDIV->GetLastChild();
+
+ if (child->IsHTMLElement(nsGkAtoms::br)) {
+ --offset;
+ hint = CaretAssociationHint::After; // for Bug 106855
+ }
+ }
+ }
+
+ const RefPtr<nsIContent> pinnedParentDIV{parentDIV};
+ const nsFrameSelection::FocusMode focusMode =
+ aExtend ? nsFrameSelection::FocusMode::kExtendSelection
+ : nsFrameSelection::FocusMode::kCollapseToNewPoint;
+ frameSelection->HandleClick(pinnedParentDIV, offset, offset, focusMode, hint);
+
+ // if we got this far, attempt to scroll no matter what the above result is
+ return CompleteScroll(aForward);
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::ScrollPage(bool aForward) {
+ if (!mScrollFrame) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mScrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), ScrollUnit::PAGES,
+ ScrollMode::Smooth);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::ScrollLine(bool aForward) {
+ if (!mScrollFrame) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mScrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), ScrollUnit::LINES,
+ ScrollMode::Smooth);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TextInputSelectionController::ScrollCharacter(bool aRight) {
+ if (!mScrollFrame) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mScrollFrame->ScrollBy(nsIntPoint(aRight ? 1 : -1, 0), ScrollUnit::LINES,
+ ScrollMode::Smooth);
+ return NS_OK;
+}
+
+void TextInputSelectionController::SelectionWillTakeFocus() {
+ if (mFrameSelection) {
+ if (PresShell* shell = mFrameSelection->GetPresShell()) {
+ shell->FrameSelectionWillTakeFocus(*mFrameSelection);
+ }
+ }
+}
+
+void TextInputSelectionController::SelectionWillLoseFocus() {
+ if (mFrameSelection) {
+ if (PresShell* shell = mFrameSelection->GetPresShell()) {
+ shell->FrameSelectionWillLoseFocus(*mFrameSelection);
+ }
+ }
+}
+
+/*****************************************************************************
+ * mozilla::TextInputListener
+ *****************************************************************************/
+
+TextInputListener::TextInputListener(TextControlElement* aTxtCtrlElement)
+ : mFrame(nullptr),
+ mTxtCtrlElement(aTxtCtrlElement),
+ mTextControlState(aTxtCtrlElement ? aTxtCtrlElement->GetTextControlState()
+ : nullptr),
+ mSelectionWasCollapsed(true),
+ mHadUndoItems(false),
+ mHadRedoItems(false),
+ mSettingValue(false),
+ mSetValueChanged(true),
+ mListeningToSelectionChange(false) {}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TextInputListener)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TextInputListener)
+
+NS_INTERFACE_MAP_BEGIN(TextInputListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener)
+ NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(TextInputListener)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(TextInputListener)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TextInputListener)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TextInputListener)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+void TextInputListener::OnSelectionChange(Selection& aSelection,
+ int16_t aReason) {
+ if (!mListeningToSelectionChange) {
+ return;
+ }
+
+ AutoWeakFrame weakFrame = mFrame;
+
+ // Fire the select event
+ // The specs don't exactly say when we should fire the select event.
+ // IE: Whenever you add/remove a character to/from the selection. Also
+ // each time for select all. Also if you get to the end of the text
+ // field you will get new event for each keypress or a continuous
+ // stream of events if you use the mouse. IE will fire select event
+ // when the selection collapses to nothing if you are holding down
+ // the shift or mouse button.
+ // Mozilla: If we have non-empty selection we will fire a new event for each
+ // keypress (or mouseup) if the selection changed. Mozilla will also
+ // create the event each time select all is called, even if
+ // everything was previously selected, because technically select all
+ // will first collapse and then extend. Mozilla will never create an
+ // event if the selection collapses to nothing.
+ // FYI: If you want to skip dispatching eFormSelect event and if there are no
+ // event listeners, you can refer
+ // nsPIDOMWindow::HasFormSelectEventListeners(), but be careful about
+ // some C++ event handlers, e.g., HTMLTextAreaElement::PostHandleEvent().
+ bool collapsed = aSelection.IsCollapsed();
+ if (!collapsed && (aReason & (nsISelectionListener::MOUSEUP_REASON |
+ nsISelectionListener::KEYPRESS_REASON |
+ nsISelectionListener::SELECTALL_REASON))) {
+ if (nsCOMPtr<nsIContent> content = mFrame->GetContent()) {
+ if (nsCOMPtr<Document> doc = content->GetComposedDoc()) {
+ if (RefPtr<PresShell> presShell = doc->GetPresShell()) {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetEvent event(true, eFormSelect);
+
+ presShell->HandleEventWithTarget(&event, mFrame, content, &status);
+ }
+ }
+ }
+ }
+
+ // if the collapsed state did not change, don't fire notifications
+ if (collapsed == mSelectionWasCollapsed) {
+ return;
+ }
+
+ mSelectionWasCollapsed = collapsed;
+
+ if (!weakFrame.IsAlive() || !mFrame ||
+ !nsContentUtils::IsFocusedContent(mFrame->GetContent())) {
+ return;
+ }
+
+ UpdateTextInputCommands(u"select"_ns);
+}
+
+MOZ_CAN_RUN_SCRIPT
+static void DoCommandCallback(Command aCommand, void* aData) {
+ nsTextControlFrame* frame = static_cast<nsTextControlFrame*>(aData);
+ nsIContent* content = frame->GetContent();
+
+ nsCOMPtr<nsIControllers> controllers;
+ HTMLInputElement* input = HTMLInputElement::FromNode(content);
+ if (input) {
+ input->GetControllers(getter_AddRefs(controllers));
+ } else {
+ HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNode(content);
+
+ if (textArea) {
+ textArea->GetControllers(getter_AddRefs(controllers));
+ }
+ }
+
+ if (!controllers) {
+ NS_WARNING("Could not get controllers");
+ return;
+ }
+
+ const char* commandStr = WidgetKeyboardEvent::GetCommandStr(aCommand);
+
+ nsCOMPtr<nsIController> controller;
+ controllers->GetControllerForCommand(commandStr, getter_AddRefs(controller));
+ if (!controller) {
+ return;
+ }
+
+ bool commandEnabled;
+ if (NS_WARN_IF(NS_FAILED(
+ controller->IsCommandEnabled(commandStr, &commandEnabled)))) {
+ return;
+ }
+ if (commandEnabled) {
+ controller->DoCommand(commandStr);
+ }
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+TextInputListener::HandleEvent(Event* aEvent) {
+ if (aEvent->DefaultPrevented()) {
+ return NS_OK;
+ }
+
+ if (!aEvent->IsTrusted()) {
+ return NS_OK;
+ }
+
+ RefPtr<KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent();
+ if (!keyEvent) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ WidgetKeyboardEvent* widgetKeyEvent =
+ aEvent->WidgetEventPtr()->AsKeyboardEvent();
+ if (!widgetKeyEvent) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ {
+ auto* input = HTMLInputElement::FromNode(mTxtCtrlElement);
+ if (input && input->StepsInputValue(*widgetKeyEvent)) {
+ // As an special case, don't handle key events that would step the value
+ // of our <input type=number>.
+ return NS_OK;
+ }
+ }
+
+ auto ExecuteOurShortcutKeys = [&](TextControlElement& aTextControlElement)
+ MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> bool {
+ KeyEventHandler* keyHandlers = ShortcutKeys::GetHandlers(
+ aTextControlElement.IsTextArea() ? HandlerType::eTextArea
+ : HandlerType::eInput);
+
+ RefPtr<nsAtom> eventTypeAtom =
+ ShortcutKeys::ConvertEventToDOMEventType(widgetKeyEvent);
+ for (KeyEventHandler* handler = keyHandlers; handler;
+ handler = handler->GetNextHandler()) {
+ if (!handler->EventTypeEquals(eventTypeAtom)) {
+ continue;
+ }
+
+ if (!handler->KeyEventMatched(keyEvent, 0, IgnoreModifierState())) {
+ continue;
+ }
+
+ // XXX Do we execute only one handler even if the handler neither stops
+ // propagation nor prevents default of the event?
+ nsresult rv = handler->ExecuteHandler(&aTextControlElement, aEvent);
+ if (NS_SUCCEEDED(rv)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ auto ExecuteNativeKeyBindings =
+ [&](TextControlElement& aTextControlElement)
+ MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> bool {
+ if (widgetKeyEvent->mMessage != eKeyPress) {
+ return false;
+ }
+
+ NativeKeyBindingsType nativeKeyBindingsType =
+ aTextControlElement.IsTextArea()
+ ? NativeKeyBindingsType::MultiLineEditor
+ : NativeKeyBindingsType::SingleLineEditor;
+
+ nsIWidget* widget = widgetKeyEvent->mWidget;
+ // If the event is created by chrome script, the widget is nullptr.
+ if (MOZ_UNLIKELY(!widget)) {
+ widget = mFrame->GetNearestWidget();
+ if (MOZ_UNLIKELY(NS_WARN_IF(!widget))) {
+ return false;
+ }
+ }
+
+ // WidgetKeyboardEvent::ExecuteEditCommands() requires non-nullptr mWidget.
+ // If the event is created by chrome script, it is nullptr but we need to
+ // execute native key bindings. Therefore, we need to set widget to
+ // WidgetEvent::mWidget temporarily.
+ AutoRestore<nsCOMPtr<nsIWidget>> saveWidget(widgetKeyEvent->mWidget);
+ widgetKeyEvent->mWidget = widget;
+ if (widgetKeyEvent->ExecuteEditCommands(nativeKeyBindingsType,
+ DoCommandCallback, mFrame)) {
+ aEvent->PreventDefault();
+ return true;
+ }
+ return false;
+ };
+
+ OwningNonNull<TextControlElement> textControlElement(*mTxtCtrlElement);
+ if (StaticPrefs::
+ ui_key_textcontrol_prefer_native_key_bindings_over_builtin_shortcut_key_definitions()) {
+ if (!ExecuteNativeKeyBindings(textControlElement)) {
+ ExecuteOurShortcutKeys(textControlElement);
+ }
+ } else {
+ if (!ExecuteOurShortcutKeys(textControlElement)) {
+ ExecuteNativeKeyBindings(textControlElement);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult TextInputListener::OnEditActionHandled(TextEditor& aTextEditor) {
+ if (mFrame) {
+ // XXX Do we still need this or can we just remove the mFrame and
+ // frame.IsAlive() conditions below?
+ AutoWeakFrame weakFrame = mFrame;
+
+ // Update the undo / redo menus
+ //
+ size_t numUndoItems = aTextEditor.NumberOfUndoItems();
+ size_t numRedoItems = aTextEditor.NumberOfRedoItems();
+ if ((numUndoItems && !mHadUndoItems) || (!numUndoItems && mHadUndoItems) ||
+ (numRedoItems && !mHadRedoItems) || (!numRedoItems && mHadRedoItems)) {
+ // Modify the menu if undo or redo items are different
+ UpdateTextInputCommands(u"undo"_ns);
+
+ mHadUndoItems = numUndoItems != 0;
+ mHadRedoItems = numRedoItems != 0;
+ }
+
+ if (weakFrame.IsAlive()) {
+ HandleValueChanged(aTextEditor);
+ }
+ }
+
+ return mTextControlState ? mTextControlState->OnEditActionHandled() : NS_OK;
+}
+
+void TextInputListener::HandleValueChanged(TextEditor& aTextEditor) {
+ // Make sure we know we were changed (do NOT set this to false if there are
+ // no undo items; JS could change the value and we'd still need to save it)
+ if (mSetValueChanged) {
+ mTxtCtrlElement->SetValueChanged(true);
+ }
+
+ if (!mSettingValue) {
+ // NOTE(emilio): execCommand might get here even though it might not be a
+ // "proper" user-interactive change. Might be worth reconsidering which
+ // ValueChangeKind are we passing down.
+ mTxtCtrlElement->OnValueChanged(ValueChangeKind::UserInteraction,
+ aTextEditor.IsEmpty(), nullptr);
+ if (mTextControlState) {
+ mTextControlState->ClearLastInteractiveValue();
+ }
+ }
+}
+
+nsresult TextInputListener::UpdateTextInputCommands(
+ const nsAString& aCommandsToUpdate) {
+ nsIContent* content = mFrame->GetContent();
+ if (NS_WARN_IF(!content)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<Document> doc = content->GetComposedDoc();
+ if (NS_WARN_IF(!doc)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsPIDOMWindowOuter* domWindow = doc->GetWindow();
+ if (NS_WARN_IF(!domWindow)) {
+ return NS_ERROR_FAILURE;
+ }
+ domWindow->UpdateCommands(aCommandsToUpdate);
+ return NS_OK;
+}
+
+/*****************************************************************************
+ * mozilla::AutoTextControlHandlingState
+ *
+ * This class is temporarily created in the stack and can manage nested
+ * handling state of TextControlState. While this instance exists, lifetime of
+ * TextControlState which created the instance is guaranteed. In other words,
+ * you can use this class as "kungFuDeathGrip" for TextControlState.
+ *****************************************************************************/
+
+enum class TextControlAction {
+ CommitComposition,
+ Destructor,
+ PrepareEditor,
+ SetRangeText,
+ SetSelectionRange,
+ SetValue,
+ UnbindFromFrame,
+ Unlink,
+};
+
+class MOZ_STACK_CLASS AutoTextControlHandlingState {
+ public:
+ AutoTextControlHandlingState() = delete;
+ explicit AutoTextControlHandlingState(const AutoTextControlHandlingState&) =
+ delete;
+ AutoTextControlHandlingState(AutoTextControlHandlingState&&) = delete;
+ void operator=(AutoTextControlHandlingState&) = delete;
+ void operator=(const AutoTextControlHandlingState&) = delete;
+
+ /**
+ * Generic constructor. If TextControlAction does not require additional
+ * data, must use this constructor.
+ */
+ MOZ_CAN_RUN_SCRIPT AutoTextControlHandlingState(
+ TextControlState& aTextControlState, TextControlAction aTextControlAction)
+ : mParent(aTextControlState.mHandlingState),
+ mTextControlState(aTextControlState),
+ mTextCtrlElement(aTextControlState.mTextCtrlElement),
+ mTextInputListener(aTextControlState.mTextListener),
+ mTextControlAction(aTextControlAction) {
+ MOZ_ASSERT(aTextControlAction != TextControlAction::SetValue,
+ "Use specific constructor");
+ MOZ_DIAGNOSTIC_ASSERT_IF(
+ !aTextControlState.mTextListener,
+ !aTextControlState.mBoundFrame || !aTextControlState.mTextEditor);
+ mTextControlState.mHandlingState = this;
+ if (Is(TextControlAction::CommitComposition)) {
+ MOZ_ASSERT(mParent);
+ MOZ_ASSERT(mParent->Is(TextControlAction::SetValue));
+ // If we're trying to commit composition before handling SetValue,
+ // the parent old values will be outdated so that we need to clear
+ // them.
+ mParent->InvalidateOldValue();
+ }
+ }
+
+ /**
+ * TextControlAction::SetValue specific constructor. Current setting value
+ * must be specified and the creator should check whether we succeeded to
+ * allocate memory for line breaker conversion.
+ */
+ MOZ_CAN_RUN_SCRIPT AutoTextControlHandlingState(
+ TextControlState& aTextControlState, TextControlAction aTextControlAction,
+ const nsAString& aSettingValue, const nsAString* aOldValue,
+ const ValueSetterOptions& aOptions, ErrorResult& aRv)
+ : mParent(aTextControlState.mHandlingState),
+ mTextControlState(aTextControlState),
+ mTextCtrlElement(aTextControlState.mTextCtrlElement),
+ mTextInputListener(aTextControlState.mTextListener),
+ mSettingValue(aSettingValue),
+ mOldValue(aOldValue),
+ mValueSetterOptions(aOptions),
+ mTextControlAction(aTextControlAction) {
+ MOZ_ASSERT(aTextControlAction == TextControlAction::SetValue,
+ "Use generic constructor");
+ MOZ_DIAGNOSTIC_ASSERT_IF(
+ !aTextControlState.mTextListener,
+ !aTextControlState.mBoundFrame || !aTextControlState.mTextEditor);
+ mTextControlState.mHandlingState = this;
+ if (!nsContentUtils::PlatformToDOMLineBreaks(mSettingValue, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ // Update all setting value's new value because older value shouldn't
+ // overwrite newer value.
+ if (mParent) {
+ // If SetValue is nested, parents cannot trust their old value anymore.
+ // So, we need to clear them.
+ mParent->UpdateSettingValueAndInvalidateOldValue(mSettingValue);
+ }
+ }
+
+ MOZ_CAN_RUN_SCRIPT ~AutoTextControlHandlingState() {
+ mTextControlState.mHandlingState = mParent;
+ if (!mParent && mTextControlStateDestroyed) {
+ mTextControlState.DeleteOrCacheForReuse();
+ }
+ if (!mTextControlStateDestroyed && mPrepareEditorLater) {
+ MOZ_ASSERT(nsContentUtils::IsSafeToRunScript());
+ MOZ_ASSERT(Is(TextControlAction::SetValue));
+ mTextControlState.PrepareEditor(&mSettingValue);
+ }
+ }
+
+ void OnDestroyTextControlState() {
+ if (IsHandling(TextControlAction::Destructor)) {
+ // Do nothing since mTextContrlState.DeleteOrCacheForReuse() has
+ // already been called.
+ return;
+ }
+ mTextControlStateDestroyed = true;
+ if (mParent) {
+ mParent->OnDestroyTextControlState();
+ }
+ }
+
+ void PrepareEditorLater() {
+ MOZ_ASSERT(IsHandling(TextControlAction::SetValue));
+ MOZ_ASSERT(!IsHandling(TextControlAction::PrepareEditor));
+ // Look for the top most SetValue.
+ AutoTextControlHandlingState* settingValue = nullptr;
+ for (AutoTextControlHandlingState* handlingSomething = this;
+ handlingSomething; handlingSomething = handlingSomething->mParent) {
+ if (handlingSomething->Is(TextControlAction::SetValue)) {
+ settingValue = handlingSomething;
+ }
+ }
+ settingValue->mPrepareEditorLater = true;
+ }
+
+ /**
+ * WillSetValueWithTextEditor() is called when TextControlState sets
+ * value with its mTextEditor.
+ */
+ void WillSetValueWithTextEditor() {
+ MOZ_ASSERT(Is(TextControlAction::SetValue));
+ MOZ_ASSERT(mTextControlState.mBoundFrame);
+ mTextControlFrame = mTextControlState.mBoundFrame;
+ // If we'reemulating user input, we don't need to manage mTextInputListener
+ // by ourselves since everything should be handled by TextEditor as normal
+ // user input.
+ if (mValueSetterOptions.contains(ValueSetterOption::BySetUserInputAPI)) {
+ return;
+ }
+ // Otherwise, if we're setting the value programatically, we need to manage
+ // mTextInputListener by ourselves since TextEditor users special path
+ // for the performance.
+ mTextInputListener->SettingValue(true);
+ mTextInputListener->SetValueChanged(
+ mValueSetterOptions.contains(ValueSetterOption::SetValueChanged));
+ mEditActionHandled = false;
+ // Even if falling back to `TextControlState::SetValueWithoutTextEditor()`
+ // due to editor destruction, it shouldn't dispatch "beforeinput" event
+ // anymore. Therefore, we should mark that we've already dispatched
+ // "beforeinput" event.
+ WillDispatchBeforeInputEvent();
+ }
+
+ /**
+ * WillDispatchBeforeInputEvent() is called immediately before dispatching
+ * "beforeinput" event in `TextControlState`.
+ */
+ void WillDispatchBeforeInputEvent() {
+ mBeforeInputEventHasBeenDispatched = true;
+ }
+
+ /**
+ * OnEditActionHandled() is called when the TextEditor handles something
+ * and immediately before dispatching "input" event.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult OnEditActionHandled() {
+ MOZ_ASSERT(!mEditActionHandled);
+ mEditActionHandled = true;
+ if (!Is(TextControlAction::SetValue)) {
+ return NS_OK;
+ }
+ if (!mValueSetterOptions.contains(ValueSetterOption::BySetUserInputAPI)) {
+ mTextInputListener->SetValueChanged(true);
+ mTextInputListener->SettingValue(
+ mParent && mParent->IsHandling(TextControlAction::SetValue));
+ }
+ if (!IsOriginalTextControlFrameAlive()) {
+ return SetValueWithoutTextEditorAgain() ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+ }
+ // The new value never includes line breaks caused by hard-wrap.
+ // So, mCachedValue can always cache the new value.
+ nsITextControlFrame* textControlFrame =
+ do_QueryFrame(mTextControlFrame.GetFrame());
+ return static_cast<nsTextControlFrame*>(textControlFrame)
+ ->CacheValue(mSettingValue, fallible)
+ ? NS_OK
+ : NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ /**
+ * SetValueWithoutTextEditorAgain() should be called if the frame for
+ * mTextControlState was destroyed during setting value.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool SetValueWithoutTextEditorAgain() {
+ MOZ_ASSERT(!IsOriginalTextControlFrameAlive());
+ // If the frame was destroyed because of a flush somewhere inside
+ // TextEditor, mBoundFrame here will be nullptr. But it's also
+ // possible for the frame to go away because of another reason (such
+ // as deleting the existing selection -- see bug 574558), in which
+ // case we don't need to reset the value here.
+ if (mTextControlState.mBoundFrame) {
+ return true;
+ }
+ // XXX It's odd to drop flags except
+ // ValueSetterOption::SetValueChanged.
+ // Probably, this intended to drop ValueSetterOption::BySetUserInputAPI
+ // and ValueSetterOption::ByContentAPI, but other flags are added later.
+ ErrorResult error;
+ AutoTextControlHandlingState handlingSetValueWithoutEditor(
+ mTextControlState, TextControlAction::SetValue, mSettingValue,
+ mOldValue, mValueSetterOptions & ValueSetterOption::SetValueChanged,
+ error);
+ if (error.Failed()) {
+ MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY));
+ error.SuppressException();
+ return false;
+ }
+ return mTextControlState.SetValueWithoutTextEditor(
+ handlingSetValueWithoutEditor);
+ }
+
+ bool IsTextControlStateDestroyed() const {
+ return mTextControlStateDestroyed;
+ }
+ bool IsOriginalTextControlFrameAlive() const {
+ return const_cast<AutoTextControlHandlingState*>(this)
+ ->mTextControlFrame.IsAlive();
+ }
+ bool HasEditActionHandled() const { return mEditActionHandled; }
+ bool HasBeforeInputEventDispatched() const {
+ return mBeforeInputEventHasBeenDispatched;
+ }
+ bool Is(TextControlAction aTextControlAction) const {
+ return mTextControlAction == aTextControlAction;
+ }
+ bool IsHandling(TextControlAction aTextControlAction) const {
+ if (mTextControlAction == aTextControlAction) {
+ return true;
+ }
+ return mParent && mParent->IsHandling(aTextControlAction);
+ }
+ TextControlElement* GetTextControlElement() const { return mTextCtrlElement; }
+ TextInputListener* GetTextInputListener() const { return mTextInputListener; }
+ const ValueSetterOptions& ValueSetterOptionsRef() const {
+ MOZ_ASSERT(Is(TextControlAction::SetValue));
+ return mValueSetterOptions;
+ }
+ const nsAString* GetOldValue() const {
+ MOZ_ASSERT(Is(TextControlAction::SetValue));
+ return mOldValue;
+ }
+ const nsString& GetSettingValue() const {
+ MOZ_ASSERT(IsHandling(TextControlAction::SetValue));
+ if (mTextControlAction == TextControlAction::SetValue) {
+ return mSettingValue;
+ }
+ return mParent->GetSettingValue();
+ }
+
+ private:
+ void UpdateSettingValueAndInvalidateOldValue(const nsString& aSettingValue) {
+ if (mTextControlAction == TextControlAction::SetValue) {
+ mSettingValue = aSettingValue;
+ }
+ mOldValue = nullptr;
+ if (mParent) {
+ mParent->UpdateSettingValueAndInvalidateOldValue(aSettingValue);
+ }
+ }
+ void InvalidateOldValue() {
+ mOldValue = nullptr;
+ if (mParent) {
+ mParent->InvalidateOldValue();
+ }
+ }
+
+ AutoTextControlHandlingState* const mParent;
+ TextControlState& mTextControlState;
+ // mTextControlFrame should be set immediately before calling methods
+ // which may destroy the frame. Then, you can check whether the frame
+ // was destroyed/replaced.
+ AutoWeakFrame mTextControlFrame;
+ // mTextCtrlElement grabs TextControlState::mTextCtrlElement since
+ // if the text control element releases mTextControlState, only this
+ // can guarantee the instance of the text control element.
+ RefPtr<TextControlElement> const mTextCtrlElement;
+ // mTextInputListener grabs TextControlState::mTextListener because if
+ // TextControlState is unbind from the frame, it's released.
+ RefPtr<TextInputListener> const mTextInputListener;
+ nsAutoString mSettingValue;
+ const nsAString* mOldValue = nullptr;
+ ValueSetterOptions mValueSetterOptions;
+ TextControlAction const mTextControlAction;
+ bool mTextControlStateDestroyed = false;
+ bool mEditActionHandled = false;
+ bool mPrepareEditorLater = false;
+ bool mBeforeInputEventHasBeenDispatched = false;
+};
+
+/*****************************************************************************
+ * mozilla::TextControlState
+ *****************************************************************************/
+
+/**
+ * For avoiding allocation cost of the instance, we should reuse instances
+ * as far as possible.
+ *
+ * FYI: `25` is just a magic number considered without enough investigation,
+ * but at least, this value must not make damage for footprint.
+ * Feel free to change it if you find better number.
+ */
+static constexpr size_t kMaxCountOfCacheToReuse = 25;
+static AutoTArray<void*, kMaxCountOfCacheToReuse>* sReleasedInstances = nullptr;
+static bool sHasShutDown = false;
+
+TextControlState::TextControlState(TextControlElement* aOwningElement)
+ : mTextCtrlElement(aOwningElement),
+ mEverInited(false),
+ mEditorInitialized(false),
+ mValueTransferInProgress(false),
+ mSelectionCached(true)
+// When adding more member variable initializations here, add the same
+// also to ::Construct.
+{
+ MOZ_COUNT_CTOR(TextControlState);
+ static_assert(sizeof(*this) <= 128,
+ "Please keep small TextControlState as far as possible");
+}
+
+TextControlState* TextControlState::Construct(
+ TextControlElement* aOwningElement) {
+ void* mem;
+ if (sReleasedInstances && !sReleasedInstances->IsEmpty()) {
+ mem = sReleasedInstances->PopLastElement();
+ } else {
+ mem = moz_xmalloc(sizeof(TextControlState));
+ }
+
+ return new (mem) TextControlState(aOwningElement);
+}
+
+TextControlState::~TextControlState() {
+ MOZ_ASSERT(!mHandlingState);
+ MOZ_COUNT_DTOR(TextControlState);
+ AutoTextControlHandlingState handlingDesctructor(
+ *this, TextControlAction::Destructor);
+ Clear();
+}
+
+void TextControlState::Shutdown() {
+ sHasShutDown = true;
+ if (sReleasedInstances) {
+ for (void* mem : *sReleasedInstances) {
+ free(mem);
+ }
+ delete sReleasedInstances;
+ }
+}
+
+void TextControlState::Destroy() {
+ // If we're handling something, we should be deleted later.
+ if (mHandlingState) {
+ mHandlingState->OnDestroyTextControlState();
+ return;
+ }
+ DeleteOrCacheForReuse();
+ // Note that this instance may have already been deleted here. Don't touch
+ // any members.
+}
+
+void TextControlState::DeleteOrCacheForReuse() {
+ MOZ_ASSERT(!IsBusy());
+
+ void* mem = this;
+ this->~TextControlState();
+
+ // If we can cache this instance, we should do it instead of deleting it.
+ if (!sHasShutDown && (!sReleasedInstances || sReleasedInstances->Length() <
+ kMaxCountOfCacheToReuse)) {
+ // Put this instance to the cache. Note that now, the array may be full,
+ // but it's not problem to cache more instances than kMaxCountOfCacheToReuse
+ // because it just requires reallocation cost of the array buffer.
+ if (!sReleasedInstances) {
+ sReleasedInstances = new AutoTArray<void*, kMaxCountOfCacheToReuse>;
+ }
+ sReleasedInstances->AppendElement(mem);
+ } else {
+ free(mem);
+ }
+}
+
+nsresult TextControlState::OnEditActionHandled() {
+ return mHandlingState ? mHandlingState->OnEditActionHandled() : NS_OK;
+}
+
+Element* TextControlState::GetRootNode() {
+ return mBoundFrame ? mBoundFrame->GetRootNode() : nullptr;
+}
+
+Element* TextControlState::GetPreviewNode() {
+ return mBoundFrame ? mBoundFrame->GetPreviewNode() : nullptr;
+}
+
+void TextControlState::Clear() {
+ MOZ_ASSERT(mHandlingState);
+ MOZ_ASSERT(mHandlingState->Is(TextControlAction::Destructor) ||
+ mHandlingState->Is(TextControlAction::Unlink));
+ if (mTextEditor) {
+ mTextEditor->SetTextInputListener(nullptr);
+ }
+
+ if (mBoundFrame) {
+ // Oops, we still have a frame!
+ // This should happen when the type of a text input control is being changed
+ // to something which is not a text control. In this case, we should
+ // pretend that a frame is being destroyed, and clean up after ourselves
+ // properly.
+ UnbindFromFrame(mBoundFrame);
+ mTextEditor = nullptr;
+ } else {
+ // If we have a bound frame around, UnbindFromFrame will call DestroyEditor
+ // for us.
+ DestroyEditor();
+ MOZ_DIAGNOSTIC_ASSERT(!mBoundFrame || !mTextEditor);
+ }
+ mTextListener = nullptr;
+}
+
+void TextControlState::Unlink() {
+ AutoTextControlHandlingState handlingUnlink(*this, TextControlAction::Unlink);
+ UnlinkInternal();
+}
+
+void TextControlState::UnlinkInternal() {
+ MOZ_ASSERT(mHandlingState);
+ MOZ_ASSERT(mHandlingState->Is(TextControlAction::Unlink));
+ TextControlState* tmp = this;
+ tmp->Clear();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelCon)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextEditor)
+}
+
+void TextControlState::Traverse(nsCycleCollectionTraversalCallback& cb) {
+ TextControlState* tmp = this;
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelCon)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextEditor)
+}
+
+nsFrameSelection* TextControlState::GetConstFrameSelection() {
+ return mSelCon ? mSelCon->GetConstFrameSelection() : nullptr;
+}
+
+TextEditor* TextControlState::GetTextEditor() {
+ // Note that if the instance is destroyed in PrepareEditor(), it returns
+ // NS_ERROR_NOT_INITIALIZED so that we don't need to create kungFuDeathGrip
+ // in this hot path.
+ if (!mTextEditor && NS_WARN_IF(NS_FAILED(PrepareEditor()))) {
+ return nullptr;
+ }
+ return mTextEditor;
+}
+
+TextEditor* TextControlState::GetTextEditorWithoutCreation() const {
+ return mTextEditor;
+}
+
+nsISelectionController* TextControlState::GetSelectionController() const {
+ return mSelCon;
+}
+
+// Helper class, used below in BindToFrame().
+class PrepareEditorEvent : public Runnable {
+ public:
+ PrepareEditorEvent(TextControlState& aState, nsIContent* aOwnerContent,
+ const nsAString& aCurrentValue)
+ : Runnable("PrepareEditorEvent"),
+ mState(&aState),
+ mOwnerContent(aOwnerContent),
+ mCurrentValue(aCurrentValue) {
+ aState.mValueTransferInProgress = true;
+ }
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ if (NS_WARN_IF(!mState)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // Transfer the saved value to the editor if we have one
+ const nsAString* value = nullptr;
+ if (!mCurrentValue.IsEmpty()) {
+ value = &mCurrentValue;
+ }
+
+ nsAutoScriptBlocker scriptBlocker;
+
+ mState->PrepareEditor(value);
+
+ mState->mValueTransferInProgress = false;
+
+ return NS_OK;
+ }
+
+ private:
+ WeakPtr<TextControlState> mState;
+ nsCOMPtr<nsIContent> mOwnerContent; // strong reference
+ nsAutoString mCurrentValue;
+};
+
+nsresult TextControlState::BindToFrame(nsTextControlFrame* aFrame) {
+ MOZ_ASSERT(
+ !nsContentUtils::IsSafeToRunScript(),
+ "TextControlState::BindToFrame() has to be called with script blocker");
+ NS_ASSERTION(aFrame, "The frame to bind to should be valid");
+ if (!aFrame) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ NS_ASSERTION(!mBoundFrame, "Cannot bind twice, need to unbind first");
+ if (mBoundFrame) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // If we'll need to transfer our current value to the editor, save it before
+ // binding to the frame.
+ nsAutoString currentValue;
+ if (mTextEditor) {
+ GetValue(currentValue, true, /* aForDisplay = */ false);
+ }
+
+ mBoundFrame = aFrame;
+
+ Element* rootNode = aFrame->GetRootNode();
+ MOZ_ASSERT(rootNode);
+
+ PresShell* presShell = aFrame->PresContext()->GetPresShell();
+ MOZ_ASSERT(presShell);
+
+ // Create a SelectionController
+ mSelCon = new TextInputSelectionController(presShell, rootNode);
+ MOZ_ASSERT(!mTextListener, "Should not overwrite the object");
+ mTextListener = new TextInputListener(mTextCtrlElement);
+
+ mTextListener->SetFrame(mBoundFrame);
+
+ // Editor will override this as needed from InitializeSelection.
+ mSelCon->SetDisplaySelection(nsISelectionController::SELECTION_HIDDEN);
+
+ // Get the caret and make it a selection listener.
+ // FYI: It's safe to use raw pointer for calling
+ // Selection::AddSelectionListner() because it only appends the listener
+ // to its internal array.
+ Selection* selection = mSelCon->GetSelection(SelectionType::eNormal);
+ if (selection) {
+ RefPtr<nsCaret> caret = presShell->GetCaret();
+ if (caret) {
+ selection->AddSelectionListener(caret);
+ }
+ mTextListener->StartToListenToSelectionChange();
+ }
+
+ // If an editor exists from before, prepare it for usage
+ if (mTextEditor) {
+ if (NS_WARN_IF(!mTextCtrlElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Set the correct direction on the newly created root node
+ if (mTextEditor->IsRightToLeft()) {
+ rootNode->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, u"rtl"_ns, false);
+ } else if (mTextEditor->IsLeftToRight()) {
+ rootNode->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, u"ltr"_ns, false);
+ } else {
+ // otherwise, inherit the content node's direction
+ }
+
+ nsContentUtils::AddScriptRunner(
+ new PrepareEditorEvent(*this, mTextCtrlElement, currentValue));
+ }
+
+ return NS_OK;
+}
+
+struct MOZ_STACK_CLASS PreDestroyer {
+ void Init(TextEditor* aTextEditor) { mTextEditor = aTextEditor; }
+ ~PreDestroyer() {
+ if (mTextEditor) {
+ // In this case, we don't need to restore the unmasked range of password
+ // editor.
+ UniquePtr<PasswordMaskData> passwordMaskData = mTextEditor->PreDestroy();
+ }
+ }
+ void Swap(RefPtr<TextEditor>& aTextEditor) {
+ return mTextEditor.swap(aTextEditor);
+ }
+
+ private:
+ RefPtr<TextEditor> mTextEditor;
+};
+
+nsresult TextControlState::PrepareEditor(const nsAString* aValue) {
+ if (!mBoundFrame) {
+ // Cannot create an editor without a bound frame.
+ // Don't return a failure code, because js callers can't handle that.
+ return NS_OK;
+ }
+
+ if (mEditorInitialized) {
+ // Do not initialize the editor multiple times.
+ return NS_OK;
+ }
+
+ AutoHideSelectionChanges hideSelectionChanges(GetConstFrameSelection());
+
+ if (mHandlingState) {
+ // Don't attempt to initialize recursively!
+ if (mHandlingState->IsHandling(TextControlAction::PrepareEditor)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ // Reschedule creating editor later if we're setting value.
+ if (mHandlingState->IsHandling(TextControlAction::SetValue)) {
+ mHandlingState->PrepareEditorLater();
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ }
+
+ MOZ_ASSERT(mTextCtrlElement);
+
+ AutoTextControlHandlingState preparingEditor(
+ *this, TextControlAction::PrepareEditor);
+
+ // Note that we don't check mTextEditor here, because we might already have
+ // one around, in which case we don't create a new one, and we'll just tie
+ // the required machinery to it.
+
+ nsPresContext* presContext = mBoundFrame->PresContext();
+ PresShell* presShell = presContext->GetPresShell();
+
+ // Setup the editor flags
+
+ // Spell check is diabled at creation time. It is enabled once
+ // the editor comes into focus.
+ uint32_t editorFlags = nsIEditor::eEditorSkipSpellCheck;
+
+ if (IsSingleLineTextControl()) {
+ editorFlags |= nsIEditor::eEditorSingleLineMask;
+ }
+ if (IsPasswordTextControl()) {
+ editorFlags |= nsIEditor::eEditorPasswordMask;
+ }
+
+ bool shouldInitializeEditor = false;
+ RefPtr<TextEditor> newTextEditor; // the editor that we might create
+ PreDestroyer preDestroyer;
+ if (!mTextEditor) {
+ shouldInitializeEditor = true;
+
+ // Create an editor
+ newTextEditor = new TextEditor();
+ preDestroyer.Init(newTextEditor);
+
+ // Make sure we clear out the non-breaking space before we initialize the
+ // editor
+ nsresult rv = mBoundFrame->UpdateValueDisplay(true, true);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsTextControlFrame::UpdateValueDisplay() failed");
+ return rv;
+ }
+ } else {
+ if (aValue || !mEditorInitialized) {
+ // Set the correct value in the root node
+ nsresult rv =
+ mBoundFrame->UpdateValueDisplay(true, !mEditorInitialized, aValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsTextControlFrame::UpdateValueDisplay() failed");
+ return rv;
+ }
+ }
+
+ newTextEditor = mTextEditor; // just pretend that we have a new editor!
+
+ // Don't lose application flags in the process.
+ if (newTextEditor->IsMailEditor()) {
+ editorFlags |= nsIEditor::eEditorMailMask;
+ }
+ }
+
+ // Get the current value of the textfield from the content.
+ // Note that if we've created a new editor, mTextEditor is null at this stage,
+ // so we will get the real value from the content.
+ nsAutoString defaultValue;
+ if (aValue) {
+ defaultValue = *aValue;
+ } else {
+ GetValue(defaultValue, true, /* aForDisplay = */ true);
+ }
+
+ if (!mEditorInitialized) {
+ // Now initialize the editor.
+ //
+ // NOTE: Conversion of '\n' to <BR> happens inside the
+ // editor's Init() call.
+
+ // Get the DOM document
+ nsCOMPtr<Document> doc = presShell->GetDocument();
+ if (NS_WARN_IF(!doc)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // What follows is a bit of a hack. The editor uses the public DOM APIs
+ // for its content manipulations, and it causes it to fail some security
+ // checks deep inside when initializing. So we explictly make it clear that
+ // we're native code.
+ // Note that any script that's directly trying to access our value
+ // has to be going through some scriptable object to do that and that
+ // already does the relevant security checks.
+ AutoNoJSAPI nojsapi;
+
+ RefPtr<Element> anonymousDivElement = GetRootNode();
+ if (NS_WARN_IF(!anonymousDivElement) || NS_WARN_IF(!mSelCon)) {
+ return NS_ERROR_FAILURE;
+ }
+ OwningNonNull<TextInputSelectionController> selectionController(*mSelCon);
+ UniquePtr<PasswordMaskData> passwordMaskData;
+ if (editorFlags & nsIEditor::eEditorPasswordMask) {
+ if (mPasswordMaskData) {
+ passwordMaskData = std::move(mPasswordMaskData);
+ } else {
+ passwordMaskData = MakeUnique<PasswordMaskData>();
+ }
+ } else {
+ mPasswordMaskData = nullptr;
+ }
+ nsresult rv =
+ newTextEditor->Init(*doc, *anonymousDivElement, selectionController,
+ editorFlags, std::move(passwordMaskData));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::Init() failed");
+ return rv;
+ }
+ }
+
+ // Initialize the controller for the editor
+
+ nsresult rv = NS_OK;
+ if (!SuppressEventHandlers(presContext)) {
+ nsCOMPtr<nsIControllers> controllers;
+ if (auto* inputElement = HTMLInputElement::FromNode(mTextCtrlElement)) {
+ nsresult rv = inputElement->GetControllers(getter_AddRefs(controllers));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ } else {
+ auto* textAreaElement = HTMLTextAreaElement::FromNode(mTextCtrlElement);
+ if (!textAreaElement) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv =
+ textAreaElement->GetControllers(getter_AddRefs(controllers));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ if (controllers) {
+ // XXX Oddly, nsresult value is overwritten in the following loop, and
+ // only the last result or `found` decides the value.
+ uint32_t numControllers;
+ bool found = false;
+ rv = controllers->GetControllerCount(&numControllers);
+ for (uint32_t i = 0; i < numControllers; i++) {
+ nsCOMPtr<nsIController> controller;
+ rv = controllers->GetControllerAt(i, getter_AddRefs(controller));
+ if (NS_SUCCEEDED(rv) && controller) {
+ nsCOMPtr<nsIControllerContext> editController =
+ do_QueryInterface(controller);
+ if (editController) {
+ editController->SetCommandContext(
+ static_cast<nsIEditor*>(newTextEditor));
+ found = true;
+ }
+ }
+ }
+ if (!found) {
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ // Initialize the plaintext editor
+ if (shouldInitializeEditor) {
+ const int32_t wrapCols = GetWrapCols();
+ MOZ_ASSERT(wrapCols >= 0);
+ newTextEditor->SetWrapColumn(wrapCols);
+ }
+
+ // Set max text field length
+ newTextEditor->SetMaxTextLength(mTextCtrlElement->UsedMaxLength());
+
+ editorFlags = newTextEditor->Flags();
+
+ // Check if the readonly/disabled attributes are set.
+ if (mTextCtrlElement->IsDisabledOrReadOnly()) {
+ editorFlags |= nsIEditor::eEditorReadonlyMask;
+ }
+
+ SetEditorFlagsIfNecessary(*newTextEditor, editorFlags);
+
+ if (shouldInitializeEditor) {
+ // Hold on to the newly created editor
+ preDestroyer.Swap(mTextEditor);
+ }
+
+ // If we have a default value, insert it under the div we created
+ // above, but be sure to use the editor so that '*' characters get
+ // displayed for password fields, etc. SetValue() will call the
+ // editor for us.
+
+ if (!defaultValue.IsEmpty()) {
+ // XXX rv may store error code which indicates there is no controller.
+ // However, we overwrite it only in this case.
+ rv = SetEditorFlagsIfNecessary(*newTextEditor, editorFlags);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Now call SetValue() which will make the necessary editor calls to set
+ // the default value. Make sure to turn off undo before setting the default
+ // value, and turn it back on afterwards. This will make sure we can't undo
+ // past the default value.
+ // So, we use ValueSetterOption::ByInternalAPI only that it will turn off
+ // undo.
+
+ if (NS_WARN_IF(!SetValue(defaultValue, ValueSetterOption::ByInternalAPI))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Now restore the original editor flags.
+ rv = SetEditorFlagsIfNecessary(*newTextEditor, editorFlags);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+ // When the default value is empty, we don't call SetValue(). That means that
+ // we have not notified IMEContentObserver of the empty value when the
+ // <textarea> is not dirty (i.e., the default value is mirrored into the
+ // anonymous subtree asynchronously) and the value was changed during a
+ // reframe (i.e., while IMEContentObserver was not observing the mutation of
+ // the anonymous subtree). Therefore, we notify IMEContentObserver here in
+ // that case.
+ else if (mTextCtrlElement && mTextCtrlElement->IsTextArea() &&
+ !mTextCtrlElement->ValueChanged()) {
+ MOZ_ASSERT(defaultValue.IsEmpty());
+ IMEContentObserver* observer = GetIMEContentObserver();
+ if (observer && observer->WasInitializedWith(*newTextEditor)) {
+ observer->OnTextControlValueChangedWhileNotObservable(defaultValue);
+ }
+ }
+
+ DebugOnly<bool> enabledUndoRedo =
+ newTextEditor->EnableUndoRedo(TextControlElement::DEFAULT_UNDO_CAP);
+ NS_WARNING_ASSERTION(enabledUndoRedo,
+ "Failed to enable undo/redo transaction");
+
+ if (!mEditorInitialized) {
+ newTextEditor->PostCreate();
+ mEverInited = true;
+ mEditorInitialized = true;
+ }
+
+ if (mTextListener) {
+ newTextEditor->SetTextInputListener(mTextListener);
+ }
+
+ // Restore our selection after being bound to a new frame
+ if (mSelectionCached) {
+ if (mRestoringSelection) { // paranoia
+ mRestoringSelection->Revoke();
+ }
+ mRestoringSelection = new RestoreSelectionState(this, mBoundFrame);
+ if (mRestoringSelection) {
+ nsContentUtils::AddScriptRunner(mRestoringSelection);
+ }
+ }
+
+ // The selection cache is no longer going to be valid.
+ //
+ // XXXbz Shouldn't we do this at the point when we're actually about to
+ // restore the properties or something? As things stand, if UnbindFromFrame
+ // happens before our RestoreSelectionState runs, it looks like we'll lose our
+ // selection info, because we will think we don't have it cached and try to
+ // read it from the selection controller, which will not have it yet.
+ mSelectionCached = false;
+
+ return preparingEditor.IsTextControlStateDestroyed()
+ ? NS_ERROR_NOT_INITIALIZED
+ : rv;
+}
+
+void TextControlState::FinishedRestoringSelection() {
+ mRestoringSelection = nullptr;
+}
+
+void TextControlState::SyncUpSelectionPropertiesBeforeDestruction() {
+ if (mBoundFrame) {
+ UnbindFromFrame(mBoundFrame);
+ }
+}
+
+void TextControlState::SetSelectionProperties(
+ TextControlState::SelectionProperties& aProps) {
+ if (mBoundFrame) {
+ mBoundFrame->SetSelectionRange(aProps.GetStart(), aProps.GetEnd(),
+ aProps.GetDirection());
+ // The instance may have already been deleted here.
+ } else {
+ mSelectionProperties = aProps;
+ }
+}
+
+void TextControlState::GetSelectionRange(uint32_t* aSelectionStart,
+ uint32_t* aSelectionEnd,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(aSelectionStart);
+ MOZ_ASSERT(aSelectionEnd);
+ MOZ_ASSERT(IsSelectionCached() || GetSelectionController(),
+ "How can we not have a cached selection if we have no selection "
+ "controller?");
+
+ // Note that we may have both IsSelectionCached() _and_
+ // GetSelectionController() if we haven't initialized our editor yet.
+ if (IsSelectionCached()) {
+ const SelectionProperties& props = GetSelectionProperties();
+ *aSelectionStart = props.GetStart();
+ *aSelectionEnd = props.GetEnd();
+ return;
+ }
+
+ Selection* sel = mSelCon->GetSelection(SelectionType::eNormal);
+ if (NS_WARN_IF(!sel)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ Element* root = GetRootNode();
+ if (NS_WARN_IF(!root)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+ nsContentUtils::GetSelectionInTextControl(sel, root, *aSelectionStart,
+ *aSelectionEnd);
+}
+
+SelectionDirection TextControlState::GetSelectionDirection(ErrorResult& aRv) {
+ MOZ_ASSERT(IsSelectionCached() || GetSelectionController(),
+ "How can we not have a cached selection if we have no selection "
+ "controller?");
+
+ // Note that we may have both IsSelectionCached() _and_
+ // GetSelectionController() if we haven't initialized our editor yet.
+ if (IsSelectionCached()) {
+ return GetSelectionProperties().GetDirection();
+ }
+
+ Selection* sel = mSelCon->GetSelection(SelectionType::eNormal);
+ if (NS_WARN_IF(!sel)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return SelectionDirection::Forward;
+ }
+
+ nsDirection direction = sel->GetDirection();
+ if (direction == eDirNext) {
+ return SelectionDirection::Forward;
+ }
+
+ MOZ_ASSERT(direction == eDirPrevious);
+ return SelectionDirection::Backward;
+}
+
+void TextControlState::SetSelectionRange(uint32_t aStart, uint32_t aEnd,
+ SelectionDirection aDirection,
+ ErrorResult& aRv,
+ ScrollAfterSelection aScroll) {
+ MOZ_ASSERT(IsSelectionCached() || mBoundFrame,
+ "How can we have a non-cached selection but no frame?");
+
+ AutoTextControlHandlingState handlingSetSelectionRange(
+ *this, TextControlAction::SetSelectionRange);
+
+ if (aStart > aEnd) {
+ aStart = aEnd;
+ }
+
+ if (!IsSelectionCached()) {
+ MOZ_ASSERT(mBoundFrame, "Our frame should still be valid");
+ aRv = mBoundFrame->SetSelectionRange(aStart, aEnd, aDirection);
+ if (aRv.Failed() ||
+ handlingSetSelectionRange.IsTextControlStateDestroyed()) {
+ return;
+ }
+ if (aScroll == ScrollAfterSelection::Yes && mBoundFrame) {
+ // mBoundFrame could be gone if selection listeners flushed layout for
+ // example.
+ mBoundFrame->ScrollSelectionIntoViewAsync();
+ }
+ return;
+ }
+
+ SelectionProperties& props = GetSelectionProperties();
+ if (!props.HasMaxLength()) {
+ // A clone without a dirty value flag may not have a max length yet
+ nsAutoString value;
+ GetValue(value, false, /* aForDisplay = */ true);
+ props.SetMaxLength(value.Length());
+ }
+
+ bool changed = props.SetStart(aStart);
+ changed |= props.SetEnd(aEnd);
+ changed |= props.SetDirection(aDirection);
+
+ if (!changed) {
+ return;
+ }
+
+ // It sure would be nice if we had an existing Element* or so to work with.
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(mTextCtrlElement, eFormSelect, CanBubble::eYes);
+ asyncDispatcher->PostDOMEvent();
+
+ // SelectionChangeEventDispatcher covers this when !IsSelectionCached().
+ // XXX(krosylight): Shouldn't it fire before select event?
+ // Currently Gecko and Blink both fire selectionchange after select.
+ if (IsSelectionCached() &&
+ StaticPrefs::dom_select_events_textcontrols_selectionchange_enabled()) {
+ asyncDispatcher = new AsyncEventDispatcher(
+ mTextCtrlElement, eSelectionChange, CanBubble::eYes);
+ asyncDispatcher->PostDOMEvent();
+ }
+}
+
+void TextControlState::SetSelectionStart(const Nullable<uint32_t>& aStart,
+ ErrorResult& aRv) {
+ uint32_t start = 0;
+ if (!aStart.IsNull()) {
+ start = aStart.Value();
+ }
+
+ uint32_t ignored, end;
+ GetSelectionRange(&ignored, &end, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ SelectionDirection dir = GetSelectionDirection(aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (end < start) {
+ end = start;
+ }
+
+ SetSelectionRange(start, end, dir, aRv);
+ // The instance may have already been deleted here.
+}
+
+void TextControlState::SetSelectionEnd(const Nullable<uint32_t>& aEnd,
+ ErrorResult& aRv) {
+ uint32_t end = 0;
+ if (!aEnd.IsNull()) {
+ end = aEnd.Value();
+ }
+
+ uint32_t start, ignored;
+ GetSelectionRange(&start, &ignored, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ SelectionDirection dir = GetSelectionDirection(aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ SetSelectionRange(start, end, dir, aRv);
+ // The instance may have already been deleted here.
+}
+
+static void DirectionToName(SelectionDirection dir, nsAString& aDirection) {
+ switch (dir) {
+ case SelectionDirection::None:
+ // TODO(mbrodesser): this should be supported, see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1541454.
+ NS_WARNING("We don't actually support this... how did we get it?");
+ return aDirection.AssignLiteral("none");
+ case SelectionDirection::Forward:
+ return aDirection.AssignLiteral("forward");
+ case SelectionDirection::Backward:
+ return aDirection.AssignLiteral("backward");
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid SelectionDirection value");
+}
+
+void TextControlState::GetSelectionDirectionString(nsAString& aDirection,
+ ErrorResult& aRv) {
+ SelectionDirection dir = GetSelectionDirection(aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ DirectionToName(dir, aDirection);
+}
+
+static SelectionDirection DirectionStringToSelectionDirection(
+ const nsAString& aDirection) {
+ if (aDirection.EqualsLiteral("backward")) {
+ return SelectionDirection::Backward;
+ }
+ // We don't support directionless selections, see bug 1541454.
+ return SelectionDirection::Forward;
+}
+
+void TextControlState::SetSelectionDirection(const nsAString& aDirection,
+ ErrorResult& aRv) {
+ SelectionDirection dir = DirectionStringToSelectionDirection(aDirection);
+
+ uint32_t start, end;
+ GetSelectionRange(&start, &end, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ SetSelectionRange(start, end, dir, aRv);
+ // The instance may have already been deleted here.
+}
+
+static SelectionDirection DirectionStringToSelectionDirection(
+ const Optional<nsAString>& aDirection) {
+ if (!aDirection.WasPassed()) {
+ // We don't support directionless selections.
+ return SelectionDirection::Forward;
+ }
+
+ return DirectionStringToSelectionDirection(aDirection.Value());
+}
+
+void TextControlState::SetSelectionRange(uint32_t aSelectionStart,
+ uint32_t aSelectionEnd,
+ const Optional<nsAString>& aDirection,
+ ErrorResult& aRv,
+ ScrollAfterSelection aScroll) {
+ SelectionDirection dir = DirectionStringToSelectionDirection(aDirection);
+
+ SetSelectionRange(aSelectionStart, aSelectionEnd, dir, aRv, aScroll);
+ // The instance may have already been deleted here.
+}
+
+void TextControlState::SetRangeText(const nsAString& aReplacement,
+ ErrorResult& aRv) {
+ uint32_t start, end;
+ GetSelectionRange(&start, &end, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ SetRangeText(aReplacement, start, end, SelectionMode::Preserve, aRv,
+ Some(start), Some(end));
+ // The instance may have already been deleted here.
+}
+
+void TextControlState::SetRangeText(const nsAString& aReplacement,
+ uint32_t aStart, uint32_t aEnd,
+ SelectionMode aSelectMode, ErrorResult& aRv,
+ const Maybe<uint32_t>& aSelectionStart,
+ const Maybe<uint32_t>& aSelectionEnd) {
+ if (aStart > aEnd) {
+ aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ AutoTextControlHandlingState handlingSetRangeText(
+ *this, TextControlAction::SetRangeText);
+
+ nsAutoString value;
+ mTextCtrlElement->GetValueFromSetRangeText(value);
+ uint32_t inputValueLength = value.Length();
+
+ if (aStart > inputValueLength) {
+ aStart = inputValueLength;
+ }
+
+ if (aEnd > inputValueLength) {
+ aEnd = inputValueLength;
+ }
+
+ uint32_t selectionStart, selectionEnd;
+ if (!aSelectionStart) {
+ MOZ_ASSERT(!aSelectionEnd);
+ GetSelectionRange(&selectionStart, &selectionEnd, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ } else {
+ MOZ_ASSERT(aSelectionEnd);
+ selectionStart = *aSelectionStart;
+ selectionEnd = *aSelectionEnd;
+ }
+
+ // Batch selectionchanges from SetValueFromSetRangeText and SetSelectionRange
+ Selection* selection =
+ mSelCon ? mSelCon->GetSelection(SelectionType::eNormal) : nullptr;
+ SelectionBatcher selectionBatcher(
+ selection, __FUNCTION__,
+ nsISelectionListener::JS_REASON); // no-op if nullptr
+
+ MOZ_ASSERT(aStart <= aEnd);
+ value.Replace(aStart, aEnd - aStart, aReplacement);
+ nsresult rv =
+ MOZ_KnownLive(mTextCtrlElement)->SetValueFromSetRangeText(value);
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return;
+ }
+
+ uint32_t newEnd = aStart + aReplacement.Length();
+ int32_t delta = aReplacement.Length() - (aEnd - aStart);
+
+ switch (aSelectMode) {
+ case SelectionMode::Select:
+ selectionStart = aStart;
+ selectionEnd = newEnd;
+ break;
+ case SelectionMode::Start:
+ selectionStart = selectionEnd = aStart;
+ break;
+ case SelectionMode::End:
+ selectionStart = selectionEnd = newEnd;
+ break;
+ case SelectionMode::Preserve:
+ if (selectionStart > aEnd) {
+ selectionStart += delta;
+ } else if (selectionStart > aStart) {
+ selectionStart = aStart;
+ }
+
+ if (selectionEnd > aEnd) {
+ selectionEnd += delta;
+ } else if (selectionEnd > aStart) {
+ selectionEnd = newEnd;
+ }
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown mode!");
+ }
+
+ SetSelectionRange(selectionStart, selectionEnd, Optional<nsAString>(), aRv);
+ if (IsSelectionCached()) {
+ // SetValueFromSetRangeText skipped SetMaxLength, set it here properly
+ GetSelectionProperties().SetMaxLength(value.Length());
+ }
+}
+
+void TextControlState::DestroyEditor() {
+ // notify the editor that we are going away
+ if (mEditorInitialized) {
+ // FYI: TextEditor checks whether it's destroyed or not immediately after
+ // changes the DOM tree or selection so that it's safe to call
+ // PreDestroy() here even while we're handling actions with
+ // mTextEditor.
+ MOZ_ASSERT(!mPasswordMaskData);
+ RefPtr<TextEditor> textEditor = mTextEditor;
+ mPasswordMaskData = textEditor->PreDestroy();
+ MOZ_ASSERT_IF(mPasswordMaskData, !mPasswordMaskData->mTimer);
+ mEditorInitialized = false;
+ }
+}
+
+void TextControlState::UnbindFromFrame(nsTextControlFrame* aFrame) {
+ if (NS_WARN_IF(!mBoundFrame)) {
+ return;
+ }
+
+ // If it was, however, it should be unbounded from the same frame.
+ MOZ_ASSERT(aFrame == mBoundFrame, "Unbinding from the wrong frame");
+ if (aFrame && aFrame != mBoundFrame) {
+ return;
+ }
+
+ AutoTextControlHandlingState handlingUnbindFromFrame(
+ *this, TextControlAction::UnbindFromFrame);
+
+ if (mSelCon) {
+ mSelCon->SelectionWillLoseFocus();
+ }
+
+ // We need to start storing the value outside of the editor if we're not
+ // going to use it anymore, so retrieve it for now.
+ nsAutoString value;
+ GetValue(value, true, /* aForDisplay = */ false);
+
+ if (mRestoringSelection) {
+ mRestoringSelection->Revoke();
+ mRestoringSelection = nullptr;
+ }
+
+ // Save our selection state if needed.
+ // Note that GetSelectionRange will attempt to work with our selection
+ // controller, so we should make sure we do it before we start doing things
+ // like destroying our editor (if we have one), tearing down the selection
+ // controller, and so forth.
+ if (!IsSelectionCached()) {
+ // Go ahead and cache it now.
+ uint32_t start = 0, end = 0;
+ GetSelectionRange(&start, &end, IgnoreErrors());
+
+ nsITextControlFrame::SelectionDirection direction =
+ GetSelectionDirection(IgnoreErrors());
+
+ SelectionProperties& props = GetSelectionProperties();
+ props.SetMaxLength(value.Length());
+ props.SetStart(start);
+ props.SetEnd(end);
+ props.SetDirection(direction);
+ mSelectionCached = true;
+ }
+
+ // Destroy our editor
+ DestroyEditor();
+
+ // Clean up the controller
+ if (!SuppressEventHandlers(mBoundFrame->PresContext())) {
+ nsCOMPtr<nsIControllers> controllers;
+ if (auto* inputElement = HTMLInputElement::FromNode(mTextCtrlElement)) {
+ inputElement->GetControllers(getter_AddRefs(controllers));
+ } else {
+ auto* textAreaElement = HTMLTextAreaElement::FromNode(mTextCtrlElement);
+ if (textAreaElement) {
+ textAreaElement->GetControllers(getter_AddRefs(controllers));
+ }
+ }
+
+ if (controllers) {
+ uint32_t numControllers;
+ nsresult rv = controllers->GetControllerCount(&numControllers);
+ NS_ASSERTION((NS_SUCCEEDED(rv)),
+ "bad result in gfx text control destructor");
+ for (uint32_t i = 0; i < numControllers; i++) {
+ nsCOMPtr<nsIController> controller;
+ rv = controllers->GetControllerAt(i, getter_AddRefs(controller));
+ if (NS_SUCCEEDED(rv) && controller) {
+ nsCOMPtr<nsIControllerContext> editController =
+ do_QueryInterface(controller);
+ if (editController) {
+ editController->SetCommandContext(nullptr);
+ }
+ }
+ }
+ }
+ }
+
+ if (mSelCon) {
+ if (mTextListener) {
+ mTextListener->EndListeningToSelectionChange();
+ }
+
+ mSelCon->SetScrollableFrame(nullptr);
+ mSelCon = nullptr;
+ }
+
+ if (mTextListener) {
+ mTextListener->SetFrame(nullptr);
+
+ EventListenerManager* manager =
+ mTextCtrlElement->GetExistingListenerManager();
+ if (manager) {
+ manager->RemoveEventListenerByType(mTextListener, u"keydown"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ manager->RemoveEventListenerByType(mTextListener, u"keypress"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ manager->RemoveEventListenerByType(mTextListener, u"keyup"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ }
+
+ mTextListener = nullptr;
+ }
+
+ mBoundFrame = nullptr;
+
+ // Now that we don't have a frame any more, store the value in the text
+ // buffer. The only case where we don't do this is if a value transfer is in
+ // progress.
+ if (!mValueTransferInProgress) {
+ DebugOnly<bool> ok = SetValue(value, ValueSetterOption::ByInternalAPI);
+ // TODO Find something better to do if this fails...
+ NS_WARNING_ASSERTION(ok, "SetValue() couldn't allocate memory");
+ }
+}
+
+void TextControlState::GetValue(nsAString& aValue, bool aIgnoreWrap,
+ bool aForDisplay) const {
+ // While SetValue() is being called and requesting to commit composition to
+ // IME, GetValue() may be called for appending text or something. Then, we
+ // need to return the latest aValue of SetValue() since the value hasn't
+ // been set to the editor yet.
+ // XXX After implementing "beforeinput" event, this becomes wrong. The
+ // value should be modified immediately after "beforeinput" event for
+ // "insertReplacementText".
+ if (mHandlingState &&
+ mHandlingState->IsHandling(TextControlAction::CommitComposition)) {
+ aValue = mHandlingState->GetSettingValue();
+ MOZ_ASSERT(aValue.FindChar(u'\r') == -1);
+ return;
+ }
+
+ if (mTextEditor && mBoundFrame &&
+ (mEditorInitialized || !IsSingleLineTextControl())) {
+ if (aIgnoreWrap && !mBoundFrame->CachedValue().IsVoid()) {
+ aValue = mBoundFrame->CachedValue();
+ MOZ_ASSERT(aValue.FindChar(u'\r') == -1);
+ return;
+ }
+
+ aValue.Truncate(); // initialize out param
+
+ uint32_t flags = (nsIDocumentEncoder::OutputLFLineBreak |
+ nsIDocumentEncoder::OutputPreformatted |
+ nsIDocumentEncoder::OutputPersistNBSP |
+ nsIDocumentEncoder::OutputBodyOnly);
+ if (!aIgnoreWrap) {
+ TextControlElement::nsHTMLTextWrap wrapProp;
+ if (mTextCtrlElement &&
+ TextControlElement::GetWrapPropertyEnum(mTextCtrlElement, wrapProp) &&
+ wrapProp == TextControlElement::eHTMLTextWrap_Hard) {
+ flags |= nsIDocumentEncoder::OutputWrap;
+ }
+ }
+
+ // What follows is a bit of a hack. The problem is that we could be in
+ // this method because we're being destroyed for whatever reason while
+ // script is executing. If that happens, editor will run with the
+ // privileges of the executing script, which means it may not be able to
+ // access its own DOM nodes! Let's try to deal with that by pushing a null
+ // JSContext on the JSContext stack to make it clear that we're native
+ // code. Note that any script that's directly trying to access our value
+ // has to be going through some scriptable object to do that and that
+ // already does the relevant security checks.
+ // XXXbz if we could just get the textContent of our anonymous content (eg
+ // if plaintext editor didn't create <br> nodes all over), we wouldn't need
+ // this.
+ { /* Scope for AutoNoJSAPI. */
+ AutoNoJSAPI nojsapi;
+
+ DebugOnly<nsresult> rv = mTextEditor->ComputeTextValue(flags, aValue);
+ MOZ_ASSERT(aValue.FindChar(u'\r') == -1);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to get value");
+ }
+ // Only when the result doesn't include line breaks caused by hard-wrap,
+ // mCacheValue should cache the value.
+ if (!(flags & nsIDocumentEncoder::OutputWrap)) {
+ mBoundFrame->CacheValue(aValue);
+ } else {
+ mBoundFrame->ClearCachedValue();
+ }
+ } else if (!mTextCtrlElement->ValueChanged() || mValue.IsVoid()) {
+ // Use nsString to avoid copying string buffer at setting aValue.
+ nsString value;
+ mTextCtrlElement->GetDefaultValueFromContent(value, aForDisplay);
+ // TODO: We should make default value not include \r.
+ nsContentUtils::PlatformToDOMLineBreaks(value);
+ aValue = std::move(value);
+ } else {
+ aValue = mValue;
+ MOZ_ASSERT(aValue.FindChar(u'\r') == -1);
+ }
+}
+
+bool TextControlState::ValueEquals(const nsAString& aValue) const {
+ nsAutoString value;
+ GetValue(value, true, /* aForDisplay = */ true);
+ return aValue.Equals(value);
+}
+
+#ifdef DEBUG
+// @param aOptions TextControlState::ValueSetterOptions
+bool AreFlagsNotDemandingContradictingMovements(
+ const ValueSetterOptions& aOptions) {
+ return !aOptions.contains(
+ {ValueSetterOption::MoveCursorToBeginSetSelectionDirectionForward,
+ ValueSetterOption::MoveCursorToEndIfValueChanged});
+}
+#endif // DEBUG
+
+bool TextControlState::SetValue(const nsAString& aValue,
+ const nsAString* aOldValue,
+ const ValueSetterOptions& aOptions) {
+ if (mHandlingState &&
+ mHandlingState->IsHandling(TextControlAction::CommitComposition)) {
+ // GetValue doesn't return current text frame's content during committing.
+ // So we cannot trust this old value
+ aOldValue = nullptr;
+ }
+
+ if (mPasswordMaskData) {
+ if (mHandlingState &&
+ mHandlingState->Is(TextControlAction::UnbindFromFrame)) {
+ // If we're called by UnbindFromFrame, we shouldn't reset unmasked range.
+ } else {
+ // Otherwise, we should mask the new password, even if it's same value
+ // since the same value may be one for different web app's.
+ mPasswordMaskData->Reset();
+ }
+ }
+
+ const bool wasHandlingSetValue =
+ mHandlingState && mHandlingState->IsHandling(TextControlAction::SetValue);
+
+ ErrorResult error;
+ AutoTextControlHandlingState handlingSetValue(
+ *this, TextControlAction::SetValue, aValue, aOldValue, aOptions, error);
+ if (error.Failed()) {
+ MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY));
+ error.SuppressException();
+ return false;
+ }
+
+ const auto changeKind = [&] {
+ if (aOptions.contains(ValueSetterOption::ByInternalAPI)) {
+ return ValueChangeKind::Internal;
+ }
+ if (aOptions.contains(ValueSetterOption::BySetUserInputAPI)) {
+ return ValueChangeKind::UserInteraction;
+ }
+ return ValueChangeKind::Script;
+ }();
+
+ if (changeKind == ValueChangeKind::Script) {
+ // This value change will not be interactive. If we're an input that was
+ // interactively edited, save the last interactive value now before it goes
+ // away.
+ if (auto* input = HTMLInputElement::FromNode(mTextCtrlElement)) {
+ if (input->LastValueChangeWasInteractive()) {
+ GetValue(mLastInteractiveValue, /* aIgnoreWrap = */ true,
+ /* aForDisplay = */ true);
+ }
+ }
+ }
+
+ // Note that if this may be called during reframe of the editor. In such
+ // case, we shouldn't commit composition. Therefore, when this is called
+ // for internal processing, we shouldn't commit the composition.
+ // TODO: In strictly speaking, we should move committing composition into
+ // editor because if "beforeinput" for this setting value is canceled,
+ // we shouldn't commit composition. However, in Firefox, we never
+ // call this via `setUserInput` during composition. Therefore, the
+ // bug must not be reproducible actually.
+ if (aOptions.contains(ValueSetterOption::BySetUserInputAPI) ||
+ aOptions.contains(ValueSetterOption::ByContentAPI)) {
+ if (EditorHasComposition()) {
+ // When this is called recursively, there shouldn't be composition.
+ if (handlingSetValue.IsHandling(TextControlAction::CommitComposition)) {
+ // Don't request to commit composition again. But if it occurs,
+ // we should skip to set the new value to the editor here. It should
+ // be set later with the newest value.
+ return true;
+ }
+ if (NS_WARN_IF(!mBoundFrame)) {
+ // We're not sure if this case is possible.
+ } else {
+ // If setting value won't change current value, we shouldn't commit
+ // composition for compatibility with the other browsers.
+ MOZ_ASSERT(!aOldValue || ValueEquals(*aOldValue));
+ bool isSameAsCurrentValue =
+ aOldValue ? aOldValue->Equals(handlingSetValue.GetSettingValue())
+ : ValueEquals(handlingSetValue.GetSettingValue());
+ if (isSameAsCurrentValue) {
+ // Note that in this case, we shouldn't fire any events with setting
+ // value because event handlers may try to set value recursively but
+ // we cannot commit composition at that time due to unsafe to run
+ // script (see below).
+ return true;
+ }
+ }
+ // If there is composition, need to commit composition first because
+ // other browsers do that.
+ // NOTE: We don't need to block nested calls of this because input nor
+ // other events won't be fired by setting values and script blocker
+ // is used during setting the value to the editor. IE also allows
+ // to set the editor value on the input event which is caused by
+ // forcibly committing composition.
+ AutoTextControlHandlingState handlingCommitComposition(
+ *this, TextControlAction::CommitComposition);
+ if (nsContentUtils::IsSafeToRunScript()) {
+ // WARNING: During this call, compositionupdate, compositionend, input
+ // events will be fired. Therefore, everything can occur. E.g., the
+ // document may be unloaded.
+ RefPtr<TextEditor> textEditor = mTextEditor;
+ nsresult rv = textEditor->CommitComposition();
+ if (handlingCommitComposition.IsTextControlStateDestroyed()) {
+ return true;
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextControlState failed to commit composition");
+ return true;
+ }
+ // Note that if a composition event listener sets editor value again,
+ // we should use the new value here. The new value is stored in
+ // handlingSetValue right now.
+ } else {
+ NS_WARNING(
+ "SetValue() is called when there is composition but "
+ "it's not safe to request to commit the composition");
+ }
+ }
+ }
+
+ if (mTextEditor && mBoundFrame) {
+ if (!SetValueWithTextEditor(handlingSetValue)) {
+ return false;
+ }
+ } else if (!SetValueWithoutTextEditor(handlingSetValue)) {
+ return false;
+ }
+
+ // If we were handling SetValue() before, don't update the DOM state twice,
+ // just let the outer call do so.
+ if (!wasHandlingSetValue) {
+ handlingSetValue.GetTextControlElement()->OnValueChanged(
+ changeKind, handlingSetValue.GetSettingValue());
+ }
+ return true;
+}
+
+bool TextControlState::SetValueWithTextEditor(
+ AutoTextControlHandlingState& aHandlingSetValue) {
+ MOZ_ASSERT(aHandlingSetValue.Is(TextControlAction::SetValue));
+ MOZ_ASSERT(mTextEditor);
+ MOZ_ASSERT(mBoundFrame);
+ NS_WARNING_ASSERTION(!EditorHasComposition(),
+ "Failed to commit composition before setting value. "
+ "Investigate the cause!");
+
+#ifdef DEBUG
+ if (IsSingleLineTextControl()) {
+ NS_ASSERTION(mEditorInitialized || aHandlingSetValue.IsHandling(
+ TextControlAction::PrepareEditor),
+ "We should never try to use the editor if we're not "
+ "initialized unless we're being initialized");
+ }
+#endif
+
+ MOZ_ASSERT(!aHandlingSetValue.GetOldValue() ||
+ ValueEquals(*aHandlingSetValue.GetOldValue()));
+ const bool isSameAsCurrentValue =
+ aHandlingSetValue.GetOldValue()
+ ? aHandlingSetValue.GetOldValue()->Equals(
+ aHandlingSetValue.GetSettingValue())
+ : ValueEquals(aHandlingSetValue.GetSettingValue());
+
+ // this is necessary to avoid infinite recursion
+ if (isSameAsCurrentValue) {
+ return true;
+ }
+
+ RefPtr<TextEditor> textEditor = mTextEditor;
+
+ nsCOMPtr<Document> document = textEditor->GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return true;
+ }
+
+ // Time to mess with our security context... See comments in GetValue()
+ // for why this is needed. Note that we have to do this up here, because
+ // otherwise SelectAll() will fail.
+ AutoNoJSAPI nojsapi;
+
+ // FYI: It's safe to use raw pointer for selection here because
+ // SelectionBatcher will grab it with RefPtr.
+ Selection* selection = mSelCon->GetSelection(SelectionType::eNormal);
+ SelectionBatcher selectionBatcher(selection, __FUNCTION__);
+
+ // get the flags, remove readonly, disabled and max-length,
+ // set the value, restore flags
+ AutoRestoreEditorState restoreState(textEditor);
+
+ aHandlingSetValue.WillSetValueWithTextEditor();
+
+ if (aHandlingSetValue.ValueSetterOptionsRef().contains(
+ ValueSetterOption::BySetUserInputAPI)) {
+ // If the caller inserts text as part of user input, for example,
+ // autocomplete, we need to replace the text as "insert string"
+ // because undo should cancel only this operation (i.e., previous
+ // transactions typed by user shouldn't be merged with this).
+ // In this case, we need to dispatch "input" event because
+ // web apps may need to know the user's operation.
+ // In this case, we need to dispatch "beforeinput" events since
+ // we're emulating the user's input. Passing nullptr as
+ // nsIPrincipal means that that may be user's input. So, let's
+ // do it.
+ nsresult rv = textEditor->ReplaceTextAsAction(
+ aHandlingSetValue.GetSettingValue(), nullptr,
+ StaticPrefs::dom_input_event_allow_to_cancel_set_user_input()
+ ? TextEditor::AllowBeforeInputEventCancelable::Yes
+ : TextEditor::AllowBeforeInputEventCancelable::No,
+ nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::ReplaceTextAsAction() failed");
+ return rv != NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Don't dispatch "beforeinput" event nor "input" event for setting value
+ // by script.
+ AutoInputEventSuppresser suppressInputEventDispatching(textEditor);
+
+ // On <input> or <textarea>, we shouldn't preserve existing undo
+ // transactions because other browsers do not preserve them too
+ // and not preserving transactions makes setting value faster.
+ //
+ // (Except if chrome opts into this behavior).
+ Maybe<AutoDisableUndo> disableUndo;
+ if (!aHandlingSetValue.ValueSetterOptionsRef().contains(
+ ValueSetterOption::PreserveUndoHistory)) {
+ disableUndo.emplace(textEditor);
+ }
+
+ if (selection) {
+ // Since we don't use undo transaction, we don't need to store
+ // selection state. SetText will set selection to tail.
+ IgnoredErrorResult ignoredError;
+ MOZ_KnownLive(selection)->RemoveAllRanges(ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "Selection::RemoveAllRanges() failed, but ignored");
+ }
+
+ // In this case, we makes the editor stop dispatching "input"
+ // event so that passing nullptr as nsIPrincipal is safe for now.
+ nsresult rv = textEditor->SetTextAsAction(
+ aHandlingSetValue.GetSettingValue(),
+ aHandlingSetValue.ValueSetterOptionsRef().contains(
+ ValueSetterOption::BySetUserInputAPI) &&
+ !StaticPrefs::dom_input_event_allow_to_cancel_set_user_input()
+ ? TextEditor::AllowBeforeInputEventCancelable::No
+ : TextEditor::AllowBeforeInputEventCancelable::Yes,
+ nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetTextAsAction() failed");
+
+ // Call the listener's OnEditActionHandled() callback manually if
+ // OnEditActionHandled() hasn't been called yet since TextEditor don't use
+ // the transaction manager in this path and it could be that the editor
+ // would bypass calling the listener for that reason.
+ if (!aHandlingSetValue.HasEditActionHandled()) {
+ nsresult rvOnEditActionHandled =
+ MOZ_KnownLive(aHandlingSetValue.GetTextInputListener())
+ ->OnEditActionHandled(*textEditor);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvOnEditActionHandled),
+ "TextInputListener::OnEditActionHandled() failed");
+ if (rv != NS_ERROR_OUT_OF_MEMORY) {
+ rv = rvOnEditActionHandled;
+ }
+ }
+
+ // When the <textarea> is not dirty, the default value is mirrored into the
+ // anonymous subtree asynchronously. This may occur during a reframe.
+ // Therefore, if IMEContentObserver was initialized with our editor but our
+ // editor is being initialized, it has not been observing the new anonymous
+ // subtree. In this case, we need to notify IMEContentObserver of the default
+ // value change.
+ if (mTextCtrlElement && mTextCtrlElement->IsTextArea() &&
+ !mTextCtrlElement->ValueChanged() && textEditor->IsBeingInitialized() &&
+ !textEditor->Destroyed()) {
+ IMEContentObserver* observer = GetIMEContentObserver();
+ if (observer && observer->WasInitializedWith(*textEditor)) {
+ nsAutoString currentValue;
+ textEditor->ComputeTextValue(0, currentValue);
+ observer->OnTextControlValueChangedWhileNotObservable(currentValue);
+ }
+ }
+
+ return rv != NS_ERROR_OUT_OF_MEMORY;
+}
+
+bool TextControlState::SetValueWithoutTextEditor(
+ AutoTextControlHandlingState& aHandlingSetValue) {
+ MOZ_ASSERT(aHandlingSetValue.Is(TextControlAction::SetValue));
+ MOZ_ASSERT(!mTextEditor || !mBoundFrame);
+ NS_WARNING_ASSERTION(!EditorHasComposition(),
+ "Failed to commit composition before setting value. "
+ "Investigate the cause!");
+
+ if (mValue.IsVoid()) {
+ mValue.SetIsVoid(false);
+ }
+
+ // We can't just early-return here, because OnValueChanged below still need to
+ // be called.
+ if (!mValue.Equals(aHandlingSetValue.GetSettingValue()) ||
+ !StaticPrefs::dom_input_skip_cursor_move_for_same_value_set()) {
+ bool handleSettingValue = true;
+ // If `SetValue()` call is nested, `GetSettingValue()` result will be
+ // modified. So, we need to store input event data value before
+ // dispatching beforeinput event.
+ nsString inputEventData(aHandlingSetValue.GetSettingValue());
+ if (aHandlingSetValue.ValueSetterOptionsRef().contains(
+ ValueSetterOption::BySetUserInputAPI) &&
+ !aHandlingSetValue.HasBeforeInputEventDispatched()) {
+ // This probably occurs when session restorer sets the old value with
+ // `setUserInput`. If so, we need to dispatch "beforeinput" event of
+ // "insertReplacementText" for conforming to the spec. However, the
+ // spec does NOT treat the session restoring case. Therefore, if this
+ // breaks session restorere in a lot of web apps, we should probably
+ // stop dispatching it or make it non-cancelable.
+ MOZ_ASSERT(aHandlingSetValue.GetTextControlElement());
+ MOZ_ASSERT(!aHandlingSetValue.GetSettingValue().IsVoid());
+ aHandlingSetValue.WillDispatchBeforeInputEvent();
+ nsEventStatus status = nsEventStatus_eIgnore;
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
+ MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()),
+ eEditorBeforeInput, EditorInputType::eInsertReplacementText, nullptr,
+ InputEventOptions(
+ inputEventData,
+ StaticPrefs::dom_input_event_allow_to_cancel_set_user_input()
+ ? InputEventOptions::NeverCancelable::No
+ : InputEventOptions::NeverCancelable::Yes),
+ &status);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch beforeinput event");
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ return true; // "beforeinput" event was canceled.
+ }
+ // If we were destroyed by "beforeinput" event listeners, probably, we
+ // don't need to keep handling it.
+ if (aHandlingSetValue.IsTextControlStateDestroyed()) {
+ return true;
+ }
+ // Even if "beforeinput" event was not canceled, its listeners may do
+ // something. If it causes creating `TextEditor` and bind this to a
+ // frame, we need to use the path, but `TextEditor` shouldn't fire
+ // "beforeinput" event again. Therefore, we need to prevent editor
+ // to dispatch it.
+ if (mTextEditor && mBoundFrame) {
+ AutoInputEventSuppresser suppressInputEvent(mTextEditor);
+ if (!SetValueWithTextEditor(aHandlingSetValue)) {
+ return false;
+ }
+ // If we were destroyed by "beforeinput" event listeners, probably, we
+ // don't need to keep handling it.
+ if (aHandlingSetValue.IsTextControlStateDestroyed()) {
+ return true;
+ }
+ handleSettingValue = false;
+ }
+ }
+
+ if (handleSettingValue) {
+ if (!mValue.Assign(aHandlingSetValue.GetSettingValue(), fallible)) {
+ return false;
+ }
+
+ // Since we have no editor we presumably have cached selection state.
+ if (IsSelectionCached()) {
+ MOZ_ASSERT(AreFlagsNotDemandingContradictingMovements(
+ aHandlingSetValue.ValueSetterOptionsRef()));
+
+ SelectionProperties& props = GetSelectionProperties();
+ // Setting a max length and thus capping selection range early prevents
+ // selection change detection in setRangeText. Temporarily disable
+ // capping here with UINT32_MAX, and set it later in ::SetRangeText().
+ props.SetMaxLength(aHandlingSetValue.ValueSetterOptionsRef().contains(
+ ValueSetterOption::BySetRangeTextAPI)
+ ? UINT32_MAX
+ : aHandlingSetValue.GetSettingValue().Length());
+ if (aHandlingSetValue.ValueSetterOptionsRef().contains(
+ ValueSetterOption::MoveCursorToEndIfValueChanged)) {
+ props.SetStart(aHandlingSetValue.GetSettingValue().Length());
+ props.SetEnd(aHandlingSetValue.GetSettingValue().Length());
+ props.SetDirection(SelectionDirection::Forward);
+ } else if (aHandlingSetValue.ValueSetterOptionsRef().contains(
+ ValueSetterOption::
+ MoveCursorToBeginSetSelectionDirectionForward)) {
+ props.SetStart(0);
+ props.SetEnd(0);
+ props.SetDirection(SelectionDirection::Forward);
+ }
+ }
+
+ // Update the frame display if needed
+ if (mBoundFrame) {
+ mBoundFrame->UpdateValueDisplay(true);
+ }
+
+ // If the text control element has focus, IMEContentObserver is not
+ // observing the content changes due to no bound frame or no TextEditor.
+ // Therefore, we need to let IMEContentObserver know all values are being
+ // replaced.
+ if (IMEContentObserver* observer = GetIMEContentObserver()) {
+ observer->OnTextControlValueChangedWhileNotObservable(mValue);
+ }
+ }
+
+ // If this is called as part of user input, we need to dispatch "input"
+ // event with "insertReplacementText" since web apps may want to know
+ // the user operation which changes editor value with a built-in function
+ // like autocomplete, password manager, session restore, etc.
+ // XXX Should we stop dispatching `input` event if the text control
+ // element has already removed from the DOM tree by a `beforeinput`
+ // event listener?
+ if (aHandlingSetValue.ValueSetterOptionsRef().contains(
+ ValueSetterOption::BySetUserInputAPI)) {
+ MOZ_ASSERT(aHandlingSetValue.GetTextControlElement());
+
+ // Update validity state before dispatching "input" event for its
+ // listeners like `EditorBase::NotifyEditorObservers()`.
+ aHandlingSetValue.GetTextControlElement()->OnValueChanged(
+ ValueChangeKind::UserInteraction,
+ aHandlingSetValue.GetSettingValue());
+
+ ClearLastInteractiveValue();
+
+ MOZ_ASSERT(!aHandlingSetValue.GetSettingValue().IsVoid());
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
+ MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()),
+ eEditorInput, EditorInputType::eInsertReplacementText, nullptr,
+ InputEventOptions(inputEventData,
+ InputEventOptions::NeverCancelable::No));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Failed to dispatch input event");
+ }
+ } else {
+ // Even if our value is not actually changing, apparently we need to mark
+ // our SelectionProperties dirty to make accessibility tests happy.
+ // Probably because they depend on the SetSelectionRange() call we make on
+ // our frame in RestoreSelectionState, but I have no idea why they do.
+ if (IsSelectionCached()) {
+ SelectionProperties& props = GetSelectionProperties();
+ props.SetIsDirty();
+ }
+ }
+
+ return true;
+}
+
+void TextControlState::InitializeKeyboardEventListeners() {
+ // register key listeners
+ EventListenerManager* manager =
+ mTextCtrlElement->GetOrCreateListenerManager();
+ if (manager) {
+ manager->AddEventListenerByType(mTextListener, u"keydown"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ manager->AddEventListenerByType(mTextListener, u"keypress"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ manager->AddEventListenerByType(mTextListener, u"keyup"_ns,
+ TrustedEventsAtSystemGroupBubble());
+ }
+
+ mSelCon->SetScrollableFrame(mBoundFrame->GetScrollTargetFrame());
+}
+
+void TextControlState::SetPreviewText(const nsAString& aValue, bool aNotify) {
+ // If we don't have a preview div, there's nothing to do.
+ Element* previewDiv = GetPreviewNode();
+ if (!previewDiv) {
+ return;
+ }
+
+ nsAutoString previewValue(aValue);
+
+ nsContentUtils::RemoveNewlines(previewValue);
+ MOZ_ASSERT(previewDiv->GetFirstChild(), "preview div has no child");
+ previewDiv->GetFirstChild()->AsText()->SetText(previewValue, aNotify);
+}
+
+void TextControlState::GetPreviewText(nsAString& aValue) {
+ // If we don't have a preview div, there's nothing to do.
+ Element* previewDiv = GetPreviewNode();
+ if (!previewDiv) {
+ return;
+ }
+
+ MOZ_ASSERT(previewDiv->GetFirstChild(), "preview div has no child");
+ const nsTextFragment* text = previewDiv->GetFirstChild()->GetText();
+
+ aValue.Truncate();
+ text->AppendTo(aValue);
+}
+
+bool TextControlState::EditorHasComposition() {
+ return mTextEditor && mTextEditor->IsIMEComposing();
+}
+
+IMEContentObserver* TextControlState::GetIMEContentObserver() const {
+ if (NS_WARN_IF(!mTextCtrlElement) ||
+ mTextCtrlElement != IMEStateManager::GetFocusedElement()) {
+ return nullptr;
+ }
+ IMEContentObserver* observer = IMEStateManager::GetActiveContentObserver();
+ // The text control element may be an editing host. In this case, the
+ // observer does not observe the anonymous nodes under mTextCtrlElement.
+ // So, it means that the observer is not for ours.
+ return observer && observer->EditorIsTextEditor() ? observer : nullptr;
+}
+
+} // namespace mozilla
diff --git a/dom/html/TextControlState.h b/dom/html/TextControlState.h
new file mode 100644
index 0000000000..3dba24d255
--- /dev/null
+++ b/dom/html/TextControlState.h
@@ -0,0 +1,550 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_TextControlState_h
+#define mozilla_TextControlState_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TextControlElement.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/dom/HTMLInputElementBinding.h"
+#include "mozilla/dom/Nullable.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsITextControlFrame.h"
+#include "nsITimer.h"
+
+class nsTextControlFrame;
+class nsISelectionController;
+class nsFrameSelection;
+class nsFrame;
+
+namespace mozilla {
+
+class AutoTextControlHandlingState;
+class ErrorResult;
+class IMEContentObserver;
+class TextEditor;
+class TextInputListener;
+class TextInputSelectionController;
+
+namespace dom {
+class Element;
+class HTMLInputElement;
+} // namespace dom
+
+/**
+ * PasswordMaskData stores making information and necessary timer for
+ * `TextEditor` instances.
+ */
+struct PasswordMaskData final {
+ // Timer to mask unmasked characters automatically. Used only when it's
+ // a password field.
+ nsCOMPtr<nsITimer> mTimer;
+
+ // Unmasked character range. Used only when it's a password field.
+ // If mUnmaskedLength is 0, it means there is no unmasked characters.
+ uint32_t mUnmaskedStart = UINT32_MAX;
+ uint32_t mUnmaskedLength = 0;
+
+ // Set to true if all characters are masked or waiting notification from
+ // `mTimer`. Otherwise, i.e., part of or all of password is unmasked
+ // without setting `mTimer`, set to false.
+ bool mIsMaskingPassword = true;
+
+ // Set to true if a manager of the instance wants to disable echoing
+ // password temporarily.
+ bool mEchoingPasswordPrevented = false;
+
+ MOZ_ALWAYS_INLINE bool IsAllMasked() const {
+ return mUnmaskedStart == UINT32_MAX && mUnmaskedLength == 0;
+ }
+ MOZ_ALWAYS_INLINE uint32_t UnmaskedEnd() const {
+ return mUnmaskedStart + mUnmaskedLength;
+ }
+ MOZ_ALWAYS_INLINE void MaskAll() {
+ mUnmaskedStart = UINT32_MAX;
+ mUnmaskedLength = 0;
+ }
+ MOZ_ALWAYS_INLINE void Reset() {
+ MaskAll();
+ mIsMaskingPassword = true;
+ }
+ enum class ReleaseTimer { No, Yes };
+ MOZ_ALWAYS_INLINE void CancelTimer(ReleaseTimer aReleaseTimer) {
+ if (mTimer) {
+ mTimer->Cancel();
+ if (aReleaseTimer == ReleaseTimer::Yes) {
+ mTimer = nullptr;
+ }
+ }
+ if (mIsMaskingPassword) {
+ MaskAll();
+ }
+ }
+};
+
+/**
+ * TextControlState is a class which is responsible for managing the state of
+ * plaintext controls. This currently includes the following HTML elements:
+ * <input type=text>
+ * <input type=search>
+ * <input type=url>
+ * <input type=telephone>
+ * <input type=email>
+ * <input type=password>
+ * <textarea>
+ *
+ * This class is held as a member of HTMLInputElement and HTMLTextAreaElement.
+ * The public functions in this class include the public APIs which dom/
+ * uses. Layout code uses the TextControlElement interface to invoke
+ * functions on this class.
+ *
+ * The design motivation behind this class is maintaining all of the things
+ * which collectively are considered the "state" of the text control in a single
+ * location. This state includes several things:
+ *
+ * * The control's value. This value is stored in the mValue member, and is
+ * only used when there is no frame for the control, or when the editor object
+ * has not been initialized yet.
+ *
+ * * The control's associated frame. This value is stored in the mBoundFrame
+ * member. A text control might never have an associated frame during its life
+ * cycle, or might have several different ones, but at any given moment in time
+ * there is a maximum of 1 bound frame to each text control.
+ *
+ * * The control's associated editor. This value is stored in the mTextEditor
+ * member. An editor is initialized for the control only when necessary (that
+ * is, when either the user is about to interact with the text control, or when
+ * some other code needs to access the editor object. Without a frame bound to
+ * the control, an editor is never initialized. Once initialized, the editor
+ * might outlive the frame, in which case the same editor will be used if a new
+ * frame gets bound to the text control.
+ *
+ * * The anonymous content associated with the text control's frame, including
+ * the value div (the DIV element responsible for holding the value of the text
+ * control) and the placeholder div (the DIV element responsible for holding the
+ * placeholder value of the text control.) These values are stored in the
+ * mRootNode and mPlaceholderDiv members, respectively. They will be created
+ * when a frame is bound to the text control. They will be destroyed when the
+ * frame is unbound from the object. We could try and hold on to the anonymous
+ * content between different frames, but unfortunately that is not currently
+ * possible because they are not unbound from the document in time.
+ *
+ * * The frame selection controller. This value is stored in the mSelCon
+ * member. The frame selection controller is responsible for maintaining the
+ * selection state on a frame. It is created when a frame is bound to the text
+ * control element, and will be destroy when the frame is being unbound from the
+ * text control element. It is created alongside with the frame selection object
+ * which is stored in the mFrameSel member.
+ *
+ * * The editor text listener. This value is stored in the mTextListener
+ * member. Its job is to listen to selection and keyboard events, and act
+ * accordingly. It is created when an a frame is first bound to the control, and
+ * will be destroyed when the frame is unbound from the text control element.
+ *
+ * * The editor's cached value. This value is stored in the mCachedValue
+ * member. It is used to improve the performance of append operations to the
+ * text control. A mutation observer stored in the mMutationObserver has the
+ * job of invalidating this cache when the anonymous contect containing the
+ * value is changed.
+ *
+ * * The editor's cached selection properties. These vales are stored in the
+ * mSelectionProperties member, and include the selection's start, end and
+ * direction. They are only used when there is no frame available for the
+ * text field.
+ *
+ *
+ * As a general rule, TextControlState objects own the value of the text
+ * control, and any attempt to retrieve or set the value must be made through
+ * those objects. Internally, the value can be represented in several different
+ * ways, based on the state the control is in.
+ *
+ * * When the control is first initialized, its value is equal to the default
+ * value of the DOM node. For <input> text controls, this default value is the
+ * value of the value attribute. For <textarea> elements, this default value is
+ * the value of the text node children of the element.
+ *
+ * * If the value has been changed through the DOM node (before the editor for
+ * the object is initialized), the value is stored as a simple string inside the
+ * mValue member of the TextControlState object.
+ *
+ * * If an editor has been initialized for the control, the value is set and
+ * retrievd via the nsIEditor interface, and is internally managed by the
+ * editor as the native anonymous content tree attached to the control's frame.
+ *
+ * * If the text control state object is unbound from the control's frame, the
+ * value is transferred to the mValue member variable, and will be managed there
+ * until a new frame is bound to the text editor state object.
+ */
+
+class RestoreSelectionState;
+
+class TextControlState final : public SupportsWeakPtr {
+ public:
+ using Element = dom::Element;
+ using HTMLInputElement = dom::HTMLInputElement;
+ using SelectionDirection = nsITextControlFrame::SelectionDirection;
+
+ static TextControlState* Construct(TextControlElement* aOwningElement);
+
+ // Note that this does not run script actually because of `sHasShutDown`
+ // is set to true before calling `DeleteOrCacheForReuse()`.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY static void Shutdown();
+
+ /**
+ * Destroy() deletes the instance immediately or later.
+ */
+ MOZ_CAN_RUN_SCRIPT void Destroy();
+
+ TextControlState() = delete;
+ explicit TextControlState(const TextControlState&) = delete;
+ TextControlState(TextControlState&&) = delete;
+
+ void operator=(const TextControlState&) = delete;
+ void operator=(TextControlState&&) = delete;
+
+ void Traverse(nsCycleCollectionTraversalCallback& cb);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void Unlink();
+
+ bool IsBusy() const { return !!mHandlingState || mValueTransferInProgress; }
+
+ MOZ_CAN_RUN_SCRIPT TextEditor* GetTextEditor();
+ TextEditor* GetTextEditorWithoutCreation() const;
+ nsISelectionController* GetSelectionController() const;
+ nsFrameSelection* GetConstFrameSelection();
+ nsresult BindToFrame(nsTextControlFrame* aFrame);
+ MOZ_CAN_RUN_SCRIPT void UnbindFromFrame(nsTextControlFrame* aFrame);
+ MOZ_CAN_RUN_SCRIPT nsresult PrepareEditor(const nsAString* aValue = nullptr);
+ void InitializeKeyboardEventListeners();
+
+ /**
+ * OnEditActionHandled() is called when mTextEditor handles something
+ * and immediately before dispatching "input" event.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult OnEditActionHandled();
+
+ enum class ValueSetterOption {
+ // The call is for setting value to initial one, computed one, etc.
+ ByInternalAPI,
+ // The value is changed by a call of setUserInput() API from chrome.
+ BySetUserInputAPI,
+ // The value is changed by changing value attribute of the element or
+ // something like setRangeText().
+ ByContentAPI,
+ // The value is changed by setRangeText(). Intended to prevent silent
+ // selection range change.
+ BySetRangeTextAPI,
+ // Whether SetValueChanged should be called as a result of this value
+ // change.
+ SetValueChanged,
+ // Whether to move the cursor to end of the value (in the case when we have
+ // cached selection offsets), in the case when the value has changed. If
+ // this is not set and MoveCursorToBeginSetSelectionDirectionForward
+ // is not set, the cached selection offsets will simply be clamped to
+ // be within the length of the new value. In either case, if the value has
+ // not changed the cursor won't move.
+ // TODO(mbrodesser): update comment and enumerator identifier to reflect
+ // that also the direction is set to forward.
+ MoveCursorToEndIfValueChanged,
+
+ // The value change should preserve undo history.
+ PreserveUndoHistory,
+
+ // Whether it should be tried to move the cursor to the beginning of the
+ // text control and set the selection direction to "forward".
+ // TODO(mbrodesser): As soon as "none" is supported
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1541454), it should be set
+ // to "none" and only fall back to "forward" if the platform doesn't support
+ // it.
+ MoveCursorToBeginSetSelectionDirectionForward,
+ };
+ using ValueSetterOptions = EnumSet<ValueSetterOption, uint32_t>;
+
+ /**
+ * SetValue() sets the value to aValue with replacing \r\n and \r with \n.
+ *
+ * @param aValue The new value. Can contain \r.
+ * @param aOldValue Optional. If you have already know current value,
+ * set this to it. However, this must not contain \r
+ * for the performance.
+ * @param aOptions See ValueSetterOption.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool SetValue(
+ const nsAString& aValue, const nsAString* aOldValue,
+ const ValueSetterOptions& aOptions);
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool SetValue(
+ const nsAString& aValue, const ValueSetterOptions& aOptions) {
+ return SetValue(aValue, nullptr, aOptions);
+ }
+
+ /**
+ * GetValue() returns current value either with or without TextEditor.
+ * The result never includes \r.
+ */
+ void GetValue(nsAString& aValue, bool aIgnoreWrap, bool aForDisplay) const;
+
+ /**
+ * ValueEquals() is designed for internal use so that aValue shouldn't
+ * include \r character. It should be handled before calling this with
+ * nsContentUtils::PlatformToDOMLineBreaks().
+ */
+ bool ValueEquals(const nsAString& aValue) const;
+ // The following methods are for textarea element to use whether default
+ // value or not.
+ // XXX We might have to add assertion when it is into editable,
+ // or reconsider fixing bug 597525 to remove these.
+ void EmptyValue() {
+ if (!mValue.IsVoid()) {
+ mValue.Truncate();
+ }
+ }
+ bool IsEmpty() const { return mValue.IsEmpty(); }
+
+ const nsAString& LastInteractiveValueIfLastChangeWasNonInteractive() const {
+ return mLastInteractiveValue;
+ }
+ // When an interactive value change happens, we clear mLastInteractiveValue
+ // because it's not needed (mValue is the new interactive value).
+ void ClearLastInteractiveValue() { mLastInteractiveValue.SetIsVoid(true); }
+
+ Element* GetRootNode();
+ Element* GetPreviewNode();
+
+ bool IsSingleLineTextControl() const {
+ return mTextCtrlElement->IsSingleLineTextControl();
+ }
+ bool IsTextArea() const { return mTextCtrlElement->IsTextArea(); }
+ bool IsPasswordTextControl() const {
+ return mTextCtrlElement->IsPasswordTextControl();
+ }
+ int32_t GetCols() { return mTextCtrlElement->GetCols(); }
+ int32_t GetWrapCols() {
+ int32_t wrapCols = mTextCtrlElement->GetWrapCols();
+ MOZ_ASSERT(wrapCols >= 0);
+ return wrapCols;
+ }
+ int32_t GetRows() { return mTextCtrlElement->GetRows(); }
+
+ // preview methods
+ void SetPreviewText(const nsAString& aValue, bool aNotify);
+ void GetPreviewText(nsAString& aValue);
+
+ struct SelectionProperties {
+ public:
+ bool IsDefault() const {
+ return mStart == 0 && mEnd == 0 &&
+ mDirection == SelectionDirection::Forward;
+ }
+ uint32_t GetStart() const { return mStart; }
+ bool SetStart(uint32_t value) {
+ uint32_t newValue = std::min(value, *mMaxLength);
+ bool changed = mStart != newValue;
+ mStart = newValue;
+ mIsDirty |= changed;
+ return changed;
+ }
+ uint32_t GetEnd() const { return mEnd; }
+ bool SetEnd(uint32_t value) {
+ uint32_t newValue = std::min(value, *mMaxLength);
+ bool changed = mEnd != newValue;
+ mEnd = newValue;
+ mIsDirty |= changed;
+ return changed;
+ }
+ SelectionDirection GetDirection() const { return mDirection; }
+ bool SetDirection(SelectionDirection value) {
+ bool changed = mDirection != value;
+ mDirection = value;
+ mIsDirty |= changed;
+ return changed;
+ }
+ void SetMaxLength(uint32_t aMax) {
+ mMaxLength = Some(aMax);
+ // recompute against the new max length
+ SetStart(GetStart());
+ SetEnd(GetEnd());
+ }
+ bool HasMaxLength() { return mMaxLength.isSome(); }
+
+ // return true only if mStart, mEnd, or mDirection have been modified,
+ // or if SetIsDirty() was explicitly called.
+ bool IsDirty() const { return mIsDirty; }
+ void SetIsDirty() { mIsDirty = true; }
+
+ private:
+ uint32_t mStart = 0;
+ uint32_t mEnd = 0;
+ Maybe<uint32_t> mMaxLength;
+ bool mIsDirty = false;
+ SelectionDirection mDirection = SelectionDirection::Forward;
+ };
+
+ bool IsSelectionCached() const { return mSelectionCached; }
+ SelectionProperties& GetSelectionProperties() { return mSelectionProperties; }
+ MOZ_CAN_RUN_SCRIPT void SetSelectionProperties(SelectionProperties& aProps);
+ bool HasNeverInitializedBefore() const { return !mEverInited; }
+ // Sync up our selection properties with our editor prior to being destroyed.
+ // This will invoke UnbindFromFrame() to ensure that we grab whatever
+ // selection state may be at the moment.
+ MOZ_CAN_RUN_SCRIPT void SyncUpSelectionPropertiesBeforeDestruction();
+
+ // Get the selection range start and end points in our text.
+ void GetSelectionRange(uint32_t* aSelectionStart, uint32_t* aSelectionEnd,
+ ErrorResult& aRv);
+
+ // Get the selection direction
+ nsITextControlFrame::SelectionDirection GetSelectionDirection(
+ ErrorResult& aRv);
+
+ enum class ScrollAfterSelection { No, Yes };
+
+ // Set the selection range (start, end, direction). aEnd is allowed to be
+ // smaller than aStart; in that case aStart will be reset to the same value as
+ // aEnd. This basically implements
+ // https://html.spec.whatwg.org/multipage/forms.html#set-the-selection-range
+ // but with the start/end already coerced to zero if null (and without the
+ // special infinity value), and the direction already converted to a
+ // SelectionDirection.
+ //
+ // If we have a frame, this method will scroll the selection into view.
+ MOZ_CAN_RUN_SCRIPT void SetSelectionRange(
+ uint32_t aStart, uint32_t aEnd,
+ nsITextControlFrame::SelectionDirection aDirection, ErrorResult& aRv,
+ ScrollAfterSelection aScroll = ScrollAfterSelection::Yes);
+
+ // Set the selection range, but with an optional string for the direction.
+ // This will convert aDirection to an nsITextControlFrame::SelectionDirection
+ // and then call our other SetSelectionRange overload.
+ MOZ_CAN_RUN_SCRIPT void SetSelectionRange(
+ uint32_t aSelectionStart, uint32_t aSelectionEnd,
+ const dom::Optional<nsAString>& aDirection, ErrorResult& aRv,
+ ScrollAfterSelection aScroll = ScrollAfterSelection::Yes);
+
+ // Set the selection start. This basically implements the
+ // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-selectionstart
+ // setter.
+ MOZ_CAN_RUN_SCRIPT void SetSelectionStart(
+ const dom::Nullable<uint32_t>& aStart, ErrorResult& aRv);
+
+ // Set the selection end. This basically implements the
+ // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-selectionend
+ // setter.
+ MOZ_CAN_RUN_SCRIPT void SetSelectionEnd(const dom::Nullable<uint32_t>& aEnd,
+ ErrorResult& aRv);
+
+ // Get the selection direction as a string. This implements the
+ // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-selectiondirection
+ // getter.
+ void GetSelectionDirectionString(nsAString& aDirection, ErrorResult& aRv);
+
+ // Set the selection direction. This basically implements the
+ // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-selectiondirection
+ // setter.
+ MOZ_CAN_RUN_SCRIPT void SetSelectionDirection(const nsAString& aDirection,
+ ErrorResult& aRv);
+
+ // Set the range text. This basically implements
+ // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-setrangetext
+ MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement,
+ ErrorResult& aRv);
+ // The last two arguments are -1 if we don't know our selection range;
+ // otherwise they're the start and end of our selection range.
+ MOZ_CAN_RUN_SCRIPT void SetRangeText(
+ const nsAString& aReplacement, uint32_t aStart, uint32_t aEnd,
+ dom::SelectionMode aSelectMode, ErrorResult& aRv,
+ const Maybe<uint32_t>& aSelectionStart = Nothing(),
+ const Maybe<uint32_t>& aSelectionEnd = Nothing());
+
+ private:
+ explicit TextControlState(TextControlElement* aOwningElement);
+ MOZ_CAN_RUN_SCRIPT ~TextControlState();
+
+ /**
+ * Delete the instance or cache to reuse it if possible.
+ */
+ MOZ_CAN_RUN_SCRIPT void DeleteOrCacheForReuse();
+
+ MOZ_CAN_RUN_SCRIPT void UnlinkInternal();
+
+ MOZ_CAN_RUN_SCRIPT void DestroyEditor();
+ MOZ_CAN_RUN_SCRIPT void Clear();
+
+ nsresult InitializeRootNode();
+
+ void FinishedRestoringSelection();
+
+ bool EditorHasComposition();
+
+ /**
+ * SetValueWithTextEditor() modifies the editor value with mTextEditor.
+ * This may cause destroying mTextEditor, mBoundFrame, the TextControlState
+ * itself. Must be called when both mTextEditor and mBoundFrame are not
+ * nullptr.
+ *
+ * @param aHandlingSetValue Must be inner-most handling state for SetValue.
+ * @return false if fallible allocation failed. Otherwise,
+ * true.
+ */
+ MOZ_CAN_RUN_SCRIPT bool SetValueWithTextEditor(
+ AutoTextControlHandlingState& aHandlingSetValue);
+
+ /**
+ * SetValueWithoutTextEditor() modifies the value without editor. I.e.,
+ * modifying the value in this instance and mBoundFrame. Must be called
+ * when at least mTextEditor or mBoundFrame is nullptr.
+ *
+ * @param aHandlingSetValue Must be inner-most handling state for SetValue.
+ * @return false if fallible allocation failed. Otherwise,
+ * true.
+ */
+ MOZ_CAN_RUN_SCRIPT bool SetValueWithoutTextEditor(
+ AutoTextControlHandlingState& aHandlingSetValue);
+
+ IMEContentObserver* GetIMEContentObserver() const;
+
+ // When this class handles something which may run script, this should be
+ // set to non-nullptr. If so, this class claims that it's busy and that
+ // prevents destroying TextControlState instance.
+ AutoTextControlHandlingState* mHandlingState = nullptr;
+
+ // The text control element owns this object, and ensures that this object
+ // has a smaller lifetime except the owner releases the instance while it
+ // does something with this.
+ TextControlElement* MOZ_NON_OWNING_REF mTextCtrlElement;
+ RefPtr<TextInputSelectionController> mSelCon;
+ RefPtr<RestoreSelectionState> mRestoringSelection;
+ RefPtr<TextEditor> mTextEditor;
+ nsTextControlFrame* mBoundFrame = nullptr;
+ RefPtr<TextInputListener> mTextListener;
+ UniquePtr<PasswordMaskData> mPasswordMaskData;
+
+ nsString mValue{VoidString()}; // Void if there's no value.
+
+ // If our input's last value change was not interactive (as in, the value
+ // change was caused by a ValueChangeKind::UserInteraction), this is the value
+ // that the last interaction had.
+ nsString mLastInteractiveValue{VoidString()};
+
+ SelectionProperties mSelectionProperties;
+
+ bool mEverInited : 1; // Have we ever been initialized?
+ bool mEditorInitialized : 1;
+ bool mValueTransferInProgress : 1; // Whether a value is being transferred to
+ // the frame
+ bool mSelectionCached : 1; // Whether mSelectionProperties is valid
+
+ friend class AutoTextControlHandlingState;
+ friend class PrepareEditorEvent;
+ friend class RestoreSelectionState;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_TextControlState_h
diff --git a/dom/html/TextInputListener.h b/dom/html/TextInputListener.h
new file mode 100644
index 0000000000..b8f02e4cfa
--- /dev/null
+++ b/dom/html/TextInputListener.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_TextInputListener_h
+#define mozilla_TextInputListener_h
+
+#include "mozilla/WeakPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIDOMEventListener.h"
+#include "nsStringFwd.h"
+#include "nsWeakReference.h"
+
+class nsIFrame;
+class nsTextControlFrame;
+
+namespace mozilla {
+class TextControlElement;
+class TextControlState;
+class TextEditor;
+
+namespace dom {
+class Selection;
+} // namespace dom
+
+class TextInputListener final : public nsIDOMEventListener,
+ public nsSupportsWeakReference {
+ public:
+ explicit TextInputListener(TextControlElement* aTextControlElement);
+
+ void SetFrame(nsIFrame* aTextControlFrame) { mFrame = aTextControlFrame; }
+ void SettingValue(bool aValue) { mSettingValue = aValue; }
+ void SetValueChanged(bool aSetValueChanged) {
+ mSetValueChanged = aSetValueChanged;
+ }
+
+ /**
+ * aFrame is an optional pointer to our frame, if not passed the method will
+ * use mFrame to compute it lazily.
+ */
+ void HandleValueChanged(TextEditor&);
+
+ /**
+ * OnEditActionHandled() is called when the editor handles each edit action.
+ */
+ [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult OnEditActionHandled(TextEditor&);
+
+ /**
+ * OnSelectionChange() is called when selection is changed in the editor.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ void OnSelectionChange(dom::Selection& aSelection, int16_t aReason);
+
+ /**
+ * Start to listen or end listening to selection change in the editor.
+ */
+ void StartToListenToSelectionChange() { mListeningToSelectionChange = true; }
+ void EndListeningToSelectionChange() { mListeningToSelectionChange = false; }
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TextInputListener,
+ nsIDOMEventListener)
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ protected:
+ virtual ~TextInputListener() = default;
+
+ nsresult UpdateTextInputCommands(const nsAString& aCommandsToUpdate);
+
+ protected:
+ nsIFrame* mFrame;
+ TextControlElement* const mTxtCtrlElement;
+ WeakPtr<TextControlState> const mTextControlState;
+
+ bool mSelectionWasCollapsed;
+
+ /**
+ * Whether we had undo items or not the last time we got EditAction()
+ * notification (when this state changes we update undo and redo menus)
+ */
+ bool mHadUndoItems;
+ /**
+ * Whether we had redo items or not the last time we got EditAction()
+ * notification (when this state changes we update undo and redo menus)
+ */
+ bool mHadRedoItems;
+ /**
+ * Whether we're in the process of a SetValue call, and should therefore
+ * refrain from calling OnValueChanged.
+ */
+ bool mSettingValue;
+ /**
+ * Whether we are in the process of a SetValue call that doesn't want
+ * |SetValueChanged| to be called.
+ */
+ bool mSetValueChanged;
+ /**
+ * Whether we're listening to selection change in the editor.
+ */
+ bool mListeningToSelectionChange;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_TextInputListener_h
diff --git a/dom/html/TextTrackManager.cpp b/dom/html/TextTrackManager.cpp
new file mode 100644
index 0000000000..7aacb5bf73
--- /dev/null
+++ b/dom/html/TextTrackManager.cpp
@@ -0,0 +1,874 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/TextTrackManager.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/HTMLTrackElement.h"
+#include "mozilla/dom/HTMLVideoElement.h"
+#include "mozilla/dom/TextTrack.h"
+#include "mozilla/dom/TextTrackCue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIFrame.h"
+#include "nsIWebVTTParserWrapper.h"
+#include "nsVariant.h"
+#include "nsVideoFrame.h"
+
+mozilla::LazyLogModule gTextTrackLog("WebVTT");
+
+#define WEBVTT_LOG(msg, ...) \
+ MOZ_LOG(gTextTrackLog, LogLevel::Debug, \
+ ("TextTrackManager=%p, " msg, this, ##__VA_ARGS__))
+#define WEBVTT_LOGV(msg, ...) \
+ MOZ_LOG(gTextTrackLog, LogLevel::Verbose, \
+ ("TextTrackManager=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+NS_IMPL_ISUPPORTS(TextTrackManager::ShutdownObserverProxy, nsIObserver);
+
+void TextTrackManager::ShutdownObserverProxy::Unregister() {
+ nsContentUtils::UnregisterShutdownObserver(this);
+ mManager = nullptr;
+}
+
+CompareTextTracks::CompareTextTracks(HTMLMediaElement* aMediaElement) {
+ mMediaElement = aMediaElement;
+}
+
+Maybe<uint32_t> CompareTextTracks::TrackChildPosition(
+ TextTrack* aTextTrack) const {
+ MOZ_DIAGNOSTIC_ASSERT(aTextTrack);
+ HTMLTrackElement* trackElement = aTextTrack->GetTrackElement();
+ if (!trackElement) {
+ return Nothing();
+ }
+ return mMediaElement->ComputeIndexOf(trackElement);
+}
+
+bool CompareTextTracks::Equals(TextTrack* aOne, TextTrack* aTwo) const {
+ // Two tracks can never be equal. If they have corresponding TrackElements
+ // they would need to occupy the same tree position (impossible) and in the
+ // case of tracks coming from AddTextTrack source we put the newest at the
+ // last position, so they won't be equal as well.
+ return false;
+}
+
+bool CompareTextTracks::LessThan(TextTrack* aOne, TextTrack* aTwo) const {
+ // Protect against nullptr TextTrack objects; treat them as
+ // sorting toward the end.
+ if (!aOne) {
+ return false;
+ }
+ if (!aTwo) {
+ return true;
+ }
+ TextTrackSource sourceOne = aOne->GetTextTrackSource();
+ TextTrackSource sourceTwo = aTwo->GetTextTrackSource();
+ if (sourceOne != sourceTwo) {
+ return sourceOne == TextTrackSource::Track ||
+ (sourceOne == TextTrackSource::AddTextTrack &&
+ sourceTwo == TextTrackSource::MediaResourceSpecific);
+ }
+ switch (sourceOne) {
+ case TextTrackSource::Track: {
+ Maybe<uint32_t> positionOne = TrackChildPosition(aOne);
+ Maybe<uint32_t> positionTwo = TrackChildPosition(aTwo);
+ // If either position one or positiontwo are Nothing then something has
+ // gone wrong. In this case we should just put them at the back of the
+ // list.
+ return positionOne.isSome() && positionTwo.isSome() &&
+ *positionOne < *positionTwo;
+ }
+ case TextTrackSource::AddTextTrack:
+ // For AddTextTrack sources the tracks will already be in the correct
+ // relative order in the source array. Assume we're called in iteration
+ // order and can therefore always report aOne < aTwo to maintain the
+ // original temporal ordering.
+ return true;
+ case TextTrackSource::MediaResourceSpecific:
+ // No rules for Media Resource Specific tracks yet.
+ break;
+ }
+ return true;
+}
+
+NS_IMPL_CYCLE_COLLECTION(TextTrackManager, mMediaElement, mTextTracks,
+ mPendingTextTracks, mNewCues)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackManager)
+ NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TextTrackManager)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TextTrackManager)
+
+StaticRefPtr<nsIWebVTTParserWrapper> TextTrackManager::sParserWrapper;
+
+TextTrackManager::TextTrackManager(HTMLMediaElement* aMediaElement)
+ : mMediaElement(aMediaElement),
+ mHasSeeked(false),
+ mLastTimeMarchesOnCalled(media::TimeUnit::Zero()),
+ mTimeMarchesOnDispatched(false),
+ mUpdateCueDisplayDispatched(false),
+ performedTrackSelection(false),
+ mShutdown(false) {
+ nsISupports* parentObject = mMediaElement->OwnerDoc()->GetParentObject();
+
+ NS_ENSURE_TRUE_VOID(parentObject);
+ WEBVTT_LOG("Create TextTrackManager");
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject);
+ mNewCues = new TextTrackCueList(window);
+ mTextTracks = new TextTrackList(window, this);
+ mPendingTextTracks = new TextTrackList(window, this);
+
+ if (!sParserWrapper) {
+ nsCOMPtr<nsIWebVTTParserWrapper> parserWrapper =
+ do_CreateInstance(NS_WEBVTTPARSERWRAPPER_CONTRACTID);
+ MOZ_ASSERT(parserWrapper, "Can't create nsIWebVTTParserWrapper");
+ sParserWrapper = parserWrapper;
+ ClearOnShutdown(&sParserWrapper);
+ }
+ mShutdownProxy = new ShutdownObserverProxy(this);
+}
+
+TextTrackManager::~TextTrackManager() {
+ WEBVTT_LOG("~TextTrackManager");
+ mShutdownProxy->Unregister();
+}
+
+TextTrackList* TextTrackManager::GetTextTracks() const { return mTextTracks; }
+
+already_AddRefed<TextTrack> TextTrackManager::AddTextTrack(
+ TextTrackKind aKind, const nsAString& aLabel, const nsAString& aLanguage,
+ TextTrackMode aMode, TextTrackReadyState aReadyState,
+ TextTrackSource aTextTrackSource) {
+ if (!mMediaElement || !mTextTracks) {
+ return nullptr;
+ }
+ RefPtr<TextTrack> track = mTextTracks->AddTextTrack(
+ aKind, aLabel, aLanguage, aMode, aReadyState, aTextTrackSource,
+ CompareTextTracks(mMediaElement));
+ WEBVTT_LOG("AddTextTrack %p kind %" PRIu32 " Label %s Language %s",
+ track.get(), static_cast<uint32_t>(aKind),
+ NS_ConvertUTF16toUTF8(aLabel).get(),
+ NS_ConvertUTF16toUTF8(aLanguage).get());
+ AddCues(track);
+
+ if (aTextTrackSource == TextTrackSource::Track) {
+ RefPtr<nsIRunnable> task = NewRunnableMethod(
+ "dom::TextTrackManager::HonorUserPreferencesForTrackSelection", this,
+ &TextTrackManager::HonorUserPreferencesForTrackSelection);
+ NS_DispatchToMainThread(task.forget());
+ }
+
+ return track.forget();
+}
+
+void TextTrackManager::AddTextTrack(TextTrack* aTextTrack) {
+ if (!mMediaElement || !mTextTracks) {
+ return;
+ }
+ WEBVTT_LOG("AddTextTrack TextTrack %p", aTextTrack);
+ mTextTracks->AddTextTrack(aTextTrack, CompareTextTracks(mMediaElement));
+ AddCues(aTextTrack);
+
+ if (aTextTrack->GetTextTrackSource() == TextTrackSource::Track) {
+ RefPtr<nsIRunnable> task = NewRunnableMethod(
+ "dom::TextTrackManager::HonorUserPreferencesForTrackSelection", this,
+ &TextTrackManager::HonorUserPreferencesForTrackSelection);
+ NS_DispatchToMainThread(task.forget());
+ }
+}
+
+void TextTrackManager::AddCues(TextTrack* aTextTrack) {
+ if (!mNewCues) {
+ WEBVTT_LOG("AddCues mNewCues is null");
+ return;
+ }
+
+ TextTrackCueList* cueList = aTextTrack->GetCues();
+ if (cueList) {
+ bool dummy;
+ WEBVTT_LOGV("AddCues, CuesNum=%d", cueList->Length());
+ for (uint32_t i = 0; i < cueList->Length(); ++i) {
+ mNewCues->AddCue(*cueList->IndexedGetter(i, dummy));
+ }
+ MaybeRunTimeMarchesOn();
+ }
+}
+
+void TextTrackManager::RemoveTextTrack(TextTrack* aTextTrack,
+ bool aPendingListOnly) {
+ if (!mPendingTextTracks || !mTextTracks) {
+ return;
+ }
+
+ WEBVTT_LOG("RemoveTextTrack TextTrack %p", aTextTrack);
+ mPendingTextTracks->RemoveTextTrack(aTextTrack);
+ if (aPendingListOnly) {
+ return;
+ }
+
+ mTextTracks->RemoveTextTrack(aTextTrack);
+ // Remove the cues in mNewCues belong to aTextTrack.
+ TextTrackCueList* removeCueList = aTextTrack->GetCues();
+ if (removeCueList) {
+ WEBVTT_LOGV("RemoveTextTrack removeCuesNum=%d", removeCueList->Length());
+ for (uint32_t i = 0; i < removeCueList->Length(); ++i) {
+ mNewCues->RemoveCue(*((*removeCueList)[i]));
+ }
+ MaybeRunTimeMarchesOn();
+ }
+}
+
+void TextTrackManager::DidSeek() {
+ WEBVTT_LOG("DidSeek");
+ mHasSeeked = true;
+}
+
+void TextTrackManager::UpdateCueDisplay() {
+ WEBVTT_LOG("UpdateCueDisplay");
+ mUpdateCueDisplayDispatched = false;
+
+ if (!mMediaElement || !mTextTracks || IsShutdown()) {
+ WEBVTT_LOG("Abort UpdateCueDisplay.");
+ return;
+ }
+
+ nsIFrame* frame = mMediaElement->GetPrimaryFrame();
+ nsVideoFrame* videoFrame = do_QueryFrame(frame);
+ if (!videoFrame) {
+ WEBVTT_LOG("Abort UpdateCueDisplay, because of no video frame.");
+ return;
+ }
+
+ nsCOMPtr<nsIContent> overlay = videoFrame->GetCaptionOverlay();
+ if (!overlay) {
+ WEBVTT_LOG("Abort UpdateCueDisplay, because of no overlay.");
+ return;
+ }
+
+ RefPtr<nsPIDOMWindowInner> window =
+ mMediaElement->OwnerDoc()->GetInnerWindow();
+ if (!window) {
+ WEBVTT_LOG("Abort UpdateCueDisplay, because of no window.");
+ }
+
+ nsTArray<RefPtr<TextTrackCue>> showingCues;
+ mTextTracks->GetShowingCues(showingCues);
+
+ WEBVTT_LOG("UpdateCueDisplay, processCues, showingCuesNum=%zu",
+ showingCues.Length());
+ RefPtr<nsVariantCC> jsCues = new nsVariantCC();
+ jsCues->SetAsArray(nsIDataType::VTYPE_INTERFACE, &NS_GET_IID(EventTarget),
+ showingCues.Length(),
+ static_cast<void*>(showingCues.Elements()));
+ nsCOMPtr<nsIContent> controls = videoFrame->GetVideoControls();
+
+ nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+ "TextTrackManager::UpdateCueDisplay",
+ [window, jsCues, overlay, controls]() {
+ if (sParserWrapper) {
+ sParserWrapper->ProcessCues(window, jsCues, overlay, controls);
+ }
+ }));
+}
+
+void TextTrackManager::NotifyCueAdded(TextTrackCue& aCue) {
+ WEBVTT_LOG("NotifyCueAdded, cue=%p", &aCue);
+ if (mNewCues) {
+ mNewCues->AddCue(aCue);
+ }
+ MaybeRunTimeMarchesOn();
+}
+
+void TextTrackManager::NotifyCueRemoved(TextTrackCue& aCue) {
+ WEBVTT_LOG("NotifyCueRemoved, cue=%p", &aCue);
+ if (mNewCues) {
+ mNewCues->RemoveCue(aCue);
+ }
+ MaybeRunTimeMarchesOn();
+ DispatchUpdateCueDisplay();
+}
+
+void TextTrackManager::PopulatePendingList() {
+ if (!mTextTracks || !mPendingTextTracks || !mMediaElement) {
+ return;
+ }
+ uint32_t len = mTextTracks->Length();
+ bool dummy;
+ for (uint32_t index = 0; index < len; ++index) {
+ TextTrack* ttrack = mTextTracks->IndexedGetter(index, dummy);
+ if (ttrack && ttrack->Mode() != TextTrackMode::Disabled &&
+ ttrack->ReadyState() == TextTrackReadyState::Loading) {
+ mPendingTextTracks->AddTextTrack(ttrack,
+ CompareTextTracks(mMediaElement));
+ }
+ }
+}
+
+void TextTrackManager::AddListeners() {
+ if (mMediaElement) {
+ mMediaElement->AddEventListener(u"resizecaption"_ns, this, false, false);
+ mMediaElement->AddEventListener(u"resizevideocontrols"_ns, this, false,
+ false);
+ mMediaElement->AddEventListener(u"seeked"_ns, this, false, false);
+ mMediaElement->AddEventListener(u"controlbarchange"_ns, this, false, true);
+ }
+}
+
+void TextTrackManager::HonorUserPreferencesForTrackSelection() {
+ if (performedTrackSelection || !mTextTracks) {
+ return;
+ }
+ WEBVTT_LOG("HonorUserPreferencesForTrackSelection");
+ TextTrackKind ttKinds[] = {TextTrackKind::Captions, TextTrackKind::Subtitles};
+
+ // Steps 1 - 3: Perform automatic track selection for different TextTrack
+ // Kinds.
+ PerformTrackSelection(ttKinds, ArrayLength(ttKinds));
+ PerformTrackSelection(TextTrackKind::Descriptions);
+ PerformTrackSelection(TextTrackKind::Chapters);
+
+ // Step 4: Set all TextTracks with a kind of metadata that are disabled
+ // to hidden.
+ for (uint32_t i = 0; i < mTextTracks->Length(); i++) {
+ TextTrack* track = (*mTextTracks)[i];
+ if (track->Kind() == TextTrackKind::Metadata && TrackIsDefault(track) &&
+ track->Mode() == TextTrackMode::Disabled) {
+ track->SetMode(TextTrackMode::Hidden);
+ }
+ }
+
+ performedTrackSelection = true;
+}
+
+bool TextTrackManager::TrackIsDefault(TextTrack* aTextTrack) {
+ HTMLTrackElement* trackElement = aTextTrack->GetTrackElement();
+ if (!trackElement) {
+ return false;
+ }
+ return trackElement->Default();
+}
+
+void TextTrackManager::PerformTrackSelection(TextTrackKind aTextTrackKind) {
+ TextTrackKind ttKinds[] = {aTextTrackKind};
+ PerformTrackSelection(ttKinds, ArrayLength(ttKinds));
+}
+
+void TextTrackManager::PerformTrackSelection(TextTrackKind aTextTrackKinds[],
+ uint32_t size) {
+ nsTArray<TextTrack*> candidates;
+ GetTextTracksOfKinds(aTextTrackKinds, size, candidates);
+
+ // Step 3: If any TextTracks in candidates are showing then abort these steps.
+ for (uint32_t i = 0; i < candidates.Length(); i++) {
+ if (candidates[i]->Mode() == TextTrackMode::Showing) {
+ WEBVTT_LOGV("PerformTrackSelection Showing return kind %d",
+ static_cast<int>(candidates[i]->Kind()));
+ return;
+ }
+ }
+
+ // Step 4: Honor user preferences for track selection, otherwise, set the
+ // first TextTrack in candidates with a default attribute to showing.
+ // TODO: Bug 981691 - Honor user preferences for text track selection.
+ for (uint32_t i = 0; i < candidates.Length(); i++) {
+ if (TrackIsDefault(candidates[i]) &&
+ candidates[i]->Mode() == TextTrackMode::Disabled) {
+ candidates[i]->SetMode(TextTrackMode::Showing);
+ WEBVTT_LOGV("PerformTrackSelection set Showing kind %d",
+ static_cast<int>(candidates[i]->Kind()));
+ return;
+ }
+ }
+}
+
+void TextTrackManager::GetTextTracksOfKinds(TextTrackKind aTextTrackKinds[],
+ uint32_t size,
+ nsTArray<TextTrack*>& aTextTracks) {
+ for (uint32_t i = 0; i < size; i++) {
+ GetTextTracksOfKind(aTextTrackKinds[i], aTextTracks);
+ }
+}
+
+void TextTrackManager::GetTextTracksOfKind(TextTrackKind aTextTrackKind,
+ nsTArray<TextTrack*>& aTextTracks) {
+ if (!mTextTracks) {
+ return;
+ }
+ for (uint32_t i = 0; i < mTextTracks->Length(); i++) {
+ TextTrack* textTrack = (*mTextTracks)[i];
+ if (textTrack->Kind() == aTextTrackKind) {
+ aTextTracks.AppendElement(textTrack);
+ }
+ }
+}
+
+NS_IMETHODIMP
+TextTrackManager::HandleEvent(Event* aEvent) {
+ if (!mTextTracks) {
+ return NS_OK;
+ }
+
+ nsAutoString type;
+ aEvent->GetType(type);
+ WEBVTT_LOG("Handle event %s", NS_ConvertUTF16toUTF8(type).get());
+
+ const bool setDirty = type.EqualsLiteral("seeked") ||
+ type.EqualsLiteral("resizecaption") ||
+ type.EqualsLiteral("resizevideocontrols");
+ const bool updateDisplay = type.EqualsLiteral("controlbarchange") ||
+ type.EqualsLiteral("resizecaption");
+
+ if (setDirty) {
+ for (uint32_t i = 0; i < mTextTracks->Length(); i++) {
+ ((*mTextTracks)[i])->SetCuesDirty();
+ }
+ }
+ if (updateDisplay) {
+ UpdateCueDisplay();
+ }
+
+ return NS_OK;
+}
+
+class SimpleTextTrackEvent : public Runnable {
+ public:
+ friend class CompareSimpleTextTrackEvents;
+ SimpleTextTrackEvent(const nsAString& aEventName, double aTime,
+ TextTrack* aTrack, TextTrackCue* aCue)
+ : Runnable("dom::SimpleTextTrackEvent"),
+ mName(aEventName),
+ mTime(aTime),
+ mTrack(aTrack),
+ mCue(aCue) {}
+
+ NS_IMETHOD Run() override {
+ WEBVTT_LOGV("SimpleTextTrackEvent cue %p mName %s mTime %lf", mCue.get(),
+ NS_ConvertUTF16toUTF8(mName).get(), mTime);
+ mCue->DispatchTrustedEvent(mName);
+ return NS_OK;
+ }
+
+ void Dispatch() {
+ if (nsCOMPtr<nsIGlobalObject> global = mCue->GetOwnerGlobal()) {
+ global->Dispatch(do_AddRef(this));
+ } else {
+ NS_DispatchToMainThread(do_AddRef(this));
+ }
+ }
+
+ private:
+ nsString mName;
+ double mTime;
+ TextTrack* mTrack;
+ RefPtr<TextTrackCue> mCue;
+};
+
+class CompareSimpleTextTrackEvents {
+ private:
+ Maybe<uint32_t> TrackChildPosition(SimpleTextTrackEvent* aEvent) const {
+ if (aEvent->mTrack) {
+ HTMLTrackElement* trackElement = aEvent->mTrack->GetTrackElement();
+ if (trackElement) {
+ return mMediaElement->ComputeIndexOf(trackElement);
+ }
+ }
+ return Nothing();
+ }
+ HTMLMediaElement* mMediaElement;
+
+ public:
+ explicit CompareSimpleTextTrackEvents(HTMLMediaElement* aMediaElement) {
+ mMediaElement = aMediaElement;
+ }
+
+ bool Equals(SimpleTextTrackEvent* aOne, SimpleTextTrackEvent* aTwo) const {
+ return false;
+ }
+
+ bool LessThan(SimpleTextTrackEvent* aOne, SimpleTextTrackEvent* aTwo) const {
+ // TimeMarchesOn step 13.1.
+ if (aOne->mTime < aTwo->mTime) {
+ return true;
+ }
+ if (aOne->mTime > aTwo->mTime) {
+ return false;
+ }
+
+ // TimeMarchesOn step 13.2 text track cue order.
+ // TextTrack position in TextTrackList
+ TextTrack* t1 = aOne->mTrack;
+ TextTrack* t2 = aTwo->mTrack;
+ MOZ_ASSERT(t1, "CompareSimpleTextTrackEvents t1 is null");
+ MOZ_ASSERT(t2, "CompareSimpleTextTrackEvents t2 is null");
+ if (t1 != t2) {
+ TextTrackList* tList = t1->GetTextTrackList();
+ MOZ_ASSERT(tList, "CompareSimpleTextTrackEvents tList is null");
+ nsTArray<RefPtr<TextTrack>>& textTracks = tList->GetTextTrackArray();
+ auto index1 = textTracks.IndexOf(t1);
+ auto index2 = textTracks.IndexOf(t2);
+ if (index1 < index2) {
+ return true;
+ }
+ if (index1 > index2) {
+ return false;
+ }
+ }
+
+ MOZ_ASSERT(t1 == t2, "CompareSimpleTextTrackEvents t1 != t2");
+ // c1 and c2 are both belongs to t1.
+ TextTrackCue* c1 = aOne->mCue;
+ TextTrackCue* c2 = aTwo->mCue;
+ if (c1 != c2) {
+ if (c1->StartTime() < c2->StartTime()) {
+ return true;
+ }
+ if (c1->StartTime() > c2->StartTime()) {
+ return false;
+ }
+ if (c1->EndTime() < c2->EndTime()) {
+ return true;
+ }
+ if (c1->EndTime() > c2->EndTime()) {
+ return false;
+ }
+
+ TextTrackCueList* cueList = t1->GetCues();
+ MOZ_ASSERT(cueList);
+ nsTArray<RefPtr<TextTrackCue>>& cues = cueList->GetCuesArray();
+ auto index1 = cues.IndexOf(c1);
+ auto index2 = cues.IndexOf(c2);
+ if (index1 < index2) {
+ return true;
+ }
+ if (index1 > index2) {
+ return false;
+ }
+ }
+
+ // TimeMarchesOn step 13.3.
+ if (aOne->mName.EqualsLiteral("enter") ||
+ aTwo->mName.EqualsLiteral("exit")) {
+ return true;
+ }
+ return false;
+ }
+};
+
+class TextTrackListInternal {
+ public:
+ void AddTextTrack(TextTrack* aTextTrack,
+ const CompareTextTracks& aCompareTT) {
+ if (!mTextTracks.Contains(aTextTrack)) {
+ mTextTracks.InsertElementSorted(aTextTrack, aCompareTT);
+ }
+ }
+ uint32_t Length() const { return mTextTracks.Length(); }
+ TextTrack* operator[](uint32_t aIndex) {
+ return mTextTracks.SafeElementAt(aIndex, nullptr);
+ }
+
+ private:
+ nsTArray<RefPtr<TextTrack>> mTextTracks;
+};
+
+void TextTrackManager::DispatchUpdateCueDisplay() {
+ if (!mUpdateCueDisplayDispatched && !IsShutdown()) {
+ WEBVTT_LOG("DispatchUpdateCueDisplay");
+ if (nsPIDOMWindowInner* win = mMediaElement->OwnerDoc()->GetInnerWindow()) {
+ nsGlobalWindowInner::Cast(win)->Dispatch(
+ NewRunnableMethod("dom::TextTrackManager::UpdateCueDisplay", this,
+ &TextTrackManager::UpdateCueDisplay));
+ mUpdateCueDisplayDispatched = true;
+ }
+ }
+}
+
+void TextTrackManager::DispatchTimeMarchesOn() {
+ // Run the algorithm if no previous instance is still running, otherwise
+ // enqueue the current playback position and whether only that changed
+ // through its usual monotonic increase during normal playback; current
+ // executing call upon completion will check queue for further 'work'.
+ if (!mTimeMarchesOnDispatched && !IsShutdown()) {
+ WEBVTT_LOG("DispatchTimeMarchesOn");
+ if (nsPIDOMWindowInner* win = mMediaElement->OwnerDoc()->GetInnerWindow()) {
+ nsGlobalWindowInner::Cast(win)->Dispatch(
+ NewRunnableMethod("dom::TextTrackManager::TimeMarchesOn", this,
+ &TextTrackManager::TimeMarchesOn));
+ mTimeMarchesOnDispatched = true;
+ }
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/embedded-content.html#time-marches-on
+void TextTrackManager::TimeMarchesOn() {
+ NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+ mTimeMarchesOnDispatched = false;
+
+ CycleCollectedJSContext* context = CycleCollectedJSContext::Get();
+ if (context && context->IsInStableOrMetaStableState()) {
+ // FireTimeUpdate can be called while at stable state following a
+ // current position change which triggered a state watcher in MediaDecoder
+ // (see bug 1443429).
+ // TimeMarchesOn() will modify JS attributes which is forbidden while in
+ // stable state. So we dispatch a task to perform such operation later
+ // instead.
+ DispatchTimeMarchesOn();
+ return;
+ }
+ WEBVTT_LOG("TimeMarchesOn");
+
+ // Early return if we don't have any TextTracks or shutting down.
+ if (!mTextTracks || mTextTracks->Length() == 0 || IsShutdown() ||
+ !mMediaElement) {
+ return;
+ }
+
+ if (mMediaElement->ReadyState() == HTMLMediaElement_Binding::HAVE_NOTHING) {
+ WEBVTT_LOG(
+ "TimeMarchesOn return because media doesn't contain any data yet");
+ return;
+ }
+
+ if (mMediaElement->Seeking()) {
+ WEBVTT_LOG("TimeMarchesOn return during seeking");
+ return;
+ }
+
+ // Step 1, 2.
+ nsISupports* parentObject = mMediaElement->OwnerDoc()->GetParentObject();
+ if (NS_WARN_IF(!parentObject)) {
+ return;
+ }
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject);
+ RefPtr<TextTrackCueList> currentCues = new TextTrackCueList(window);
+ RefPtr<TextTrackCueList> otherCues = new TextTrackCueList(window);
+
+ // Step 3.
+ auto currentPlaybackTime =
+ media::TimeUnit::FromSeconds(mMediaElement->CurrentTime());
+ bool hasNormalPlayback = !mHasSeeked;
+ mHasSeeked = false;
+ WEBVTT_LOG(
+ "TimeMarchesOn mLastTimeMarchesOnCalled %lf currentPlaybackTime %lf "
+ "hasNormalPlayback %d",
+ mLastTimeMarchesOnCalled.ToSeconds(), currentPlaybackTime.ToSeconds(),
+ hasNormalPlayback);
+
+ // The reason we collect other cues is (1) to change active cues to inactive,
+ // (2) find missing cues, so we actually no need to process all cues. We just
+ // need to handle cues which are in the time interval [lastTime:currentTime]
+ // or [currentTime:lastTime] (seeking forward). That can help us to reduce the
+ // size of other cues, which can improve execution time.
+ auto start = std::min(mLastTimeMarchesOnCalled, currentPlaybackTime);
+ auto end = std::max(mLastTimeMarchesOnCalled, currentPlaybackTime);
+ media::TimeInterval interval(start, end);
+ WEBVTT_LOGV("TimeMarchesOn Time interval [%f:%f]", start.ToSeconds(),
+ end.ToSeconds());
+ for (uint32_t idx = 0; idx < mTextTracks->Length(); ++idx) {
+ TextTrack* track = (*mTextTracks)[idx];
+ if (track) {
+ track->GetCurrentCuesAndOtherCues(currentCues, otherCues, interval);
+ }
+ }
+
+ // Step 4.
+ RefPtr<TextTrackCueList> missedCues = new TextTrackCueList(window);
+ if (hasNormalPlayback) {
+ for (uint32_t i = 0; i < otherCues->Length(); ++i) {
+ TextTrackCue* cue = (*otherCues)[i];
+ if (cue->StartTime() >= mLastTimeMarchesOnCalled.ToSeconds() &&
+ cue->EndTime() <= currentPlaybackTime.ToSeconds()) {
+ missedCues->AddCue(*cue);
+ }
+ }
+ }
+
+ WEBVTT_LOGV("TimeMarchesOn currentCues %d", currentCues->Length());
+ WEBVTT_LOGV("TimeMarchesOn otherCues %d", otherCues->Length());
+ WEBVTT_LOGV("TimeMarchesOn missedCues %d", missedCues->Length());
+ // Step 5. Empty now.
+ // TODO: Step 6: fire timeupdate?
+
+ // Step 7. Abort steps if condition 1, 2, 3 are satisfied.
+ // 1. All of the cues in current cues have their active flag set.
+ // 2. None of the cues in other cues have their active flag set.
+ // 3. Missed cues is empty.
+ bool c1 = true;
+ for (uint32_t i = 0; i < currentCues->Length(); ++i) {
+ if (!(*currentCues)[i]->GetActive()) {
+ c1 = false;
+ break;
+ }
+ }
+ bool c2 = true;
+ for (uint32_t i = 0; i < otherCues->Length(); ++i) {
+ if ((*otherCues)[i]->GetActive()) {
+ c2 = false;
+ break;
+ }
+ }
+ bool c3 = (missedCues->Length() == 0);
+ if (c1 && c2 && c3) {
+ mLastTimeMarchesOnCalled = currentPlaybackTime;
+ WEBVTT_LOG("TimeMarchesOn step 7 return, mLastTimeMarchesOnCalled %lf",
+ mLastTimeMarchesOnCalled.ToSeconds());
+ return;
+ }
+
+ // Step 8. Respect PauseOnExit flag if not seek.
+ if (hasNormalPlayback) {
+ for (uint32_t i = 0; i < otherCues->Length(); ++i) {
+ TextTrackCue* cue = (*otherCues)[i];
+ if (cue && cue->PauseOnExit() && cue->GetActive()) {
+ WEBVTT_LOG("TimeMarchesOn pause the MediaElement");
+ mMediaElement->Pause();
+ break;
+ }
+ }
+ for (uint32_t i = 0; i < missedCues->Length(); ++i) {
+ TextTrackCue* cue = (*missedCues)[i];
+ if (cue && cue->PauseOnExit()) {
+ WEBVTT_LOG("TimeMarchesOn pause the MediaElement");
+ mMediaElement->Pause();
+ break;
+ }
+ }
+ }
+
+ // Step 15.
+ // Sort text tracks in the same order as the text tracks appear
+ // in the media element's list of text tracks, and remove
+ // duplicates.
+ TextTrackListInternal affectedTracks;
+ // Step 13, 14.
+ nsTArray<RefPtr<SimpleTextTrackEvent>> eventList;
+ // Step 9, 10.
+ // For each text track cue in missed cues, prepare an event named
+ // enter for the TextTrackCue object with the cue start time.
+ for (uint32_t i = 0; i < missedCues->Length(); ++i) {
+ TextTrackCue* cue = (*missedCues)[i];
+ if (cue) {
+ WEBVTT_LOG("Prepare 'enter' event for cue %p [%f, %f] in missing cues",
+ cue, cue->StartTime(), cue->EndTime());
+ SimpleTextTrackEvent* event = new SimpleTextTrackEvent(
+ u"enter"_ns, cue->StartTime(), cue->GetTrack(), cue);
+ eventList.InsertElementSorted(
+ event, CompareSimpleTextTrackEvents(mMediaElement));
+ affectedTracks.AddTextTrack(cue->GetTrack(),
+ CompareTextTracks(mMediaElement));
+ }
+ }
+
+ // Step 11, 17.
+ for (uint32_t i = 0; i < otherCues->Length(); ++i) {
+ TextTrackCue* cue = (*otherCues)[i];
+ if (cue->GetActive() || missedCues->IsCueExist(cue)) {
+ double time =
+ cue->StartTime() > cue->EndTime() ? cue->StartTime() : cue->EndTime();
+ WEBVTT_LOG("Prepare 'exit' event for cue %p [%f, %f] in other cues", cue,
+ cue->StartTime(), cue->EndTime());
+ SimpleTextTrackEvent* event =
+ new SimpleTextTrackEvent(u"exit"_ns, time, cue->GetTrack(), cue);
+ eventList.InsertElementSorted(
+ event, CompareSimpleTextTrackEvents(mMediaElement));
+ affectedTracks.AddTextTrack(cue->GetTrack(),
+ CompareTextTracks(mMediaElement));
+ }
+ cue->SetActive(false);
+ }
+
+ // Step 12, 17.
+ for (uint32_t i = 0; i < currentCues->Length(); ++i) {
+ TextTrackCue* cue = (*currentCues)[i];
+ if (!cue->GetActive()) {
+ WEBVTT_LOG("Prepare 'enter' event for cue %p [%f, %f] in current cues",
+ cue, cue->StartTime(), cue->EndTime());
+ SimpleTextTrackEvent* event = new SimpleTextTrackEvent(
+ u"enter"_ns, cue->StartTime(), cue->GetTrack(), cue);
+ eventList.InsertElementSorted(
+ event, CompareSimpleTextTrackEvents(mMediaElement));
+ affectedTracks.AddTextTrack(cue->GetTrack(),
+ CompareTextTracks(mMediaElement));
+ }
+ cue->SetActive(true);
+ }
+
+ // Fire the eventList
+ for (uint32_t i = 0; i < eventList.Length(); ++i) {
+ eventList[i]->Dispatch();
+ }
+
+ // Step 16.
+ for (uint32_t i = 0; i < affectedTracks.Length(); ++i) {
+ TextTrack* ttrack = affectedTracks[i];
+ if (ttrack) {
+ ttrack->DispatchAsyncTrustedEvent(u"cuechange"_ns);
+ HTMLTrackElement* trackElement = ttrack->GetTrackElement();
+ if (trackElement) {
+ trackElement->DispatchTrackRunnable(u"cuechange"_ns);
+ }
+ }
+ }
+
+ mLastTimeMarchesOnCalled = currentPlaybackTime;
+
+ // Step 18.
+ UpdateCueDisplay();
+}
+
+void TextTrackManager::NotifyCueUpdated(TextTrackCue* aCue) {
+ // TODO: Add/Reorder the cue to mNewCues if we have some optimization?
+ WEBVTT_LOG("NotifyCueUpdated, cue=%p", aCue);
+ MaybeRunTimeMarchesOn();
+ // For the case "Texttrack.mode = hidden/showing", if the mode
+ // changing between showing and hidden, TimeMarchesOn
+ // doesn't render the cue. Call DispatchUpdateCueDisplay() explicitly.
+ DispatchUpdateCueDisplay();
+}
+
+void TextTrackManager::NotifyReset() {
+ // https://html.spec.whatwg.org/multipage/media.html#text-track-cue-active-flag
+ // This will unset all cues' active flag and update the cue display.
+ WEBVTT_LOG("NotifyReset");
+ mLastTimeMarchesOnCalled = media::TimeUnit::Zero();
+ for (uint32_t idx = 0; idx < mTextTracks->Length(); ++idx) {
+ (*mTextTracks)[idx]->SetCuesInactive();
+ }
+ UpdateCueDisplay();
+}
+
+bool TextTrackManager::IsLoaded() {
+ return mTextTracks ? mTextTracks->AreTextTracksLoaded() : true;
+}
+
+bool TextTrackManager::IsShutdown() const {
+ return (mShutdown || !sParserWrapper);
+}
+
+void TextTrackManager::MaybeRunTimeMarchesOn() {
+ MOZ_ASSERT(mMediaElement);
+ // According to spec, we should check media element's show poster flag before
+ // running `TimeMarchesOn` in following situations, (1) add cue (2) remove cue
+ // (3) cue's start time changes (4) cues's end time changes
+ // https://html.spec.whatwg.org/multipage/media.html#playing-the-media-resource:time-marches-on
+ // https://html.spec.whatwg.org/multipage/media.html#text-track-api:time-marches-on
+ if (mMediaElement->GetShowPosterFlag()) {
+ return;
+ }
+ TimeMarchesOn();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/TextTrackManager.h b/dom/html/TextTrackManager.h
new file mode 100644
index 0000000000..6ce19963d5
--- /dev/null
+++ b/dom/html/TextTrackManager.h
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_TextTrackManager_h
+#define mozilla_dom_TextTrackManager_h
+
+#include "mozilla/dom/TextTrack.h"
+#include "mozilla/dom/TextTrackList.h"
+#include "mozilla/dom/TextTrackCueList.h"
+#include "mozilla/StaticPtr.h"
+#include "nsContentUtils.h"
+#include "nsIDOMEventListener.h"
+#include "TimeUnits.h"
+
+class nsIWebVTTParserWrapper;
+
+namespace mozilla {
+template <typename T>
+class Maybe;
+namespace dom {
+
+class HTMLMediaElement;
+
+class CompareTextTracks {
+ private:
+ HTMLMediaElement* mMediaElement;
+ Maybe<uint32_t> TrackChildPosition(TextTrack* aTrack) const;
+
+ public:
+ explicit CompareTextTracks(HTMLMediaElement* aMediaElement);
+ bool Equals(TextTrack* aOne, TextTrack* aTwo) const;
+ bool LessThan(TextTrack* aOne, TextTrack* aTwo) const;
+};
+
+class TextTrack;
+class TextTrackCue;
+
+class TextTrackManager final : public nsIDOMEventListener {
+ ~TextTrackManager();
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(TextTrackManager)
+
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ explicit TextTrackManager(HTMLMediaElement* aMediaElement);
+
+ TextTrackList* GetTextTracks() const;
+ already_AddRefed<TextTrack> AddTextTrack(TextTrackKind aKind,
+ const nsAString& aLabel,
+ const nsAString& aLanguage,
+ TextTrackMode aMode,
+ TextTrackReadyState aReadyState,
+ TextTrackSource aTextTrackSource);
+ void AddTextTrack(TextTrack* aTextTrack);
+ void RemoveTextTrack(TextTrack* aTextTrack, bool aPendingListOnly);
+ void DidSeek();
+
+ void NotifyCueAdded(TextTrackCue& aCue);
+ void AddCues(TextTrack* aTextTrack);
+ void NotifyCueRemoved(TextTrackCue& aCue);
+ /**
+ * Overview of WebVTT cuetext and anonymous content setup.
+ *
+ * WebVTT nodes are the parsed version of WebVTT cuetext. WebVTT cuetext is
+ * the portion of a WebVTT cue that specifies what the caption will actually
+ * show up as on screen.
+ *
+ * WebVTT cuetext can contain markup that loosely relates to HTML markup. It
+ * can contain tags like <b>, <u>, <i>, <c>, <v>, <ruby>, <rt>, <lang>,
+ * including timestamp tags.
+ *
+ * When the caption is ready to be displayed the WebVTT nodes are converted
+ * over to anonymous DOM content. <i>, <u>, <b>, <ruby>, and <rt> all become
+ * HTMLElements of their corresponding HTML markup tags. <c> and <v> are
+ * converted to <span> tags. Timestamp tags are converted to XML processing
+ * instructions. Additionally, all cuetext tags support specifying of classes.
+ * This takes the form of <foo.class.subclass>. These classes are then parsed
+ * and set as the anonymous content's class attribute.
+ *
+ * Rules on constructing DOM objects from WebVTT nodes can be found here
+ * http://dev.w3.org/html5/webvtt/#webvtt-cue-text-dom-construction-rules.
+ * Current rules are taken from revision on April 15, 2013.
+ */
+
+ void PopulatePendingList();
+
+ void AddListeners();
+
+ // The HTMLMediaElement that this TextTrackManager manages the TextTracks of.
+ RefPtr<HTMLMediaElement> mMediaElement;
+
+ void DispatchTimeMarchesOn();
+ void TimeMarchesOn();
+ void DispatchUpdateCueDisplay();
+
+ void NotifyShutdown() { mShutdown = true; }
+
+ void NotifyCueUpdated(TextTrackCue* aCue);
+
+ void NotifyReset();
+
+ bool IsLoaded();
+
+ private:
+ /**
+ * Converts the TextTrackCue's cuetext into a tree of DOM objects
+ * and attaches it to a div on its owning TrackElement's
+ * MediaElement's caption overlay.
+ */
+ void UpdateCueDisplay();
+
+ // List of the TextTrackManager's owning HTMLMediaElement's TextTracks.
+ RefPtr<TextTrackList> mTextTracks;
+ // List of text track objects awaiting loading.
+ RefPtr<TextTrackList> mPendingTextTracks;
+ // List of newly introduced Text Track cues.
+
+ // Contain all cues for a MediaElement. Not sorted.
+ RefPtr<TextTrackCueList> mNewCues;
+
+ // True if the media player playback changed due to seeking prior to and
+ // during running the "Time Marches On" algorithm.
+ bool mHasSeeked;
+ // Playback position at the time of last "Time Marches On" call
+ media::TimeUnit mLastTimeMarchesOnCalled;
+
+ bool mTimeMarchesOnDispatched;
+ bool mUpdateCueDisplayDispatched;
+
+ static StaticRefPtr<nsIWebVTTParserWrapper> sParserWrapper;
+
+ bool performedTrackSelection;
+
+ // Runs the algorithm for performing automatic track selection.
+ void HonorUserPreferencesForTrackSelection();
+ // Performs track selection for a single TextTrackKind.
+ void PerformTrackSelection(TextTrackKind aTextTrackKind);
+ // Performs track selection for a set of TextTrackKinds, for example,
+ // 'subtitles' and 'captions' should be selected together.
+ void PerformTrackSelection(TextTrackKind aTextTrackKinds[], uint32_t size);
+ void GetTextTracksOfKinds(TextTrackKind aTextTrackKinds[], uint32_t size,
+ nsTArray<TextTrack*>& aTextTracks);
+ void GetTextTracksOfKind(TextTrackKind aTextTrackKind,
+ nsTArray<TextTrack*>& aTextTracks);
+ bool TrackIsDefault(TextTrack* aTextTrack);
+
+ bool IsShutdown() const;
+
+ // This function will check media element's show poster flag to decide whether
+ // we need to run `TimeMarchesOn`.
+ void MaybeRunTimeMarchesOn();
+
+ class ShutdownObserverProxy final : public nsIObserver {
+ NS_DECL_ISUPPORTS
+
+ public:
+ explicit ShutdownObserverProxy(TextTrackManager* aManager)
+ : mManager(aManager) {
+ nsContentUtils::RegisterShutdownObserver(this);
+ }
+
+ NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
+ if (mManager) {
+ mManager->NotifyShutdown();
+ }
+ Unregister();
+ }
+ return NS_OK;
+ }
+
+ void Unregister();
+
+ private:
+ ~ShutdownObserverProxy() = default;
+
+ TextTrackManager* mManager;
+ };
+
+ RefPtr<ShutdownObserverProxy> mShutdownProxy;
+ bool mShutdown;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_TextTrackManager_h
diff --git a/dom/html/TimeRanges.cpp b/dom/html/TimeRanges.cpp
new file mode 100644
index 0000000000..21f1f56baa
--- /dev/null
+++ b/dom/html/TimeRanges.cpp
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/TimeRanges.h"
+#include "mozilla/dom/TimeRangesBinding.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "TimeUnits.h"
+#include "nsError.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TimeRanges, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TimeRanges)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TimeRanges)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TimeRanges)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+TimeRanges::TimeRanges() : mParent(nullptr) {}
+
+TimeRanges::TimeRanges(nsISupports* aParent) : mParent(aParent) {}
+
+TimeRanges::TimeRanges(nsISupports* aParent,
+ const media::TimeIntervals& aTimeIntervals)
+ : TimeRanges(aParent) {
+ if (aTimeIntervals.IsInvalid()) {
+ return;
+ }
+ for (const media::TimeInterval& interval : aTimeIntervals) {
+ Add(interval.mStart.ToSeconds(), interval.mEnd.ToSeconds());
+ }
+}
+
+TimeRanges::TimeRanges(nsISupports* aParent,
+ const media::TimeRanges& aTimeRanges)
+ : TimeRanges(aParent) {
+ if (aTimeRanges.IsInvalid()) {
+ return;
+ }
+ for (const media::TimeRange& interval : aTimeRanges) {
+ Add(interval.mStart, interval.mEnd);
+ }
+}
+
+TimeRanges::TimeRanges(const media::TimeIntervals& aTimeIntervals)
+ : TimeRanges(nullptr, aTimeIntervals) {}
+
+TimeRanges::TimeRanges(const media::TimeRanges& aTimeRanges)
+ : TimeRanges(nullptr, aTimeRanges) {}
+
+media::TimeIntervals TimeRanges::ToTimeIntervals() const {
+ media::TimeIntervals t;
+ for (uint32_t i = 0; i < Length(); i++) {
+ t += media::TimeInterval(media::TimeUnit::FromSeconds(Start(i)),
+ media::TimeUnit::FromSeconds(End(i)));
+ }
+ return t;
+}
+
+TimeRanges::~TimeRanges() = default;
+
+double TimeRanges::Start(uint32_t aIndex, ErrorResult& aRv) const {
+ if (aIndex >= mRanges.Length()) {
+ aRv = NS_ERROR_DOM_INDEX_SIZE_ERR;
+ return 0;
+ }
+
+ return Start(aIndex);
+}
+
+double TimeRanges::End(uint32_t aIndex, ErrorResult& aRv) const {
+ if (aIndex >= mRanges.Length()) {
+ aRv = NS_ERROR_DOM_INDEX_SIZE_ERR;
+ return 0;
+ }
+
+ return End(aIndex);
+}
+
+void TimeRanges::Add(double aStart, double aEnd) {
+ if (aStart > aEnd) {
+ NS_WARNING("Can't add a range if the end is older that the start.");
+ return;
+ }
+ mRanges.AppendElement(TimeRange(aStart, aEnd));
+}
+
+double TimeRanges::GetStartTime() {
+ if (mRanges.IsEmpty()) {
+ return -1.0;
+ }
+ return mRanges[0].mStart;
+}
+
+double TimeRanges::GetEndTime() {
+ if (mRanges.IsEmpty()) {
+ return -1.0;
+ }
+ return mRanges[mRanges.Length() - 1].mEnd;
+}
+
+void TimeRanges::Normalize(double aTolerance) {
+ if (mRanges.Length() >= 2) {
+ AutoTArray<TimeRange, 4> normalized;
+
+ mRanges.Sort(CompareTimeRanges());
+
+ // This merges the intervals.
+ TimeRange current(mRanges[0]);
+ for (uint32_t i = 1; i < mRanges.Length(); i++) {
+ if (current.mStart <= mRanges[i].mStart &&
+ current.mEnd >= mRanges[i].mEnd) {
+ continue;
+ }
+ if (current.mEnd + aTolerance >= mRanges[i].mStart) {
+ current.mEnd = mRanges[i].mEnd;
+ } else {
+ normalized.AppendElement(current);
+ current = mRanges[i];
+ }
+ }
+
+ normalized.AppendElement(current);
+
+ mRanges = std::move(normalized);
+ }
+}
+
+void TimeRanges::Union(const TimeRanges* aOtherRanges, double aTolerance) {
+ mRanges.AppendElements(aOtherRanges->mRanges);
+ Normalize(aTolerance);
+}
+
+void TimeRanges::Intersection(const TimeRanges* aOtherRanges) {
+ AutoTArray<TimeRange, 4> intersection;
+
+ const nsTArray<TimeRange>& otherRanges = aOtherRanges->mRanges;
+ for (index_type i = 0, j = 0;
+ i < mRanges.Length() && j < otherRanges.Length();) {
+ double start = std::max(mRanges[i].mStart, otherRanges[j].mStart);
+ double end = std::min(mRanges[i].mEnd, otherRanges[j].mEnd);
+ if (start < end) {
+ intersection.AppendElement(TimeRange(start, end));
+ }
+ if (mRanges[i].mEnd < otherRanges[j].mEnd) {
+ i += 1;
+ } else {
+ j += 1;
+ }
+ }
+
+ mRanges = std::move(intersection);
+}
+
+TimeRanges::index_type TimeRanges::Find(double aTime,
+ double aTolerance /* = 0 */) {
+ for (index_type i = 0; i < mRanges.Length(); ++i) {
+ if (aTime < mRanges[i].mEnd && (aTime + aTolerance) >= mRanges[i].mStart) {
+ return i;
+ }
+ }
+ return NoIndex;
+}
+
+JSObject* TimeRanges::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return TimeRanges_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* TimeRanges::GetParentObject() const { return mParent; }
+
+void TimeRanges::Shift(double aOffset) {
+ for (index_type i = 0; i < mRanges.Length(); ++i) {
+ mRanges[i].mStart += aOffset;
+ mRanges[i].mEnd += aOffset;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/TimeRanges.h b/dom/html/TimeRanges.h
new file mode 100644
index 0000000000..b96e747c01
--- /dev/null
+++ b/dom/html/TimeRanges.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_TimeRanges_h_
+#define mozilla_dom_TimeRanges_h_
+
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+#include "TimeUnits.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class TimeRanges;
+} // namespace dom
+
+namespace dom {
+
+// Implements media TimeRanges:
+// http://www.whatwg.org/specs/web-apps/current-work/multipage/video.html#timeranges
+class TimeRanges final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TimeRanges)
+
+ TimeRanges();
+ explicit TimeRanges(nsISupports* aParent);
+ explicit TimeRanges(const media::TimeIntervals& aTimeIntervals);
+ explicit TimeRanges(const media::TimeRanges& aTimeRanges);
+ TimeRanges(nsISupports* aParent, const media::TimeIntervals& aTimeIntervals);
+ TimeRanges(nsISupports* aParent, const media::TimeRanges& aTimeRanges);
+
+ media::TimeIntervals ToTimeIntervals() const;
+
+ void Add(double aStart, double aEnd);
+
+ // Returns the start time of the first range, or -1 if no ranges exist.
+ double GetStartTime();
+
+ // Returns the end time of the last range, or -1 if no ranges exist.
+ double GetEndTime();
+
+ // See http://www.whatwg.org/html/#normalized-timeranges-object
+ void Normalize(double aTolerance = 0.0);
+
+ // Mutate this TimeRange to be the union of this and aOtherRanges.
+ void Union(const TimeRanges* aOtherRanges, double aTolerance);
+
+ // Mutate this TimeRange to be the intersection of this and aOtherRanges.
+ void Intersection(const TimeRanges* aOtherRanges);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject() const;
+
+ uint32_t Length() const { return mRanges.Length(); }
+
+ double Start(uint32_t aIndex, ErrorResult& aRv) const;
+
+ double End(uint32_t aIndex, ErrorResult& aRv) const;
+
+ double Start(uint32_t aIndex) const { return mRanges[aIndex].mStart; }
+
+ double End(uint32_t aIndex) const { return mRanges[aIndex].mEnd; }
+
+ // Shift all values by aOffset seconds.
+ void Shift(double aOffset);
+
+ private:
+ ~TimeRanges();
+
+ // Comparator which orders TimeRanges by start time. Used by Normalize().
+ struct TimeRange {
+ TimeRange(double aStart, double aEnd) : mStart(aStart), mEnd(aEnd) {}
+ double mStart;
+ double mEnd;
+ };
+
+ struct CompareTimeRanges {
+ bool Equals(const TimeRange& aTr1, const TimeRange& aTr2) const {
+ return aTr1.mStart == aTr2.mStart && aTr1.mEnd == aTr2.mEnd;
+ }
+
+ bool LessThan(const TimeRange& aTr1, const TimeRange& aTr2) const {
+ return aTr1.mStart < aTr2.mStart;
+ }
+ };
+
+ AutoTArray<TimeRange, 4> mRanges;
+
+ nsCOMPtr<nsISupports> mParent;
+
+ public:
+ typedef nsTArray<TimeRange>::index_type index_type;
+ static const index_type NoIndex = index_type(-1);
+
+ index_type Find(double aTime, double aTolerance = 0);
+
+ bool Contains(double aStart, double aEnd) {
+ index_type target = Find(aStart);
+ if (target == NoIndex) {
+ return false;
+ }
+
+ return mRanges[target].mEnd >= aEnd;
+ }
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_TimeRanges_h_
diff --git a/dom/html/ValidityState.cpp b/dom/html/ValidityState.cpp
new file mode 100644
index 0000000000..0c78cf9385
--- /dev/null
+++ b/dom/html/ValidityState.cpp
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ValidityState.h"
+#include "mozilla/dom/ValidityStateBinding.h"
+
+#include "nsCycleCollectionParticipant.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ValidityState, mConstraintValidation)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ValidityState)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ValidityState)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ValidityState)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+ValidityState::ValidityState(nsIConstraintValidation* aConstraintValidation)
+ : mConstraintValidation(aConstraintValidation) {}
+
+JSObject* ValidityState::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ValidityState_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/ValidityState.h b/dom/html/ValidityState.h
new file mode 100644
index 0000000000..b9cf7cf464
--- /dev/null
+++ b/dom/html/ValidityState.h
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ValidityState_h
+#define mozilla_dom_ValidityState_h
+
+#include "nsCOMPtr.h"
+#include "nsIConstraintValidation.h"
+#include "nsWrapperCache.h"
+#include "js/TypeDecls.h"
+
+namespace mozilla::dom {
+
+class ValidityState final : public nsISupports, public nsWrapperCache {
+ ~ValidityState() = default;
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ValidityState)
+
+ friend class ::nsIConstraintValidation;
+
+ nsIConstraintValidation* GetParentObject() const {
+ return mConstraintValidation;
+ }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Web IDL methods
+ bool ValueMissing() const {
+ return GetValidityState(
+ nsIConstraintValidation::VALIDITY_STATE_VALUE_MISSING);
+ }
+ bool TypeMismatch() const {
+ return GetValidityState(
+ nsIConstraintValidation::VALIDITY_STATE_TYPE_MISMATCH);
+ }
+ bool PatternMismatch() const {
+ return GetValidityState(
+ nsIConstraintValidation::VALIDITY_STATE_PATTERN_MISMATCH);
+ }
+ bool TooLong() const {
+ return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_TOO_LONG);
+ }
+ bool TooShort() const {
+ return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_TOO_SHORT);
+ }
+ bool RangeUnderflow() const {
+ return GetValidityState(
+ nsIConstraintValidation::VALIDITY_STATE_RANGE_UNDERFLOW);
+ }
+ bool RangeOverflow() const {
+ return GetValidityState(
+ nsIConstraintValidation::VALIDITY_STATE_RANGE_OVERFLOW);
+ }
+ bool StepMismatch() const {
+ return GetValidityState(
+ nsIConstraintValidation::VALIDITY_STATE_STEP_MISMATCH);
+ }
+ bool BadInput() const {
+ return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_BAD_INPUT);
+ }
+ bool CustomError() const {
+ return GetValidityState(
+ nsIConstraintValidation::VALIDITY_STATE_CUSTOM_ERROR);
+ }
+ bool Valid() const {
+ return !mConstraintValidation || mConstraintValidation->IsValid();
+ }
+
+ protected:
+ explicit ValidityState(nsIConstraintValidation* aConstraintValidation);
+
+ /**
+ * Helper function to get a validity state from constraint validation
+ * instance.
+ */
+ inline bool GetValidityState(
+ nsIConstraintValidation::ValidityStateType aState) const {
+ return mConstraintValidation &&
+ mConstraintValidation->GetValidityState(aState);
+ }
+
+ nsCOMPtr<nsIConstraintValidation> mConstraintValidation;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_ValidityState_h
diff --git a/dom/html/VideoDocument.cpp b/dom/html/VideoDocument.cpp
new file mode 100644
index 0000000000..00b5bf5308
--- /dev/null
+++ b/dom/html/VideoDocument.cpp
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDocument.h"
+#include "nsGkAtoms.h"
+#include "nsNodeInfoManager.h"
+#include "nsContentCreatorFunctions.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "DocumentInlines.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/Element.h"
+
+namespace mozilla::dom {
+
+class VideoDocument final : public MediaDocument {
+ public:
+ enum MediaDocumentKind MediaDocumentKind() const override {
+ return MediaDocumentKind::Video;
+ }
+
+ virtual nsresult StartDocumentLoad(const char* aCommand, nsIChannel* aChannel,
+ nsILoadGroup* aLoadGroup,
+ nsISupports* aContainer,
+ nsIStreamListener** aDocListener,
+ bool aReset = true) override;
+ virtual void SetScriptGlobalObject(
+ nsIScriptGlobalObject* aScriptGlobalObject) override;
+
+ virtual void Destroy() override {
+ if (mStreamListener) {
+ mStreamListener->DropDocumentRef();
+ }
+ MediaDocument::Destroy();
+ }
+
+ nsresult StartLayout() override;
+
+ protected:
+ nsresult CreateVideoElement();
+ // Sets document <title> to reflect the file name and description.
+ void UpdateTitle(nsIChannel* aChannel);
+
+ RefPtr<MediaDocumentStreamListener> mStreamListener;
+};
+
+nsresult VideoDocument::StartDocumentLoad(
+ const char* aCommand, nsIChannel* aChannel, nsILoadGroup* aLoadGroup,
+ nsISupports* aContainer, nsIStreamListener** aDocListener, bool aReset) {
+ nsresult rv = MediaDocument::StartDocumentLoad(
+ aCommand, aChannel, aLoadGroup, aContainer, aDocListener, aReset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mStreamListener = new MediaDocumentStreamListener(this);
+ NS_ADDREF(*aDocListener = mStreamListener);
+ return rv;
+}
+
+nsresult VideoDocument::StartLayout() {
+ // Create video element, and begin loading the media resource. Note we
+ // delay creating the video element until now (we're called from
+ // MediaDocumentStreamListener::OnStartRequest) as the PresShell is likely
+ // to have been created by now, so the MediaDecoder will be able to tell
+ // what kind of compositor we have, so the video element knows whether
+ // it can create a hardware accelerated video decoder or not.
+ nsresult rv = CreateVideoElement();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = MediaDocument::StartLayout();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+void VideoDocument::SetScriptGlobalObject(
+ nsIScriptGlobalObject* aScriptGlobalObject) {
+ // Set the script global object on the superclass before doing
+ // anything that might require it....
+ MediaDocument::SetScriptGlobalObject(aScriptGlobalObject);
+
+ if (aScriptGlobalObject && !InitialSetupHasBeenDone()) {
+ DebugOnly<nsresult> rv = CreateSyntheticDocument();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create synthetic video document");
+
+ if (!nsContentUtils::IsChildOfSameType(this)) {
+ LinkStylesheet(nsLiteralString(
+ u"resource://content-accessible/TopLevelVideoDocument.css"));
+ LinkScript(u"chrome://global/content/TopLevelVideoDocument.js"_ns);
+ }
+ InitialSetupDone();
+ }
+}
+
+nsresult VideoDocument::CreateVideoElement() {
+ RefPtr<Element> body = GetBodyElement();
+ if (!body) {
+ NS_WARNING("no body on video document!");
+ return NS_ERROR_FAILURE;
+ }
+
+ // make content
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::video, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ RefPtr<HTMLMediaElement> element = static_cast<HTMLMediaElement*>(
+ NS_NewHTMLVideoElement(nodeInfo.forget(), NOT_FROM_PARSER));
+ if (!element) return NS_ERROR_OUT_OF_MEMORY;
+ element->SetAutoplay(true, IgnoreErrors());
+ element->SetControls(true, IgnoreErrors());
+ element->LoadWithChannel(mChannel,
+ getter_AddRefs(mStreamListener->mNextStream));
+ UpdateTitle(mChannel);
+
+ if (nsContentUtils::IsChildOfSameType(this)) {
+ // Video documents that aren't toplevel should fill their frames and
+ // not have margins
+ element->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::style,
+ nsLiteralString(
+ u"position:absolute; top:0; left:0; width:100%; height:100%"),
+ true);
+ }
+
+ ErrorResult rv;
+ body->AppendChildTo(element, false, rv);
+ return rv.StealNSResult();
+}
+
+void VideoDocument::UpdateTitle(nsIChannel* aChannel) {
+ if (!aChannel) return;
+
+ nsAutoString fileName;
+ GetFileName(fileName, aChannel);
+ IgnoredErrorResult ignored;
+ SetTitle(fileName, ignored);
+}
+
+} // namespace mozilla::dom
+
+nsresult NS_NewVideoDocument(mozilla::dom::Document** aResult,
+ nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) {
+ auto* doc = new mozilla::dom::VideoDocument();
+
+ NS_ADDREF(doc);
+ nsresult rv = doc->Init(aPrincipal, aPartitionedPrincipal);
+
+ if (NS_FAILED(rv)) {
+ NS_RELEASE(doc);
+ }
+
+ *aResult = doc;
+
+ return rv;
+}
diff --git a/dom/html/crashtests/1032654.html b/dom/html/crashtests/1032654.html
new file mode 100644
index 0000000000..3067e10770
--- /dev/null
+++ b/dom/html/crashtests/1032654.html
@@ -0,0 +1 @@
+<template>&#x000d;#
diff --git a/dom/html/crashtests/1141260.html b/dom/html/crashtests/1141260.html
new file mode 100644
index 0000000000..c5ced3a447
--- /dev/null
+++ b/dom/html/crashtests/1141260.html
@@ -0,0 +1,4 @@
+<img src="foo" srcset="bar">
+<script>
+ document.querySelector("img").removeAttribute('src');
+</script>
diff --git a/dom/html/crashtests/1228876.html b/dom/html/crashtests/1228876.html
new file mode 100644
index 0000000000..b7beb645cf
--- /dev/null
+++ b/dom/html/crashtests/1228876.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var a = document.createElement("select");
+ var f = document.createElement("optgroup");
+ var g = document.createElement("optgroup");
+ a.appendChild(f);
+ g.appendChild(document.createElement("option"));
+ f.appendChild(g);
+ a.appendChild(document.createElement("option"));
+ document.body.appendChild(a);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/html/crashtests/1230110.html b/dom/html/crashtests/1230110.html
new file mode 100644
index 0000000000..4654641874
--- /dev/null
+++ b/dom/html/crashtests/1230110.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+// This test case should not leak.
+function leak()
+{
+ var img = document.createElement("img");
+ var iframe = document.createElement("iframe");
+ img.appendChild(iframe);
+ document.body.appendChild(img);
+
+ document.addEventListener('Foo', function(){});
+}
+</script>
+</head>
+<body onload="leak();"></body>
+</html>
diff --git a/dom/html/crashtests/1237633.html b/dom/html/crashtests/1237633.html
new file mode 100644
index 0000000000..c235f03158
--- /dev/null
+++ b/dom/html/crashtests/1237633.html
@@ -0,0 +1 @@
+<img srcset="data:,a 2400w" sizes="(min-width: 1px) calc(300px - 100vw)">
diff --git a/dom/html/crashtests/1281972-1.html b/dom/html/crashtests/1281972-1.html
new file mode 100644
index 0000000000..6fa5bb250b
--- /dev/null
+++ b/dom/html/crashtests/1281972-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<body onload="window[0].document.forms[0].submit();">
+<iframe src="data:text/html;charset=UTF-8,<form accept-charset=HZ-GB-2312>"></iframe>
+</body>
diff --git a/dom/html/crashtests/1282894.html b/dom/html/crashtests/1282894.html
new file mode 100644
index 0000000000..456a4541fd
--- /dev/null
+++ b/dom/html/crashtests/1282894.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom() {
+ var table = document.createElement("table");
+ var cap = document.createElement("caption");
+ cap.appendChild(table)
+ table.caption = cap;
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/html/crashtests/1290904.html b/dom/html/crashtests/1290904.html
new file mode 100644
index 0000000000..5ea23ba3b6
--- /dev/null
+++ b/dom/html/crashtests/1290904.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <fieldset id="outer">
+ <fieldset id="inner">
+ </fieldset>
+ </fieldset>
+ </body>
+</html>
+<script>
+function appendTextareaToFieldset(fieldset) {
+ var textarea = document.createElement("textarea");
+ textarea.setAttribute("required", "");
+ fieldset.appendChild(textarea);
+}
+
+var innerFieldset = document.getElementById('inner');
+var outerFieldset = document.getElementById('outer');
+
+var fieldset = document.createElement('fieldset');
+appendTextareaToFieldset(fieldset);
+appendTextareaToFieldset(fieldset);
+appendTextareaToFieldset(fieldset);
+appendTextareaToFieldset(fieldset);
+
+// Adding a fieldset to a nested fieldset.
+innerFieldset.appendChild(fieldset);
+appendTextareaToFieldset(fieldset);
+appendTextareaToFieldset(fieldset);
+// This triggers mInvalidElementsCount checks in outer fieldset.
+appendTextareaToFieldset(outerFieldset);
+
+// Removing a fieldset from a nested fieldset.
+innerFieldset.removeChild(fieldset);
+// This triggers mInvalidElementsCount checks in outer fieldset.
+appendTextareaToFieldset(outerFieldset);
+</script>
diff --git a/dom/html/crashtests/1343886-1.html b/dom/html/crashtests/1343886-1.html
new file mode 100644
index 0000000000..fe84959ac2
--- /dev/null
+++ b/dom/html/crashtests/1343886-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ document.documentElement.scrollTop = "500";
+ o1 = document.createRange();
+ o2 = document.createElement('input');
+ o1.selectNode(document.documentElement);
+ o1.surroundContents(o2);
+ o2.selectionStart;
+ </script>
+ </head>
+ <body></body>
+</html> \ No newline at end of file
diff --git a/dom/html/crashtests/1343886-2.xml b/dom/html/crashtests/1343886-2.xml
new file mode 100644
index 0000000000..91ddae103d
--- /dev/null
+++ b/dom/html/crashtests/1343886-2.xml
@@ -0,0 +1,3 @@
+<input xmlns="http://www.w3.org/1999/xhtml">
+ <script>document.documentElement.selectionStart</script>
+</input>
diff --git a/dom/html/crashtests/1343886-3.xml b/dom/html/crashtests/1343886-3.xml
new file mode 100644
index 0000000000..a579a5e074
--- /dev/null
+++ b/dom/html/crashtests/1343886-3.xml
@@ -0,0 +1,3 @@
+<textarea xmlns="http://www.w3.org/1999/xhtml">
+ <script>document.documentElement.selectionStart</script>
+</textarea>
diff --git a/dom/html/crashtests/1350972.html b/dom/html/crashtests/1350972.html
new file mode 100644
index 0000000000..7af7f9e174
--- /dev/null
+++ b/dom/html/crashtests/1350972.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+ try { o1 = document.createElement('tr'); } catch(e) {};
+ try { o2 = document.createElement('div'); } catch(e) {};
+ try { o3 = document.createElement('hr'); } catch(e) {};
+ try { o4 = document.createElement('textarea'); } catch(e) {};
+ try { o5 = document.getSelection(); } catch(e) {};
+ try { o6 = document.createRange(); } catch(e) {};
+ try { document.documentElement.appendChild(o2); } catch(e) {};
+ try { document.documentElement.appendChild(o3); } catch(e) {};
+ try { o2.appendChild(o4); } catch(e) {};
+ try { o3.outerHTML = "<noscript contenteditable='true'>"; } catch(e) {};
+ try { o4.select(); } catch(e) {};
+ try { o5.addRange(o6); } catch(e) {};
+ try { document.documentElement.appendChild(o1); } catch(e) {};
+ try { o5.selectAllChildren(o1); } catch(e) {};
+ try { o6.selectNode(o1); } catch(e) {};
+</script>
+</head>
+</html> \ No newline at end of file
diff --git a/dom/html/crashtests/1386905.html b/dom/html/crashtests/1386905.html
new file mode 100644
index 0000000000..6ecc59e23b
--- /dev/null
+++ b/dom/html/crashtests/1386905.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+document.documentElement.getBoundingClientRect()
+document.documentElement.innerHTML = "<input placeholder=e type=number readonly>"
+document.designMode = "on"
+document.execCommand("inserttext", false, "")
+document.designMode = "off"
+document.documentElement.style.display = 'none'
+</script>
+</head>
+</html>
diff --git a/dom/html/crashtests/1401726.html b/dom/html/crashtests/1401726.html
new file mode 100644
index 0000000000..bf4b4918ab
--- /dev/null
+++ b/dom/html/crashtests/1401726.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script>
+ try { o1 = document.createElement('button') } catch(e) { }
+ try { o2 = document.createElement('p') } catch(e) { }
+ try { o3 = o1.labels } catch(e) { }
+ try { o4 = document.createNSResolver(document.documentElement) } catch(e) { }
+ try { o5 = document.createRange(); } catch(e) { }
+ try { document.documentElement.appendChild(o1) } catch(e) { }
+ try { o5.selectNode(o4); } catch(e) { }
+ try { o5.surroundContents(o2) } catch(e) { }
+ try { o5.surroundContents(o2) } catch(e) { }
+ try { o1.labels.length } catch(e) { }
+</script>
+</head>
+</html>
diff --git a/dom/html/crashtests/1412173.html b/dom/html/crashtests/1412173.html
new file mode 100644
index 0000000000..6989260cc7
--- /dev/null
+++ b/dom/html/crashtests/1412173.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+let f = document.createElement('frame');
+f.onload = function() {
+ let frameDocument = f.contentDocument;
+ frameDocument.body.onbeforeunload = function () { frameDocument.write('<p>beforeUnload</p>') };
+ frameDocument.write('<p>trigger unload</p>')
+ window.stop();
+ document.documentElement.className = '';
+};
+document.documentElement.appendChild(f);
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/html/crashtests/1429783.html b/dom/html/crashtests/1429783.html
new file mode 100644
index 0000000000..211f9e7ee2
--- /dev/null
+++ b/dom/html/crashtests/1429783.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <script>
+ try { o2 = document.createElement('textarea') } catch(e) { }
+ try { o3 = document.getSelection() } catch(e) { }
+ try { document.documentElement.appendChild(o2) } catch(e) { }
+ try { o2.select() } catch(e) { }
+ try { o4 = o3.getRangeAt(0) } catch(e) { }
+ try { o5 = o4.commonAncestorContainer } catch(e) { }
+ try { o6 = o4.commonAncestorContainer } catch(e) { }
+ try { document.documentElement.after("", o5, "") } catch(e) { }
+ try { o4 = document.createElement('t') } catch(e) { }
+ try { document.appendChild(o4); } catch(e) { }
+ try { document.write(atob("PHNjcmlwdCBzcmM9Jyc+PC9zY3JpcHQ+Cg==")) } catch(e) { }
+ try { document.replaceChild(o6, document.documentElement); } catch(e) { }
+ </script>
+ </head>
+</html>
diff --git a/dom/html/crashtests/1440523.html b/dom/html/crashtests/1440523.html
new file mode 100644
index 0000000000..11ce699781
--- /dev/null
+++ b/dom/html/crashtests/1440523.html
@@ -0,0 +1,13 @@
+<html>
+ <head>
+ <script>
+ try { frame = document.createElement('frame') } catch(e) { }
+ try { document.documentElement.appendChild(frame) } catch(e) { }
+ try { contentDocument = frame.contentDocument } catch(e) { }
+ try { contentDocument.writeln("<p contenteditable='true'>") } catch(e) { }
+ try { anotherDocument = document.implementation.createHTMLDocument(); } catch(e) { }
+ try { rootOfAnotherDocument = anotherDocument.documentElement; } catch(e) { }
+ try { document.replaceChild(rootOfAnotherDocument, document.documentElement); } catch(e) { }
+ </script>
+ </head>
+</html>
diff --git a/dom/html/crashtests/1440827.html b/dom/html/crashtests/1440827.html
new file mode 100644
index 0000000000..ee204212ff
--- /dev/null
+++ b/dom/html/crashtests/1440827.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+<meta charset="UTF-8">
+>
+<meta content='referrer no-referrer;manifest-src self http:' http-equiv='Content-Security-Policy'>
+<script src='http://zzzzzz.z'></script>
diff --git a/dom/html/crashtests/1547057.html b/dom/html/crashtests/1547057.html
new file mode 100644
index 0000000000..89f48b5344
--- /dev/null
+++ b/dom/html/crashtests/1547057.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <script>
+ function start() {
+ const iframe = document.createElement('iframe')
+ iframe.policy.allowedFeatures()
+ }
+ window.addEventListener('load', start)
+ </script>
+</head>
+</html>
diff --git a/dom/html/crashtests/1550524.html b/dom/html/crashtests/1550524.html
new file mode 100644
index 0000000000..1b41f527dc
--- /dev/null
+++ b/dom/html/crashtests/1550524.html
@@ -0,0 +1,7 @@
+<iframe></iframe>
+<script>
+ var doc = frames[0].document;
+ doc.open();
+ doc.open();
+ doc.close();
+</script>
diff --git a/dom/html/crashtests/1550881-1.html b/dom/html/crashtests/1550881-1.html
new file mode 100644
index 0000000000..b9d73df957
--- /dev/null
+++ b/dom/html/crashtests/1550881-1.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<iframe srcdoc="<iframe></iframe>"></iframe>
+<script>
+ onload = function() {
+ parent = frames[0];
+ child = parent[0];
+ child.onunload = function() {
+ parent.document.open();
+ }
+ parent.document.open();
+ parent.document.write("Hello");
+ }
+</script>
diff --git a/dom/html/crashtests/1550881-2.html b/dom/html/crashtests/1550881-2.html
new file mode 100644
index 0000000000..97bf5ef350
--- /dev/null
+++ b/dom/html/crashtests/1550881-2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<iframe srcdoc="<iframe></iframe>"></iframe>
+<script>
+ onload = function() {
+ parent = frames[0];
+ child = parent[0];
+ child.onunload = function() {
+ parent.document.open();
+ parent.document.write("Nested");
+ }
+ parent.document.open();
+ parent.document.write("Hello");
+ }
+</script>
diff --git a/dom/html/crashtests/1667493.html b/dom/html/crashtests/1667493.html
new file mode 100644
index 0000000000..d7cf6c6174
--- /dev/null
+++ b/dom/html/crashtests/1667493.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function runTest() {
+ let win = window.open("1667493_1.html");
+ win.finish = function() {
+ document.documentElement.removeAttribute("class");
+ };
+}
+</script>
+<body onload="runTest()">
+</body>
+</html>
diff --git a/dom/html/crashtests/1667493_1.html b/dom/html/crashtests/1667493_1.html
new file mode 100644
index 0000000000..9b1a5fc047
--- /dev/null
+++ b/dom/html/crashtests/1667493_1.html
@@ -0,0 +1,7 @@
+<script>
+window.requestIdleCallback(() => {
+ window.close();
+ finish();
+});
+</script>
+<input required="" value="r" type="number">
diff --git a/dom/html/crashtests/1680418.html b/dom/html/crashtests/1680418.html
new file mode 100644
index 0000000000..ccebe0ed33
--- /dev/null
+++ b/dom/html/crashtests/1680418.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+window.addEventListener("hashchange", function() {
+ document.documentElement.removeAttribute("class");
+});
+
+async function runTest() {
+ document.getElementById("a").click();
+ await new Promise(resolve => setTimeout(resolve, 0));
+ document.location.hash = "#foo";
+}
+</script>
+<body onload="runTest()">
+<a id='a' type='text/html' href='telnet://'>
+</body>
+</html>
diff --git a/dom/html/crashtests/1704660.html b/dom/html/crashtests/1704660.html
new file mode 100644
index 0000000000..01497975df
--- /dev/null
+++ b/dom/html/crashtests/1704660.html
@@ -0,0 +1,17 @@
+<html>
+ <head>
+ <script>
+ function test() {
+ var ifr = document.getElementsByTagName("iframe")[0];
+ var form = ifr.contentDocument.getElementsByTagName("form")[0];
+ form.onsubmit = function() {
+ ifr.remove()
+ }
+ form.lastChild.click();
+ }
+ </script>
+ </head>
+ <body onload="test()">
+ <iframe srcdoc="<form><input name=f type=file><button></button></form>"></iframe>
+ </body>
+</html>
diff --git a/dom/html/crashtests/1724816.html b/dom/html/crashtests/1724816.html
new file mode 100644
index 0000000000..9264a5225f
--- /dev/null
+++ b/dom/html/crashtests/1724816.html
@@ -0,0 +1,14 @@
+<script>
+window.onload = () => {
+ window[0].focus()
+ a.close()
+ a.showModal()
+ document.execCommand("selectAll", false)
+}
+function go() {
+ document.onselectstart = document.createElement("frameset").onload
+}
+</script>
+<details open="true" ontoggle="go()">a</details>
+<iframe></iframe>
+<dialog id="a" tabindex="3">a</dialog>
diff --git a/dom/html/crashtests/1785933-inner.html b/dom/html/crashtests/1785933-inner.html
new file mode 100644
index 0000000000..8c97da8a8e
--- /dev/null
+++ b/dom/html/crashtests/1785933-inner.html
@@ -0,0 +1,28 @@
+<style>
+* {
+ image-rendering: pixelated;
+}
+</style>
+<script>
+window.requestIdleCallback(() => {
+ a.setSelectionRange(-1, 1)
+ document.head.innerHTML = undefined
+})
+</script>
+<embed src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="></embed>
+<textarea id="a">aaaaaaaaaaaaaaaa</textarea>
+<script>
+ const embed = document.querySelector("embed");
+ embed.onload = function() {
+ var countdown = 20;
+ if (location.search) {
+ countdown = parseInt(location.search.slice(1)) - 1;
+ }
+
+ if (countdown > 0) {
+ location.search = countdown;
+ } else {
+ window.parent.postMessage("done", "*");
+ }
+ }
+</script>
diff --git a/dom/html/crashtests/1785933.html b/dom/html/crashtests/1785933.html
new file mode 100644
index 0000000000..e73395faea
--- /dev/null
+++ b/dom/html/crashtests/1785933.html
@@ -0,0 +1,10 @@
+<html class="reftest-wait">
+<body>
+ <iframe src="./1785933-inner.html"></iframe>
+ <script>
+ window.onmessage = function() {
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+</body>
+</html>
diff --git a/dom/html/crashtests/1787671.html b/dom/html/crashtests/1787671.html
new file mode 100644
index 0000000000..1b4106435e
--- /dev/null
+++ b/dom/html/crashtests/1787671.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+let pp;
+window.addEventListener("MozReftestInvalidate", finish);
+document.addEventListener('DOMContentLoaded', () => {
+ pp = SpecialPowers.wrap(self).printPreview();
+ pp?.print();
+ setTimeout(window.close, 250);
+});
+function finish() {
+ setTimeout(function() {
+ pp.close();
+ document.documentElement.className = "";
+ }, 0);
+}
+</script>
+<img srcset="#"></img>
+</html>
diff --git a/dom/html/crashtests/1789475.html b/dom/html/crashtests/1789475.html
new file mode 100644
index 0000000000..f12924a742
--- /dev/null
+++ b/dom/html/crashtests/1789475.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<script>
+window.addEventListener('load', () => {
+ let a = document.createElement("img");
+ a.srcset = "1";
+ let b = new DOMParser()
+ let c = b.parseFromString("<div></div>", "text/html")
+ c.adoptNode(a)
+})
+</script>
diff --git a/dom/html/crashtests/1801380.html b/dom/html/crashtests/1801380.html
new file mode 100644
index 0000000000..f4ce3e109f
--- /dev/null
+++ b/dom/html/crashtests/1801380.html
@@ -0,0 +1 @@
+<input dir="auto" type="tel">
diff --git a/dom/html/crashtests/1840088.html b/dom/html/crashtests/1840088.html
new file mode 100644
index 0000000000..826b465b01
--- /dev/null
+++ b/dom/html/crashtests/1840088.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<table cellpadding="-129">
diff --git a/dom/html/crashtests/257818-1.html b/dom/html/crashtests/257818-1.html
new file mode 100644
index 0000000000..27929fd793
--- /dev/null
+++ b/dom/html/crashtests/257818-1.html
@@ -0,0 +1,82 @@
+<html><head>
+<script type="text/javascript">
+function cE (v) {
+ return document.createElement(v)
+}
+function cTN (v) {
+ return document.createTextNode(v)
+}
+
+function OSXBarIcon(elt) {
+ this.element = elt;
+ this.labelNode = this.element.firstChild;
+ this.labelNodeParent = this.element;
+ this.labelNodeParent.removeChild(this.labelNode);
+
+ this.contents = [];
+ var kids = this.element.childNodes;
+ for(var i=0; i<kids.length; i++) this.contents[this.contents.length] = this.element.removeChild(kids[i]);
+ this.popupSubmenu = new OSXBarSubmenu(this);
+}
+
+function OSXBarSubmenu(icon) {
+ this.parentIcon = icon;
+ this.create();
+ this.addContent();
+}
+OSXBarSubmenu.prototype = {
+ create : function() {
+ var p = this.popupNode = document.createElement("div");
+ var b = document.getElementsByTagName("body").item(0);
+ if(b) b.appendChild(p);
+ this.popupNode.style.display = "none";
+ // Uncomment next line to fix the problem
+// var v = document.body.offsetWidth;
+ }
+};
+OSXBarSubmenu.prototype.addContent = function() {
+
+ // add popup label:
+ var label = document.createElement("div");
+ label.appendChild(document.createTextNode(this.parentIcon.label));
+ this.popupNode.appendChild(label);
+
+ // add <li> children to the popup:
+ var contents = this.parentIcon.contents;
+ for(var i=0; i<contents.length; i++) {
+ this.popupNode.appendChild(contents[i]);
+
+ }
+};
+
+</script>
+
+<script type="text/javascript">
+function createControlPanel() {
+ var bar = document.getElementById("navigation");
+ var item = cE("li");
+ item.appendChild(cTN("aaa"));
+ var textfield = cE("input");
+ textfield.value = 0;
+ item.appendChild(textfield);
+ bar.insertBefore(item, bar.firstChild);
+}
+
+window.addEventListener("load", createControlPanel);
+</script>
+
+<script type="text/javascript">
+function ssload() {
+ new OSXBarIcon(document.getElementById("navigation").childNodes[0]);
+}
+window.addEventListener("load",ssload);
+
+
+</script>
+</head>
+
+<body>
+<ul id="navigation"></ul>
+</body></html>
+
+
diff --git a/dom/html/crashtests/285166-1.html b/dom/html/crashtests/285166-1.html
new file mode 100644
index 0000000000..8a73d7d74e
--- /dev/null
+++ b/dom/html/crashtests/285166-1.html
@@ -0,0 +1,3 @@
+<script>
+document.createElement("#text");
+</script>
diff --git a/dom/html/crashtests/294235-1.html b/dom/html/crashtests/294235-1.html
new file mode 100644
index 0000000000..00eed38f4e
--- /dev/null
+++ b/dom/html/crashtests/294235-1.html
@@ -0,0 +1,14 @@
+<html>
+ <head>
+ <title>bug 294235</title>
+ <script>
+ function countdown(){
+ count2.innerHTML="foo";
+ }
+ document.links.length;
+ </script>
+ </head>
+ <body>
+ <strong><div id="count2"><script>countdown();</script><noscript>bar</noscript></div></strong>
+ </body>
+</html>
diff --git a/dom/html/crashtests/307616-1.html b/dom/html/crashtests/307616-1.html
new file mode 100644
index 0000000000..8f28ddd6e8
--- /dev/null
+++ b/dom/html/crashtests/307616-1.html
@@ -0,0 +1,8 @@
+<html>
+<head><title>Testcase for assertion</title></head>
+<body>
+
+<input type="image" src="./nosuch.gif" alt="Search">
+
+</body>
+</html> \ No newline at end of file
diff --git a/dom/html/crashtests/324918-1.xhtml b/dom/html/crashtests/324918-1.xhtml
new file mode 100644
index 0000000000..921714cff3
--- /dev/null
+++ b/dom/html/crashtests/324918-1.xhtml
@@ -0,0 +1,26 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+
+
+function init()
+{
+ var sel = document.getElementById("sel");
+ var div = document.getElementById("div");
+ var newo = document.createElementNS("http://www.w3.org/1999/xhtml", "option");
+
+ sel.appendChild(div);
+ div.appendChild(newo);
+ sel.removeChild(div);
+}
+
+</script>
+</head>
+<body onload="init();">
+
+<div id="div"><option></option></div>
+
+<select id="sel"></select>
+
+</body>
+</html>
diff --git a/dom/html/crashtests/338649-1.xhtml b/dom/html/crashtests/338649-1.xhtml
new file mode 100644
index 0000000000..b0bf3186fc
--- /dev/null
+++ b/dom/html/crashtests/338649-1.xhtml
@@ -0,0 +1,22 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>ASSERTION: Options collection broken</title>
+</head>
+
+<body>
+
+<div>
+
+<select>
+ <option id="n29">B
+ <optgroup label="middle" id="n20">
+ <option>N</option>
+ </optgroup>
+ </option>
+ <option>M</option>
+</select>
+
+</div>
+
+</body>
+</html> \ No newline at end of file
diff --git a/dom/html/crashtests/339501-1.xhtml b/dom/html/crashtests/339501-1.xhtml
new file mode 100644
index 0000000000..1a231ee645
--- /dev/null
+++ b/dom/html/crashtests/339501-1.xhtml
@@ -0,0 +1,33 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+
+<script>
+
+function boom()
+{
+ document.addEventListener("DOMNodeRemoved", foo, false);
+ remove(document.getElementById("A"));
+ document.removeEventListener("DOMNodeRemoved", foo, false);
+
+ function foo()
+ {
+ document.removeEventListener("DOMNodeRemoved", foo, false);
+ remove(document.getElementById("B"));
+ }
+}
+
+
+window.addEventListener("load", boom, false);
+
+function remove(q1) { q1.parentNode.removeChild(q1); }
+
+</script>
+
+</head>
+
+<body>
+
+<select><option id="A">A</option><option id="B">B</option></select>
+
+</body>
+</html>
diff --git a/dom/html/crashtests/339501-2.xhtml b/dom/html/crashtests/339501-2.xhtml
new file mode 100644
index 0000000000..6a1835fb71
--- /dev/null
+++ b/dom/html/crashtests/339501-2.xhtml
@@ -0,0 +1,33 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+
+<script>
+
+function boom()
+{
+ document.addEventListener("DOMNodeRemoved", foo, false);
+ remove(document.getElementById("B"));
+ document.removeEventListener("DOMNodeRemoved", foo, false);
+
+ function foo()
+ {
+ document.removeEventListener("DOMNodeRemoved", foo, false);
+ remove(document.getElementById("A"));
+ }
+}
+
+
+window.addEventListener("load", boom, false);
+
+function remove(q1) { q1.parentNode.removeChild(q1); }
+
+</script>
+
+</head>
+
+<body>
+
+<select><option id="A">A</option><option id="B">B</option></select>
+
+</body>
+</html>
diff --git a/dom/html/crashtests/378993-1.xhtml b/dom/html/crashtests/378993-1.xhtml
new file mode 100644
index 0000000000..99cbb01dae
--- /dev/null
+++ b/dom/html/crashtests/378993-1.xhtml
@@ -0,0 +1,7 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body>
+<script>
+document.createElement('AREA');
+</script>
+</body>
+</html>
diff --git a/dom/html/crashtests/382568-1-inner.xhtml b/dom/html/crashtests/382568-1-inner.xhtml
new file mode 100644
index 0000000000..67d7427582
--- /dev/null
+++ b/dom/html/crashtests/382568-1-inner.xhtml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!DOCTYPE html
+PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"> <head>
+<title>Test</title>
+<script type="text/javascript"><![CDATA[
+
+function onAttrModified(evt) {
+// window.alert("Mutation event fired within the frame code.");
+// evt.target.focus();
+// evt.target.blur();
+ evt.target.style.background = 'green';
+ bounce(evt.target);
+// evt.target.normalize();
+// bounce(evt.target.parentNode);
+}
+function die(n) {
+ p = n.parentNode;
+ p.removeChild(n);
+}
+
+function bounce(n) {
+ p = n.parentNode;
+ p.removeChild(n);
+ p.appendChild(n);
+}
+
+
+function test_AttrModified() {
+ var x = document.getElementById("x");
+ x.addEventListener("DOMAttrModified", onAttrModified);
+ bounce(x);
+}
+
+function test() {
+ setTimeout(test_AttrModified, 3000);
+}
+]]></script>
+</head>
+
+<body onload="test()">
+<h1>TestCase for unsafe mutable events from textarea</h1>
+<p>Please wait for 3 seconds after document was loaded,
+if your browser is vulnerable, it may stop responding
+to keyboard and mouse event
+and most likely it will eventually crash (may take a
+while for debug builds).</p>
+<p>
+<textarea id="x"></textarea>
+</p>
+</body> </html>
diff --git a/dom/html/crashtests/382568-1.html b/dom/html/crashtests/382568-1.html
new file mode 100644
index 0000000000..c10c71fd14
--- /dev/null
+++ b/dom/html/crashtests/382568-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 3500);
+</script>
+<body>
+<iframe src="382568-1-inner.xhtml"></iframe>
+</body>
+</html>
diff --git a/dom/html/crashtests/383137.xhtml b/dom/html/crashtests/383137.xhtml
new file mode 100644
index 0000000000..87d853c035
--- /dev/null
+++ b/dom/html/crashtests/383137.xhtml
@@ -0,0 +1,13 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<script id="script" xmlns="http://www.w3.org/1999/xhtml">//<![CDATA[
+function doe(){
+document.documentElement.setAttribute('style', '');
+}
+
+setTimeout(doe,100);
+//]]></script>
+
+<style style="overflow: scroll; display: block;">
+style::before {content: counter(section); }
+</style>
+</html> \ No newline at end of file
diff --git a/dom/html/crashtests/388183-1.html b/dom/html/crashtests/388183-1.html
new file mode 100644
index 0000000000..fcca410e93
--- /dev/null
+++ b/dom/html/crashtests/388183-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html contenteditable="true">
+<head>
+</head>
+<body>
+<div contenteditable="true"></div>
+</body>
+</html>
diff --git a/dom/html/crashtests/395340-1.html b/dom/html/crashtests/395340-1.html
new file mode 100644
index 0000000000..ddbccfe968
--- /dev/null
+++ b/dom/html/crashtests/395340-1.html
@@ -0,0 +1,28 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ var div1 = document.createElement("div");
+ div1.style.counterIncrement = "chicken";
+ div1.contentEditable = "true";
+
+ var div2 = document.createElement("div");
+ div2.style.counterIncrement = "chicken";
+
+ document.body.style.content = "'A'";
+
+ div1.appendChild(div2);
+ document.body.appendChild(div1);
+ div1.removeChild(div2);
+}
+
+</script>
+
+</head>
+
+<body onload="boom();">
+
+</body>
+</html>
diff --git a/dom/html/crashtests/399694-1.html b/dom/html/crashtests/399694-1.html
new file mode 100644
index 0000000000..e6db2342b5
--- /dev/null
+++ b/dom/html/crashtests/399694-1.html
@@ -0,0 +1,20 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+<head>
+<script>
+function boom()
+{
+ var legend = document.createElementNS("http://www.w3.org/1999/xhtml", "legend")
+ var input = document.getElementById("input");
+ input.appendChild(legend);
+ legend.focus();
+
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+<body onload="setTimeout(boom, 3);" contenteditable="true">
+
+<input contenteditable="false"><input id="input"><q><div></div></q>
+
+</body>
+</html>
diff --git a/dom/html/crashtests/407053.html b/dom/html/crashtests/407053.html
new file mode 100644
index 0000000000..b80a231722
--- /dev/null
+++ b/dom/html/crashtests/407053.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body onload="document.execCommand('copy', false, '');">
+<div contenteditable="true"></div>
+</body>
+</html>
diff --git a/dom/html/crashtests/423371-1.html b/dom/html/crashtests/423371-1.html
new file mode 100644
index 0000000000..0069cf95db
--- /dev/null
+++ b/dom/html/crashtests/423371-1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<div style="position:fixed; top:100px;" id="d">hello</div>
+<script>
+document.write(document.getElementById("d").offsetTop);
+</script>
+</body>
+</html>
diff --git a/dom/html/crashtests/448564.html b/dom/html/crashtests/448564.html
new file mode 100644
index 0000000000..f31830afa1
--- /dev/null
+++ b/dom/html/crashtests/448564.html
@@ -0,0 +1,7 @@
+<s>
+<form>a</form>
+<iframe></iframe>
+<script src=a></script>
+<form></form>
+<table>
+<optgroup>
diff --git a/dom/html/crashtests/451123-1.html b/dom/html/crashtests/451123-1.html
new file mode 100644
index 0000000000..abf23c1e73
--- /dev/null
+++ b/dom/html/crashtests/451123-1.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+</head>
+<body>
+<input type="file" type="file">
+</body>
+</html>
diff --git a/dom/html/crashtests/453406-1.html b/dom/html/crashtests/453406-1.html
new file mode 100644
index 0000000000..bf75686553
--- /dev/null
+++ b/dom/html/crashtests/453406-1.html
@@ -0,0 +1,34 @@
+<html>
+<head>
+<script type="text/javascript">
+function boom()
+{
+ var r = document.documentElement;
+ while (r.firstChild)
+ r.firstChild.remove();
+
+ var b = document.createElement("BODY");
+ var s = document.createElement("SCRIPT");
+ var f1 = document.createElement("FORM");
+ var i = document.createElement("INPUT");
+ var br = document.createElement("BR");
+ var f2 = document.createElement("FORM");
+ var span = document.createElement("SPAN");
+ f1.appendChild(i);
+ f1.appendChild(br);
+ s.appendChild(f1);
+ b.appendChild(s);
+ f2.appendChild(span);
+ b.appendChild(f2)
+ document.documentElement.appendChild(b);
+ b.contentEditable = "true";
+ document.execCommand("indent", false, null);
+ b.contentEditable = "false";
+ span.appendChild(i);
+ i.remove();
+}
+</script>
+</head>
+<body onload="boom();">
+</body>
+</html>
diff --git a/dom/html/crashtests/464197-1.html b/dom/html/crashtests/464197-1.html
new file mode 100644
index 0000000000..785686023e
--- /dev/null
+++ b/dom/html/crashtests/464197-1.html
@@ -0,0 +1,23 @@
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+
+var i = 0;
+
+function boom()
+{
+ var s = document.createElement("span");
+ s.innerHTML = "<audio src='javascript:5'><\/audio>";
+ s.innerHTML = "";
+
+ if (++i < 10)
+ setTimeout(boom, 0);
+ else
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+<body onload="boom();">
+</body>
+</html>
diff --git a/dom/html/crashtests/468562-1.html b/dom/html/crashtests/468562-1.html
new file mode 100644
index 0000000000..81123fe2ea
--- /dev/null
+++ b/dom/html/crashtests/468562-1.html
@@ -0,0 +1,6 @@
+<html>
+<head></head>
+<body>
+<table><script>document.write("Q");</script><link>
+</body>
+</html>
diff --git a/dom/html/crashtests/468562-2.html b/dom/html/crashtests/468562-2.html
new file mode 100644
index 0000000000..e0db5cd974
--- /dev/null
+++ b/dom/html/crashtests/468562-2.html
@@ -0,0 +1,6 @@
+<html>
+<head></head>
+<body>
+<table><script>document.write("Q");</script><link></table>
+</body>
+</html>
diff --git a/dom/html/crashtests/494225.html b/dom/html/crashtests/494225.html
new file mode 100644
index 0000000000..a78051d2a1
--- /dev/null
+++ b/dom/html/crashtests/494225.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html><head><script>
+ var x = "f: '" + document.fgColor +
+ "' b: '" + document.bgColor +
+ "' l: '" + document.linkColor +
+ "' a: '" + document.alinkColor +
+ "' v: '" + document.vlinkColor + "'";
+</script></head><body
+onload="document.body.appendChild(document.createTextNode(x))"
+></body></html>
diff --git a/dom/html/crashtests/495543.svg b/dom/html/crashtests/495543.svg
new file mode 100644
index 0000000000..b795796ad4
--- /dev/null
+++ b/dom/html/crashtests/495543.svg
@@ -0,0 +1,16 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+<script type="text/javascript">
+
+function boom()
+{
+ var htmlBody = document.createElementNS("http://www.w3.org/1999/xhtml", "body");
+ document.documentElement.appendChild(htmlBody);
+ htmlBody.setAttribute('vlink', "transparent");
+ document.height;
+ document.vlinkColor;
+}
+
+window.addEventListener("load", boom, false);
+</script>
+
+</svg>
diff --git a/dom/html/crashtests/495546-1.html b/dom/html/crashtests/495546-1.html
new file mode 100644
index 0000000000..0547b98674
--- /dev/null
+++ b/dom/html/crashtests/495546-1.html
@@ -0,0 +1,19 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ var video = document.createElement("video");
+ var source = document.createElement("source");
+ source.setAttributeNS(null, "src", "http://127.0.0.1/");
+ video.appendChild(source);
+ document.body.appendChild(video);
+ setTimeout(function() { document.body.removeChild(video); }, 20);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/dom/html/crashtests/504183-1.html b/dom/html/crashtests/504183-1.html
new file mode 100644
index 0000000000..e44db41520
--- /dev/null
+++ b/dom/html/crashtests/504183-1.html
@@ -0,0 +1,12 @@
+<html class="reftest-wait">
+<input id="a" type="file">
+<script>
+function doe() {
+var elem = document.getElementById('a');
+elem.style.display = "none";
+elem.focus();
+document.documentElement.className = "";
+}
+setTimeout(doe, 0);
+</script>
+</html>
diff --git a/dom/html/crashtests/515829-1.html b/dom/html/crashtests/515829-1.html
new file mode 100644
index 0000000000..e2fc655c01
--- /dev/null
+++ b/dom/html/crashtests/515829-1.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<head></head>
+<body onload="document.getElementById('x').innerHTML = '<button></button>';">
+<form><div id="x"><button></button></div><button></button></form>
+</body>
+</html>
diff --git a/dom/html/crashtests/515829-2.html b/dom/html/crashtests/515829-2.html
new file mode 100644
index 0000000000..6de6d5986c
--- /dev/null
+++ b/dom/html/crashtests/515829-2.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<head></head>
+<body onload="document.getElementById('x').innerHTML = '<button></button>';">
+<form><div id="x"><button></button></div><button></button><input type="image"></form>
+</body>
+</html>
diff --git a/dom/html/crashtests/570566-1.html b/dom/html/crashtests/570566-1.html
new file mode 100644
index 0000000000..70ec003a68
--- /dev/null
+++ b/dom/html/crashtests/570566-1.html
@@ -0,0 +1,2 @@
+<body onload="document.getElementById('i').removeAttribute('type')"><input id=i type=image></body>
+
diff --git a/dom/html/crashtests/571428-1.html b/dom/html/crashtests/571428-1.html
new file mode 100644
index 0000000000..ae2b960cac
--- /dev/null
+++ b/dom/html/crashtests/571428-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function boom()
+{
+ var i = document.getElementById("i");
+ i.setAttribute("type", "button");
+ i.removeAttribute("type");
+}
+</script>
+</head>
+<body onload="boom();"><input id="i"></body>
+</html>
diff --git a/dom/html/crashtests/580507-1.xhtml b/dom/html/crashtests/580507-1.xhtml
new file mode 100644
index 0000000000..eff3fb255d
--- /dev/null
+++ b/dom/html/crashtests/580507-1.xhtml
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var input = document.getElementById("i");
+ input.setAttribute('type', "image");
+ input.removeAttribute('type');
+ input.placeholder = "Y";
+}
+
+</script>
+</head>
+
+<body onload="boom();"><input id="i" /></body>
+</html>
diff --git a/dom/html/crashtests/590387.html b/dom/html/crashtests/590387.html
new file mode 100644
index 0000000000..50cd05ac9e
--- /dev/null
+++ b/dom/html/crashtests/590387.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+document.createElement("div").innerHTML = "<output form=x>";
+</script>
+</head>
+</html>
diff --git a/dom/html/crashtests/596785-1.html b/dom/html/crashtests/596785-1.html
new file mode 100644
index 0000000000..5d830628b0
--- /dev/null
+++ b/dom/html/crashtests/596785-1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <body onload="document.getElementById('i').type = 'image';
+ document.documentElement.className = '';">
+ <form>
+ <input id='i' required>
+ </form>
+ </body>
+</html>
diff --git a/dom/html/crashtests/596785-2.html b/dom/html/crashtests/596785-2.html
new file mode 100644
index 0000000000..18cfe2788d
--- /dev/null
+++ b/dom/html/crashtests/596785-2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <body onload="document.getElementById('i').type = 'text';
+ document.documentElement.className = '';">
+ <form>
+ <input id='i' type='image' required>
+ </form>
+ </body>
+</html>
diff --git a/dom/html/crashtests/602117.html b/dom/html/crashtests/602117.html
new file mode 100644
index 0000000000..0d1818b2a9
--- /dev/null
+++ b/dom/html/crashtests/602117.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <script>
+ var isindex = document.createElement("isindex");
+ isindex.setAttribute("form", "f");
+ isindex.form;
+ </script>
+</html>
diff --git a/dom/html/crashtests/604807.html b/dom/html/crashtests/604807.html
new file mode 100644
index 0000000000..bec4012e5c
--- /dev/null
+++ b/dom/html/crashtests/604807.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<script>
+try {
+ var selectElem = document.createElementNS("http://www.w3.org/1999/xhtml", "select");
+ selectElem.QueryInterface(Ci.nsISelectElement);
+ selectElem.getOptionIndex(null, 0, true);
+} catch (e) {
+}
+</script>
diff --git a/dom/html/crashtests/605264.html b/dom/html/crashtests/605264.html
new file mode 100644
index 0000000000..782720a9d6
--- /dev/null
+++ b/dom/html/crashtests/605264.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<script>
+
+var v = document.createElementNS("http://www.w3.org/1999/xhtml", "video");
+v.QueryInterface(Ci.nsIObserver);
+v.observe(null, null, null);
+
+</script>
diff --git a/dom/html/crashtests/606430-1.html b/dom/html/crashtests/606430-1.html
new file mode 100644
index 0000000000..c347e3c9f1
--- /dev/null
+++ b/dom/html/crashtests/606430-1.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var r = document.documentElement;
+ var i = document.getElementById("i");
+
+ document.removeChild(r);
+ document.appendChild(r);
+ w("dump('A\\n')");
+ document.removeChild(r);
+ w("dump('B\\n')");
+ document.appendChild(r);
+
+ function w(s)
+ {
+ var ns = document.createElement("script");
+ var nt = document.createTextNode(s);
+ ns.appendChild(nt);
+ i.appendChild(ns);
+ }
+}
+
+</script>
+</head>
+
+<body onload="boom();"><input id="i"></body>
+</html>
diff --git a/dom/html/crashtests/613027.html b/dom/html/crashtests/613027.html
new file mode 100644
index 0000000000..a866860f10
--- /dev/null
+++ b/dom/html/crashtests/613027.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var a = document.createElementNS("http://www.w3.org/1999/xhtml", "fieldset");
+ var b = document.createElementNS("http://www.w3.org/1999/xhtml", "legend");
+ var c = document.createElementNS("http://www.w3.org/1999/xhtml", "input");
+
+ a.appendChild(b);
+ a.appendChild(c);
+ a.removeChild(b);
+ c.expandoQ = a;
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/html/crashtests/614279.html b/dom/html/crashtests/614279.html
new file mode 100644
index 0000000000..ff1c505167
--- /dev/null
+++ b/dom/html/crashtests/614279.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var a = document.createElementNS("http://www.w3.org/1999/xhtml", "datalist");
+ var b = document.createElementNS("http://www.w3.org/1999/xhtml", "option");
+
+ a.appendChild(b);
+ b.expando = a.options;
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/html/crashtests/614988-1.html b/dom/html/crashtests/614988-1.html
new file mode 100644
index 0000000000..931f83ac73
--- /dev/null
+++ b/dom/html/crashtests/614988-1.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<head><script>window.addEventListener("load", function() { var t = document.getElementById("t"); t.appendChild(t.previousSibling); }, false);</script></head>
+<body><fieldset><legend></legend><textarea id="t"></textarea></fieldset></body>
+</html>
diff --git a/dom/html/crashtests/616401.html b/dom/html/crashtests/616401.html
new file mode 100644
index 0000000000..eb5a0bcf4c
--- /dev/null
+++ b/dom/html/crashtests/616401.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<script>
+var c = document.createElement("canvas");
+c.getContext("experimental-webgl", {
+ get a() { throw 7; },
+ get b() { throw 8; }
+});
+</script>
diff --git a/dom/html/crashtests/620078-1.html b/dom/html/crashtests/620078-1.html
new file mode 100644
index 0000000000..39462706c1
--- /dev/null
+++ b/dom/html/crashtests/620078-1.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var frame = document.getElementById("f");
+ var frameRoot = frame.contentDocument.documentElement;
+ document.body.removeChild(frame);
+ var i = document.createElementNS("http://www.w3.org/1999/xhtml", "input");
+ i.setAttribute("autofocus", "true");
+ frameRoot.appendChild(i);
+}
+
+</script>
+</head>
+
+<body onload="boom();"><iframe id="f" src="data:text/html,"></iframe></body>
+</html>
diff --git a/dom/html/crashtests/620078-2.html b/dom/html/crashtests/620078-2.html
new file mode 100644
index 0000000000..1be23fa1f2
--- /dev/null
+++ b/dom/html/crashtests/620078-2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body onload='document.cloneNode(1)'>
+ <button autofocus></button>
+ </body>
+</html>
diff --git a/dom/html/crashtests/631421.html b/dom/html/crashtests/631421.html
new file mode 100644
index 0000000000..e4a7b9192b
--- /dev/null
+++ b/dom/html/crashtests/631421.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+"use strict";
+
+var f2;
+
+function newIframe()
+{
+ var f = document.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
+ f.setAttributeNS(null, "src", "631421.png");
+ document.body.appendChild(f);
+ return f;
+}
+
+function b1()
+{
+ void newIframe();
+ f2 = newIframe();
+ setTimeout(b2, 0);
+}
+
+function b2()
+{
+ document.body.removeChild(f2);
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+<body onload="b1();"></body>
+</html>
diff --git a/dom/html/crashtests/631421.png b/dom/html/crashtests/631421.png
new file mode 100644
index 0000000000..ef350c4678
--- /dev/null
+++ b/dom/html/crashtests/631421.png
Binary files differ
diff --git a/dom/html/crashtests/673853.html b/dom/html/crashtests/673853.html
new file mode 100644
index 0000000000..1325fa9ed6
--- /dev/null
+++ b/dom/html/crashtests/673853.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var otherDoc = document.implementation.createDocument('', '', null);
+ var input = otherDoc.createElementNS("http://www.w3.org/1999/xhtml", "input");
+ var form = otherDoc.createElementNS("http://www.w3.org/1999/xhtml", "form");
+ input.setAttributeNS(null, "form", "x");
+ form.setAttributeNS(null, "id", "x");
+ input.appendChild(form);
+ otherDoc.appendChild(input);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/html/crashtests/682058.xhtml b/dom/html/crashtests/682058.xhtml
new file mode 100644
index 0000000000..95e7da98fc
--- /dev/null
+++ b/dom/html/crashtests/682058.xhtml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <body onload="test()">
+ <script>
+ function test() {
+ document.body.innerHTML = "Foobar";
+ }
+ document.addEventListener("DOMNodeRemoved", function() {});
+ </script>
+ </body>
+</html>
diff --git a/dom/html/crashtests/682460.html b/dom/html/crashtests/682460.html
new file mode 100644
index 0000000000..91306be71d
--- /dev/null
+++ b/dom/html/crashtests/682460.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var f = function() {
+ document.documentElement.offsetHeight;
+ };
+ window.addEventListener("DOMSubtreeModified", f, true);
+
+ document.getElementsByTagName("table")[0].setAttribute("cellpadding", "2");
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+<table><tr><td></td></tr></table>
+</body>
+</html>
diff --git a/dom/html/crashtests/68912-1.html b/dom/html/crashtests/68912-1.html
new file mode 100644
index 0000000000..bdd2ab4614
--- /dev/null
+++ b/dom/html/crashtests/68912-1.html
@@ -0,0 +1,24 @@
+<html>
+<head>
+<title>Crash TR.cells = null</title>
+<script language="javascript">
+function crashme()
+{
+ var elm = document.createElement('tr');
+
+ elm.cells = null;
+}
+
+</script>
+</head>
+<body onload="crashme()">
+
+<p>
+This test case creates a TR element then tries to assign to the cells property
+</p>
+<p>
+Crash
+</p>
+
+</body>
+</html>
diff --git a/dom/html/crashtests/738744.xhtml b/dom/html/crashtests/738744.xhtml
new file mode 100644
index 0000000000..7f91d0149b
--- /dev/null
+++ b/dom/html/crashtests/738744.xhtml
@@ -0,0 +1,4 @@
+<input xmlns="http://www.w3.org/1999/xhtml" form="f">
+ <form id="f" />
+ <input form="f" />
+</input>
diff --git a/dom/html/crashtests/741218.json b/dom/html/crashtests/741218.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/dom/html/crashtests/741218.json
@@ -0,0 +1 @@
+{}
diff --git a/dom/html/crashtests/741218.json^headers^ b/dom/html/crashtests/741218.json^headers^
new file mode 100644
index 0000000000..7b5e82d4b7
--- /dev/null
+++ b/dom/html/crashtests/741218.json^headers^
@@ -0,0 +1 @@
+Content-Type: application/json
diff --git a/dom/html/crashtests/741250.xhtml b/dom/html/crashtests/741250.xhtml
new file mode 100644
index 0000000000..e18a9409fe
--- /dev/null
+++ b/dom/html/crashtests/741250.xhtml
@@ -0,0 +1,9 @@
+<input form="f" id="x" xmlns="http://www.w3.org/1999/xhtml">
+<form id="f"></form>
+<script>
+window.addEventListener("load", function() {
+ var x = document.getElementById("x");
+ x.appendChild(x.cloneNode(true));
+}, false);
+</script>
+</input>
diff --git a/dom/html/crashtests/768344.html b/dom/html/crashtests/768344.html
new file mode 100644
index 0000000000..4830e6674d
--- /dev/null
+++ b/dom/html/crashtests/768344.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ function f() {
+ document.removeEventListener("DOMSubtreeModified", f, true);
+ document.documentElement.setAttributeNS(null, "contenteditable", "false");
+ }
+
+ document.addEventListener("DOMSubtreeModified", f, true);
+
+ document.documentElement.contentEditable = "true";
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/dom/html/crashtests/795221-1.html b/dom/html/crashtests/795221-1.html
new file mode 100644
index 0000000000..70e400bed9
--- /dev/null
+++ b/dom/html/crashtests/795221-1.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<style>
+ div { }
+</style>
+<script>
+ document.styleSheets[0].cssRules[0].style.foo = document;
+</script>
diff --git a/dom/html/crashtests/795221-2.html b/dom/html/crashtests/795221-2.html
new file mode 100644
index 0000000000..fc2aa7d506
--- /dev/null
+++ b/dom/html/crashtests/795221-2.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<style>
+ @media all {
+ div { }
+ }
+</style>
+<script>
+ document.styleSheets[0].cssRules[0].cssRules[0].style.foo = document;
+</script>
diff --git a/dom/html/crashtests/795221-3.html b/dom/html/crashtests/795221-3.html
new file mode 100644
index 0000000000..93348af0bd
--- /dev/null
+++ b/dom/html/crashtests/795221-3.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<body>
+<script>
+ // Create the stylesheet via script, so that the parser's preloading doesn't
+ // make the CSS loader hold on to the sheet we're _not_ trying to form a
+ // cycle through, thus accidentally avoiding the cycle
+ var link = document.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("href", "data:text/css,div { }");
+ link.onload = function () {
+ document.styleSheets[0].cssRules[0].style.foo = document;
+ }
+ document.body.appendChild(link);
+</script>
diff --git a/dom/html/crashtests/795221-4.html b/dom/html/crashtests/795221-4.html
new file mode 100644
index 0000000000..476dd34672
--- /dev/null
+++ b/dom/html/crashtests/795221-4.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<svg>
+ <style>
+ div { }
+ </style>
+</svg>
+<script>
+ document.styleSheets[0].cssRules[0].style.foo = document;
+</script>
diff --git a/dom/html/crashtests/795221-5.xml b/dom/html/crashtests/795221-5.xml
new file mode 100644
index 0000000000..286dcff0d4
--- /dev/null
+++ b/dom/html/crashtests/795221-5.xml
@@ -0,0 +1,6 @@
+<?xml-stylesheet href="data:text/css,div {}"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <script>
+ document.styleSheets[0].cssRules[0].style.foo = document;
+ </script>
+</html>
diff --git a/dom/html/crashtests/798802-1.html b/dom/html/crashtests/798802-1.html
new file mode 100644
index 0000000000..92ab50fd87
--- /dev/null
+++ b/dom/html/crashtests/798802-1.html
@@ -0,0 +1,18 @@
+<html>
+ <head>
+ <script>
+ onload = function() {
+ var canvas2d = document.createElement('canvas')
+ canvas2d.setAttribute('width', 0)
+ document.body.appendChild(canvas2d)
+ var ctx2d = canvas2d.getContext('2d')
+ ctx2d.fillStyle = 'black'
+ var gl = document.createElement('canvas').getContext('experimental-webgl')
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas2d)
+ ctx2d.fillRect(0, 0, 1, 1)
+ }
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/dom/html/crashtests/811226.html b/dom/html/crashtests/811226.html
new file mode 100644
index 0000000000..990ef9af53
--- /dev/null
+++ b/dom/html/crashtests/811226.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body onload="document.getElementById('s').onerror;">
+<span id="s" onerror="0;"></span>
+</body>
+</html>
diff --git a/dom/html/crashtests/819745.html b/dom/html/crashtests/819745.html
new file mode 100644
index 0000000000..389e6b8c13
--- /dev/null
+++ b/dom/html/crashtests/819745.html
@@ -0,0 +1,5 @@
+<!doctype="html">
+<body>
+<script>
+ document.body.onerror = null;
+</script>
diff --git a/dom/html/crashtests/828180.html b/dom/html/crashtests/828180.html
new file mode 100644
index 0000000000..055c8a34ba
--- /dev/null
+++ b/dom/html/crashtests/828180.html
@@ -0,0 +1,5 @@
+<script>
+var table = document.createElement("table");
+table.tHead = null;
+table.tFoot = null;
+</script>
diff --git a/dom/html/crashtests/828472.html b/dom/html/crashtests/828472.html
new file mode 100644
index 0000000000..59ce4d280b
--- /dev/null
+++ b/dom/html/crashtests/828472.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+<input type='date' value='2013-01-01'>
+</body>
+</html>
diff --git a/dom/html/crashtests/837033.html b/dom/html/crashtests/837033.html
new file mode 100644
index 0000000000..772d24014c
--- /dev/null
+++ b/dom/html/crashtests/837033.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<html>
+<body onload="document.createElement('button').validity.x = null;"></body>
+</html>
diff --git a/dom/html/crashtests/838256-1.html b/dom/html/crashtests/838256-1.html
new file mode 100644
index 0000000000..e1cdc588e8
--- /dev/null
+++ b/dom/html/crashtests/838256-1.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<style>
+
+::-moz-range-track {
+ display: none ! important;
+}
+
+::-moz-range-thumb {
+ display: none ! important;
+}
+
+</style>
+</head>
+<body>
+<input type="range">
+</body>
+</html>
diff --git a/dom/html/crashtests/862084.html b/dom/html/crashtests/862084.html
new file mode 100644
index 0000000000..d6d04f74de
--- /dev/null
+++ b/dom/html/crashtests/862084.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<select></select>
+<script>
+var select = document.getElementsByTagName("select");
+select.item(0);
+select[0];
+select.namedItem("x")
+select["x"]
+</script>
diff --git a/dom/html/crashtests/865147.html b/dom/html/crashtests/865147.html
new file mode 100644
index 0000000000..841cf8b6d8
--- /dev/null
+++ b/dom/html/crashtests/865147.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<select></select>
+<script>
+var select = document.getElementsByTagName("select")[0];
+var newOpt = document.createElement("option");
+select.add(newOpt, newOpt);
+</script>
diff --git a/dom/html/crashtests/877910.html b/dom/html/crashtests/877910.html
new file mode 100644
index 0000000000..d454c4478b
--- /dev/null
+++ b/dom/html/crashtests/877910.html
@@ -0,0 +1 @@
+<select><option id="foo"><script>document.querySelector("select").namedItem("foo")</script>
diff --git a/dom/html/crashtests/903106.html b/dom/html/crashtests/903106.html
new file mode 100644
index 0000000000..fceaf4761a
--- /dev/null
+++ b/dom/html/crashtests/903106.html
@@ -0,0 +1,3 @@
+<script>
+ document.createElement("tr").sectionRowIndex;
+</script>
diff --git a/dom/html/crashtests/916322-1.html b/dom/html/crashtests/916322-1.html
new file mode 100644
index 0000000000..56b43d6394
--- /dev/null
+++ b/dom/html/crashtests/916322-1.html
@@ -0,0 +1,10 @@
+<canvas height=16 id=gl>
+<script>
+// Without the fix, this would trigger an assertion in the debug build
+document.addEventListener("DOMContentLoaded", DifferentSizes);
+function DifferentSizes() {
+ gl.getContext("2d");
+ gl.removeAttribute('height');
+ gl.toBlob(function() { }, false)
+}
+</script>
diff --git a/dom/html/crashtests/916322-2.html b/dom/html/crashtests/916322-2.html
new file mode 100644
index 0000000000..c2e5c29205
--- /dev/null
+++ b/dom/html/crashtests/916322-2.html
@@ -0,0 +1,10 @@
+<canvas height=200 id=gl>
+<script>
+// Without the fix, this would trigger an assertion in the debug build
+document.addEventListener("DOMContentLoaded", DifferentSizes);
+function DifferentSizes() {
+ gl.getContext("2d");
+ gl.removeAttribute('height');
+ gl.toBlob(function() { }, false)
+}
+</script>
diff --git a/dom/html/crashtests/978644.xhtml b/dom/html/crashtests/978644.xhtml
new file mode 100644
index 0000000000..0fd9043a28
--- /dev/null
+++ b/dom/html/crashtests/978644.xhtml
@@ -0,0 +1,11 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<body onload="document.getElementById('b').appendChild(document.getElementById('v'));">
+
+<fieldset><fieldset id="b"></fieldset></fieldset>
+
+<div id="v"><fieldset><input required="" /><input required="" /></fieldset></div>
+
+</body>
+
+</html>
diff --git a/dom/html/crashtests/crashtests.list b/dom/html/crashtests/crashtests.list
new file mode 100644
index 0000000000..be51d248f0
--- /dev/null
+++ b/dom/html/crashtests/crashtests.list
@@ -0,0 +1,99 @@
+load 68912-1.html
+load 257818-1.html
+load 285166-1.html
+load 294235-1.html
+load 307616-1.html
+load 324918-1.xhtml
+load 338649-1.xhtml
+load 339501-1.xhtml
+load 339501-2.xhtml
+load 378993-1.xhtml
+load 382568-1.html
+load 383137.xhtml
+load 388183-1.html
+load 395340-1.html
+load 399694-1.html
+load 407053.html
+load 423371-1.html
+load 448564.html
+load 451123-1.html
+load 453406-1.html
+load 464197-1.html
+load 468562-1.html
+load 468562-2.html
+load 494225.html
+load 495543.svg
+load 495546-1.html
+load 504183-1.html
+load 515829-1.html
+load 515829-2.html
+load 570566-1.html
+load 571428-1.html
+load 580507-1.xhtml
+load 590387.html
+load 596785-1.html
+load 596785-2.html
+load 602117.html
+load 604807.html
+load 605264.html
+load 606430-1.html
+load 613027.html
+load 614279.html
+load 614988-1.html
+load 620078-1.html
+load 620078-2.html
+load 631421.html
+load 673853.html
+load 682058.xhtml
+load 682460.html
+load 738744.xhtml
+load 741218.json
+load 741250.xhtml
+load 768344.html
+load 795221-1.html
+load 795221-2.html
+load 795221-3.html
+load 795221-4.html
+load 795221-5.xml
+load 811226.html
+load 819745.html
+load 828180.html
+load 828472.html
+load 837033.html
+load 838256-1.html
+load 862084.html
+load 865147.html
+load 877910.html
+load 903106.html
+load 916322-1.html
+load 916322-2.html
+load 978644.xhtml
+load 1032654.html
+load 1141260.html
+load 1228876.html
+load 1230110.html
+load 1237633.html
+load 1281972-1.html
+load 1282894.html
+load 1290904.html
+load 1343886-1.html
+load 1343886-2.xml
+load 1343886-3.xml
+load 1350972.html
+load 1386905.html
+asserts(0-4) load 1401726.html
+load 1412173.html
+load 1440523.html
+load 1547057.html
+load 1550524.html
+load 1550881-1.html
+load 1550881-2.html
+skip-if(Android) pref(dom.disable_open_during_load,false) load 1667493.html
+load 1680418.html
+load 1704660.html
+load 1724816.html
+load 1785933.html
+skip-if(Android) load 1787671.html # printPreview doesn't work on android
+load 1789475.html
+load 1801380.html
+load 1840088.html
diff --git a/dom/html/input/ButtonInputTypes.h b/dom/html/input/ButtonInputTypes.h
new file mode 100644
index 0000000000..e891db00e0
--- /dev/null
+++ b/dom/html/input/ButtonInputTypes.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ButtonInputTypes_h__
+#define mozilla_dom_ButtonInputTypes_h__
+
+#include "mozilla/dom/InputType.h"
+
+namespace mozilla::dom {
+
+class ButtonInputTypeBase : public InputType {
+ public:
+ ~ButtonInputTypeBase() override = default;
+
+ protected:
+ explicit ButtonInputTypeBase(HTMLInputElement* aInputElement)
+ : InputType(aInputElement) {}
+};
+
+// input type=button
+class ButtonInputType : public ButtonInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) ButtonInputType(aInputElement);
+ }
+
+ private:
+ explicit ButtonInputType(HTMLInputElement* aInputElement)
+ : ButtonInputTypeBase(aInputElement) {}
+};
+
+// input type=image
+class ImageInputType : public ButtonInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) ImageInputType(aInputElement);
+ }
+
+ private:
+ explicit ImageInputType(HTMLInputElement* aInputElement)
+ : ButtonInputTypeBase(aInputElement) {}
+};
+
+// input type=reset
+class ResetInputType : public ButtonInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) ResetInputType(aInputElement);
+ }
+
+ private:
+ explicit ResetInputType(HTMLInputElement* aInputElement)
+ : ButtonInputTypeBase(aInputElement) {}
+};
+
+// input type=submit
+class SubmitInputType : public ButtonInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) SubmitInputType(aInputElement);
+ }
+
+ private:
+ explicit SubmitInputType(HTMLInputElement* aInputElement)
+ : ButtonInputTypeBase(aInputElement) {}
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_ButtonInputTypes_h__ */
diff --git a/dom/html/input/CheckableInputTypes.cpp b/dom/html/input/CheckableInputTypes.cpp
new file mode 100644
index 0000000000..f9d4d69d19
--- /dev/null
+++ b/dom/html/input/CheckableInputTypes.cpp
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/CheckableInputTypes.h"
+
+#include "mozilla/dom/HTMLInputElement.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+/* input type=checkbox */
+
+bool CheckboxInputType::IsValueMissing() const {
+ if (!mInputElement->IsRequired()) {
+ return false;
+ }
+
+ return !mInputElement->Checked();
+}
+
+nsresult CheckboxInputType::GetValueMissingMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationCheckboxMissing",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+/* input type=radio */
+
+nsresult RadioInputType::GetValueMissingMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationRadioMissing",
+ mInputElement->OwnerDoc(), aMessage);
+}
diff --git a/dom/html/input/CheckableInputTypes.h b/dom/html/input/CheckableInputTypes.h
new file mode 100644
index 0000000000..98c673685b
--- /dev/null
+++ b/dom/html/input/CheckableInputTypes.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CheckableInputTypes_h__
+#define mozilla_dom_CheckableInputTypes_h__
+
+#include "mozilla/dom/InputType.h"
+
+namespace mozilla::dom {
+
+class CheckableInputTypeBase : public InputType {
+ public:
+ ~CheckableInputTypeBase() override = default;
+
+ protected:
+ explicit CheckableInputTypeBase(HTMLInputElement* aInputElement)
+ : InputType(aInputElement) {}
+};
+
+// input type=checkbox
+class CheckboxInputType : public CheckableInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) CheckboxInputType(aInputElement);
+ }
+
+ bool IsValueMissing() const override;
+
+ nsresult GetValueMissingMessage(nsAString& aMessage) override;
+
+ private:
+ explicit CheckboxInputType(HTMLInputElement* aInputElement)
+ : CheckableInputTypeBase(aInputElement) {}
+};
+
+// input type=radio
+class RadioInputType : public CheckableInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) RadioInputType(aInputElement);
+ }
+
+ nsresult GetValueMissingMessage(nsAString& aMessage) override;
+
+ private:
+ explicit RadioInputType(HTMLInputElement* aInputElement)
+ : CheckableInputTypeBase(aInputElement) {}
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_CheckableInputTypes_h__ */
diff --git a/dom/html/input/ColorInputType.h b/dom/html/input/ColorInputType.h
new file mode 100644
index 0000000000..b6749849b7
--- /dev/null
+++ b/dom/html/input/ColorInputType.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ColorInputType_h__
+#define mozilla_dom_ColorInputType_h__
+
+#include "mozilla/dom/InputType.h"
+
+namespace mozilla::dom {
+
+// input type=color
+class ColorInputType : public InputType {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) ColorInputType(aInputElement);
+ }
+
+ private:
+ explicit ColorInputType(HTMLInputElement* aInputElement)
+ : InputType(aInputElement) {}
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_ColorInputType_h__ */
diff --git a/dom/html/input/DateTimeInputTypes.cpp b/dom/html/input/DateTimeInputTypes.cpp
new file mode 100644
index 0000000000..56d6c57c49
--- /dev/null
+++ b/dom/html/input/DateTimeInputTypes.cpp
@@ -0,0 +1,501 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/DateTimeInputTypes.h"
+
+#include "js/Date.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/ShadowRoot.h"
+#include "nsDOMTokenList.h"
+
+namespace mozilla::dom {
+
+const double DateTimeInputTypeBase::kMinimumYear = 1;
+const double DateTimeInputTypeBase::kMaximumYear = 275760;
+const double DateTimeInputTypeBase::kMaximumMonthInMaximumYear = 9;
+const double DateTimeInputTypeBase::kMaximumWeekInMaximumYear = 37;
+const double DateTimeInputTypeBase::kMsPerDay = 24 * 60 * 60 * 1000;
+
+bool DateTimeInputTypeBase::IsMutable() const {
+ return !mInputElement->IsDisabledOrReadOnly();
+}
+
+bool DateTimeInputTypeBase::IsValueMissing() const {
+ if (!mInputElement->IsRequired()) {
+ return false;
+ }
+
+ if (!IsMutable()) {
+ return false;
+ }
+
+ return IsValueEmpty();
+}
+
+bool DateTimeInputTypeBase::IsRangeOverflow() const {
+ Decimal maximum = mInputElement->GetMaximum();
+ if (maximum.isNaN()) {
+ return false;
+ }
+
+ Decimal value = mInputElement->GetValueAsDecimal();
+ if (value.isNaN()) {
+ return false;
+ }
+
+ return value > maximum;
+}
+
+bool DateTimeInputTypeBase::IsRangeUnderflow() const {
+ Decimal minimum = mInputElement->GetMinimum();
+ if (minimum.isNaN()) {
+ return false;
+ }
+
+ Decimal value = mInputElement->GetValueAsDecimal();
+ if (value.isNaN()) {
+ return false;
+ }
+
+ return value < minimum;
+}
+
+bool DateTimeInputTypeBase::HasStepMismatch() const {
+ Decimal value = mInputElement->GetValueAsDecimal();
+ return mInputElement->ValueIsStepMismatch(value);
+}
+
+bool DateTimeInputTypeBase::HasBadInput() const {
+ ShadowRoot* shadow = mInputElement->GetShadowRoot();
+ if (!shadow) {
+ return false;
+ }
+
+ Element* editWrapperElement = shadow->GetElementById(u"edit-wrapper"_ns);
+ if (!editWrapperElement) {
+ return false;
+ }
+
+ bool allEmpty = true;
+ // Empty field does not imply bad input, but incomplete field does.
+ for (Element* child = editWrapperElement->GetFirstElementChild(); child;
+ child = child->GetNextElementSibling()) {
+ if (!child->ClassList()->Contains(u"datetime-edit-field"_ns)) {
+ continue;
+ }
+ nsAutoString value;
+ child->GetAttr(nsGkAtoms::value, value);
+ if (!value.IsEmpty()) {
+ allEmpty = false;
+ break;
+ }
+ }
+
+ // If some fields are available but input element's value is empty implies it
+ // has been sanitized.
+ return !allEmpty && IsValueEmpty();
+}
+
+nsresult DateTimeInputTypeBase::GetRangeOverflowMessage(nsAString& aMessage) {
+ nsAutoString maxStr;
+ mInputElement->GetAttr(nsGkAtoms::max, maxStr);
+
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationDateTimeRangeOverflow", mInputElement->OwnerDoc(), maxStr);
+}
+
+nsresult DateTimeInputTypeBase::GetRangeUnderflowMessage(nsAString& aMessage) {
+ nsAutoString minStr;
+ mInputElement->GetAttr(nsGkAtoms::min, minStr);
+
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationDateTimeRangeUnderflow", mInputElement->OwnerDoc(),
+ minStr);
+}
+
+void DateTimeInputTypeBase::MinMaxStepAttrChanged() {
+ if (Element* dateTimeBoxElement = mInputElement->GetDateTimeBoxElement()) {
+ AsyncEventDispatcher::RunDOMEventWhenSafe(
+ *dateTimeBoxElement, u"MozNotifyMinMaxStepAttrChanged"_ns,
+ CanBubble::eNo, ChromeOnlyDispatch::eNo);
+ }
+}
+
+bool DateTimeInputTypeBase::GetTimeFromMs(double aValue, uint16_t* aHours,
+ uint16_t* aMinutes,
+ uint16_t* aSeconds,
+ uint16_t* aMilliseconds) const {
+ MOZ_ASSERT(aValue >= 0 && aValue < kMsPerDay,
+ "aValue must be milliseconds within a day!");
+
+ uint32_t value = floor(aValue);
+
+ *aMilliseconds = value % 1000;
+ value /= 1000;
+
+ *aSeconds = value % 60;
+ value /= 60;
+
+ *aMinutes = value % 60;
+ value /= 60;
+
+ *aHours = value;
+
+ return true;
+}
+
+// input type=date
+
+nsresult DateInputType::GetBadInputMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidDate",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+auto DateInputType::ConvertStringToNumber(const nsAString& aValue) const
+ -> StringToNumberResult {
+ uint32_t year, month, day;
+ if (!ParseDate(aValue, &year, &month, &day)) {
+ return {};
+ }
+ JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day));
+ if (!time.isValid()) {
+ return {};
+ }
+ return {Decimal::fromDouble(time.toDouble())};
+}
+
+bool DateInputType::ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const {
+ MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
+
+ aResultString.Truncate();
+
+ // The specs (and our JS APIs) require |aValue| to be truncated.
+ aValue = aValue.floor();
+
+ double year = JS::YearFromTime(aValue.toDouble());
+ double month = JS::MonthFromTime(aValue.toDouble());
+ double day = JS::DayFromTime(aValue.toDouble());
+
+ if (std::isnan(year) || std::isnan(month) || std::isnan(day)) {
+ return false;
+ }
+
+ aResultString.AppendPrintf("%04.0f-%02.0f-%02.0f", year, month + 1, day);
+ return true;
+}
+
+// input type=time
+
+nsresult TimeInputType::GetBadInputMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidTime",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+auto TimeInputType::ConvertStringToNumber(const nsAString& aValue) const
+ -> StringToNumberResult {
+ uint32_t milliseconds;
+ if (!ParseTime(aValue, &milliseconds)) {
+ return {};
+ }
+ return {Decimal(int32_t(milliseconds))};
+}
+
+bool TimeInputType::ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const {
+ MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
+
+ aResultString.Truncate();
+
+ aValue = aValue.floor();
+ // Per spec, we need to truncate |aValue| and we should only represent
+ // times inside a day [00:00, 24:00[, which means that we should do a
+ // modulo on |aValue| using the number of milliseconds in a day (86400000).
+ uint32_t value =
+ NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble();
+
+ uint16_t milliseconds, seconds, minutes, hours;
+ if (!GetTimeFromMs(value, &hours, &minutes, &seconds, &milliseconds)) {
+ return false;
+ }
+
+ if (milliseconds != 0) {
+ aResultString.AppendPrintf("%02d:%02d:%02d.%03d", hours, minutes, seconds,
+ milliseconds);
+ } else if (seconds != 0) {
+ aResultString.AppendPrintf("%02d:%02d:%02d", hours, minutes, seconds);
+ } else {
+ aResultString.AppendPrintf("%02d:%02d", hours, minutes);
+ }
+
+ return true;
+}
+
+bool TimeInputType::HasReversedRange() const {
+ mozilla::Decimal maximum = mInputElement->GetMaximum();
+ if (maximum.isNaN()) {
+ return false;
+ }
+
+ mozilla::Decimal minimum = mInputElement->GetMinimum();
+ if (minimum.isNaN()) {
+ return false;
+ }
+
+ return maximum < minimum;
+}
+
+bool TimeInputType::IsReversedRangeUnderflowAndOverflow() const {
+ mozilla::Decimal maximum = mInputElement->GetMaximum();
+ mozilla::Decimal minimum = mInputElement->GetMinimum();
+ mozilla::Decimal value = mInputElement->GetValueAsDecimal();
+
+ MOZ_ASSERT(HasReversedRange(), "Must have reserved range.");
+
+ if (value.isNaN()) {
+ return false;
+ }
+
+ // When an element has a reversed range, and the value is more than the
+ // maximum and less than the minimum the element is simultaneously suffering
+ // from an underflow and suffering from an overflow.
+ return value > maximum && value < minimum;
+}
+
+bool TimeInputType::IsRangeOverflow() const {
+ return HasReversedRange() ? IsReversedRangeUnderflowAndOverflow()
+ : DateTimeInputTypeBase::IsRangeOverflow();
+}
+
+bool TimeInputType::IsRangeUnderflow() const {
+ return HasReversedRange() ? IsReversedRangeUnderflowAndOverflow()
+ : DateTimeInputTypeBase::IsRangeUnderflow();
+}
+
+nsresult TimeInputType::GetReversedRangeUnderflowAndOverflowMessage(
+ nsAString& aMessage) {
+ nsAutoString maxStr;
+ mInputElement->GetAttr(nsGkAtoms::max, maxStr);
+
+ nsAutoString minStr;
+ mInputElement->GetAttr(nsGkAtoms::min, minStr);
+
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationTimeReversedRangeUnderflowAndOverflow",
+ mInputElement->OwnerDoc(), minStr, maxStr);
+}
+
+nsresult TimeInputType::GetRangeOverflowMessage(nsAString& aMessage) {
+ return HasReversedRange()
+ ? GetReversedRangeUnderflowAndOverflowMessage(aMessage)
+ : DateTimeInputTypeBase::GetRangeOverflowMessage(aMessage);
+}
+
+nsresult TimeInputType::GetRangeUnderflowMessage(nsAString& aMessage) {
+ return HasReversedRange()
+ ? GetReversedRangeUnderflowAndOverflowMessage(aMessage)
+ : DateTimeInputTypeBase::GetRangeUnderflowMessage(aMessage);
+}
+
+// input type=week
+
+nsresult WeekInputType::GetBadInputMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidWeek",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+auto WeekInputType::ConvertStringToNumber(const nsAString& aValue) const
+ -> StringToNumberResult {
+ uint32_t year, week;
+ if (!ParseWeek(aValue, &year, &week)) {
+ return {};
+ }
+ if (year < kMinimumYear || year > kMaximumYear) {
+ return {};
+ }
+ // Maximum week is 275760-W37, the week of 275760-09-13.
+ if (year == kMaximumYear && week > kMaximumWeekInMaximumYear) {
+ return {};
+ }
+ double days = DaysSinceEpochFromWeek(year, week);
+ return {Decimal::fromDouble(days * kMsPerDay)};
+}
+
+bool WeekInputType::ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const {
+ MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
+
+ aResultString.Truncate();
+
+ aValue = aValue.floor();
+
+ // Based on ISO 8601 date.
+ double year = JS::YearFromTime(aValue.toDouble());
+ double month = JS::MonthFromTime(aValue.toDouble());
+ double day = JS::DayFromTime(aValue.toDouble());
+ // Adding 1 since day starts from 0.
+ double dayInYear = JS::DayWithinYear(aValue.toDouble(), year) + 1;
+
+ // Return if aValue is outside the valid JS date-time range.
+ if (std::isnan(year) || std::isnan(month) || std::isnan(day) ||
+ std::isnan(dayInYear)) {
+ return false;
+ }
+
+ // DayOfWeek requires the year to be non-negative.
+ if (year < 0) {
+ return false;
+ }
+
+ // Adding 1 since month starts from 0.
+ uint32_t isoWeekday = DayOfWeek(year, month + 1, day, true);
+ // Target on Wednesday since ISO 8601 states that week 1 is the week
+ // with the first Thursday of that year.
+ uint32_t week = (dayInYear - isoWeekday + 10) / 7;
+
+ if (week < 1) {
+ year--;
+ if (year < 1) {
+ return false;
+ }
+ week = MaximumWeekInYear(year);
+ } else if (week > MaximumWeekInYear(year)) {
+ year++;
+ if (year > kMaximumYear ||
+ (year == kMaximumYear && week > kMaximumWeekInMaximumYear)) {
+ return false;
+ }
+ week = 1;
+ }
+
+ aResultString.AppendPrintf("%04.0f-W%02d", year, week);
+ return true;
+}
+
+// input type=month
+
+nsresult MonthInputType::GetBadInputMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidMonth",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+auto MonthInputType::ConvertStringToNumber(const nsAString& aValue) const
+ -> StringToNumberResult {
+ uint32_t year, month;
+ if (!ParseMonth(aValue, &year, &month)) {
+ return {};
+ }
+
+ if (year < kMinimumYear || year > kMaximumYear) {
+ return {};
+ }
+
+ // Maximum valid month is 275760-09.
+ if (year == kMaximumYear && month > kMaximumMonthInMaximumYear) {
+ return {};
+ }
+
+ int32_t months = MonthsSinceJan1970(year, month);
+ return {Decimal(int32_t(months))};
+}
+
+bool MonthInputType::ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const {
+ MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
+
+ aResultString.Truncate();
+
+ aValue = aValue.floor();
+
+ double month = NS_floorModulo(aValue, Decimal(12)).toDouble();
+ month = (month < 0 ? month + 12 : month);
+
+ double year = 1970 + (aValue.toDouble() - month) / 12;
+
+ // Maximum valid month is 275760-09.
+ if (year < kMinimumYear || year > kMaximumYear) {
+ return false;
+ }
+
+ if (year == kMaximumYear && month > 8) {
+ return false;
+ }
+
+ aResultString.AppendPrintf("%04.0f-%02.0f", year, month + 1);
+ return true;
+}
+
+// input type=datetime-local
+
+nsresult DateTimeLocalInputType::GetBadInputMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidDateTime",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+auto DateTimeLocalInputType::ConvertStringToNumber(
+ const nsAString& aValue) const -> StringToNumberResult {
+ uint32_t year, month, day, timeInMs;
+ if (!ParseDateTimeLocal(aValue, &year, &month, &day, &timeInMs)) {
+ return {};
+ }
+ JS::ClippedTime time =
+ JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs));
+ if (!time.isValid()) {
+ return {};
+ }
+ return {Decimal::fromDouble(time.toDouble())};
+}
+
+bool DateTimeLocalInputType::ConvertNumberToString(
+ Decimal aValue, nsAString& aResultString) const {
+ MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
+
+ aResultString.Truncate();
+
+ aValue = aValue.floor();
+
+ uint32_t timeValue =
+ NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble();
+
+ uint16_t milliseconds, seconds, minutes, hours;
+ if (!GetTimeFromMs(timeValue, &hours, &minutes, &seconds, &milliseconds)) {
+ return false;
+ }
+
+ double year = JS::YearFromTime(aValue.toDouble());
+ double month = JS::MonthFromTime(aValue.toDouble());
+ double day = JS::DayFromTime(aValue.toDouble());
+
+ if (std::isnan(year) || std::isnan(month) || std::isnan(day)) {
+ return false;
+ }
+
+ if (milliseconds != 0) {
+ aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d.%03d", year,
+ month + 1, day, hours, minutes, seconds,
+ milliseconds);
+ } else if (seconds != 0) {
+ aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d", year,
+ month + 1, day, hours, minutes, seconds);
+ } else {
+ aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d", year,
+ month + 1, day, hours, minutes);
+ }
+
+ return true;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/html/input/DateTimeInputTypes.h b/dom/html/input/DateTimeInputTypes.h
new file mode 100644
index 0000000000..fa8805f67e
--- /dev/null
+++ b/dom/html/input/DateTimeInputTypes.h
@@ -0,0 +1,154 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_DateTimeInputTypes_h__
+#define mozilla_dom_DateTimeInputTypes_h__
+
+#include "mozilla/dom/InputType.h"
+
+namespace mozilla::dom {
+
+class DateTimeInputTypeBase : public InputType {
+ public:
+ ~DateTimeInputTypeBase() override = default;
+
+ bool IsValueMissing() const override;
+ bool IsRangeOverflow() const override;
+ bool IsRangeUnderflow() const override;
+ bool HasStepMismatch() const override;
+ bool HasBadInput() const override;
+
+ nsresult GetRangeOverflowMessage(nsAString& aMessage) override;
+ nsresult GetRangeUnderflowMessage(nsAString& aMessage) override;
+
+ void MinMaxStepAttrChanged() override;
+
+ protected:
+ explicit DateTimeInputTypeBase(HTMLInputElement* aInputElement)
+ : InputType(aInputElement) {}
+
+ bool IsMutable() const override;
+
+ nsresult GetBadInputMessage(nsAString& aMessage) override = 0;
+
+ /**
+ * This method converts aValue (milliseconds within a day) to hours, minutes,
+ * seconds and milliseconds.
+ */
+ bool GetTimeFromMs(double aValue, uint16_t* aHours, uint16_t* aMinutes,
+ uint16_t* aSeconds, uint16_t* aMilliseconds) const;
+
+ // Minimum year limited by HTML standard, year >= 1.
+ static const double kMinimumYear;
+ // Maximum year limited by ECMAScript date object range, year <= 275760.
+ static const double kMaximumYear;
+ // Maximum valid month is 275760-09.
+ static const double kMaximumMonthInMaximumYear;
+ // Maximum valid week is 275760-W37.
+ static const double kMaximumWeekInMaximumYear;
+ // Milliseconds in a day.
+ static const double kMsPerDay;
+};
+
+// input type=date
+class DateInputType : public DateTimeInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) DateInputType(aInputElement);
+ }
+
+ nsresult GetBadInputMessage(nsAString& aMessage) override;
+
+ StringToNumberResult ConvertStringToNumber(const nsAString&) const override;
+ bool ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const override;
+
+ private:
+ explicit DateInputType(HTMLInputElement* aInputElement)
+ : DateTimeInputTypeBase(aInputElement) {}
+};
+
+// input type=time
+class TimeInputType : public DateTimeInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) TimeInputType(aInputElement);
+ }
+
+ nsresult GetBadInputMessage(nsAString& aMessage) override;
+
+ StringToNumberResult ConvertStringToNumber(const nsAString&) const override;
+ bool ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const override;
+ bool IsRangeOverflow() const override;
+ bool IsRangeUnderflow() const override;
+ nsresult GetRangeOverflowMessage(nsAString& aMessage) override;
+ nsresult GetRangeUnderflowMessage(nsAString& aMessage) override;
+
+ private:
+ explicit TimeInputType(HTMLInputElement* aInputElement)
+ : DateTimeInputTypeBase(aInputElement) {}
+
+ // https://html.spec.whatwg.org/multipage/input.html#has-a-reversed-range
+ bool HasReversedRange() const;
+ bool IsReversedRangeUnderflowAndOverflow() const;
+ nsresult GetReversedRangeUnderflowAndOverflowMessage(nsAString& aMessage);
+};
+
+// input type=week
+class WeekInputType : public DateTimeInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) WeekInputType(aInputElement);
+ }
+
+ nsresult GetBadInputMessage(nsAString& aMessage) override;
+ StringToNumberResult ConvertStringToNumber(const nsAString&) const override;
+ bool ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const override;
+
+ private:
+ explicit WeekInputType(HTMLInputElement* aInputElement)
+ : DateTimeInputTypeBase(aInputElement) {}
+};
+
+// input type=month
+class MonthInputType : public DateTimeInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) MonthInputType(aInputElement);
+ }
+
+ nsresult GetBadInputMessage(nsAString& aMessage) override;
+ StringToNumberResult ConvertStringToNumber(const nsAString&) const override;
+ bool ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const override;
+
+ private:
+ explicit MonthInputType(HTMLInputElement* aInputElement)
+ : DateTimeInputTypeBase(aInputElement) {}
+};
+
+// input type=datetime-local
+class DateTimeLocalInputType : public DateTimeInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) DateTimeLocalInputType(aInputElement);
+ }
+
+ nsresult GetBadInputMessage(nsAString& aMessage) override;
+ StringToNumberResult ConvertStringToNumber(const nsAString&) const override;
+ bool ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const override;
+
+ private:
+ explicit DateTimeLocalInputType(HTMLInputElement* aInputElement)
+ : DateTimeInputTypeBase(aInputElement) {}
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_DateTimeInputTypes_h__ */
diff --git a/dom/html/input/FileInputType.cpp b/dom/html/input/FileInputType.cpp
new file mode 100644
index 0000000000..ed14aaa48d
--- /dev/null
+++ b/dom/html/input/FileInputType.cpp
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/FileInputType.h"
+
+#include "mozilla/dom/HTMLInputElement.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+bool FileInputType::IsValueMissing() const {
+ if (!mInputElement->IsRequired()) {
+ return false;
+ }
+
+ return mInputElement->GetFilesOrDirectoriesInternal().IsEmpty();
+}
+
+nsresult FileInputType::GetValueMissingMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationFileMissing",
+ mInputElement->OwnerDoc(), aMessage);
+}
diff --git a/dom/html/input/FileInputType.h b/dom/html/input/FileInputType.h
new file mode 100644
index 0000000000..870ef93136
--- /dev/null
+++ b/dom/html/input/FileInputType.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FileInputType_h__
+#define mozilla_dom_FileInputType_h__
+
+#include "mozilla/dom/InputType.h"
+
+namespace mozilla::dom {
+
+// input type=file
+class FileInputType : public InputType {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) FileInputType(aInputElement);
+ }
+
+ bool IsValueMissing() const override;
+
+ nsresult GetValueMissingMessage(nsAString& aMessage) override;
+
+ private:
+ explicit FileInputType(HTMLInputElement* aInputElement)
+ : InputType(aInputElement) {}
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_FileInputType_h__ */
diff --git a/dom/html/input/HiddenInputType.h b/dom/html/input/HiddenInputType.h
new file mode 100644
index 0000000000..ac7c9c571a
--- /dev/null
+++ b/dom/html/input/HiddenInputType.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_HiddenInputType_h__
+#define mozilla_dom_HiddenInputType_h__
+
+#include "mozilla/dom/InputType.h"
+
+namespace mozilla::dom {
+
+// input type=hidden
+class HiddenInputType : public InputType {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) HiddenInputType(aInputElement);
+ }
+
+ private:
+ explicit HiddenInputType(HTMLInputElement* aInputElement)
+ : InputType(aInputElement) {}
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_HiddenInputType_h__ */
diff --git a/dom/html/input/InputType.cpp b/dom/html/input/InputType.cpp
new file mode 100644
index 0000000000..1d6254c2e2
--- /dev/null
+++ b/dom/html/input/InputType.cpp
@@ -0,0 +1,354 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/InputType.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Likely.h"
+#include "nsIFormControl.h"
+#include "mozilla/dom/ButtonInputTypes.h"
+#include "mozilla/dom/CheckableInputTypes.h"
+#include "mozilla/dom/ColorInputType.h"
+#include "mozilla/dom/DateTimeInputTypes.h"
+#include "mozilla/dom/FileInputType.h"
+#include "mozilla/dom/HiddenInputType.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/NumericInputTypes.h"
+#include "mozilla/dom/SingleLineTextInputTypes.h"
+
+#include "nsContentUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+constexpr Decimal InputType::kStepAny;
+
+/* static */ UniquePtr<InputType, InputType::DoNotDelete> InputType::Create(
+ HTMLInputElement* aInputElement, FormControlType aType, void* aMemory) {
+ UniquePtr<InputType, InputType::DoNotDelete> inputType;
+ switch (aType) {
+ // Single line text
+ case FormControlType::InputText:
+ inputType.reset(TextInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputTel:
+ inputType.reset(TelInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputEmail:
+ inputType.reset(EmailInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputSearch:
+ inputType.reset(SearchInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputPassword:
+ inputType.reset(PasswordInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputUrl:
+ inputType.reset(URLInputType::Create(aInputElement, aMemory));
+ break;
+ // Button
+ case FormControlType::InputButton:
+ inputType.reset(ButtonInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputSubmit:
+ inputType.reset(SubmitInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputImage:
+ inputType.reset(ImageInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputReset:
+ inputType.reset(ResetInputType::Create(aInputElement, aMemory));
+ break;
+ // Checkable
+ case FormControlType::InputCheckbox:
+ inputType.reset(CheckboxInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputRadio:
+ inputType.reset(RadioInputType::Create(aInputElement, aMemory));
+ break;
+ // Numeric
+ case FormControlType::InputNumber:
+ inputType.reset(NumberInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputRange:
+ inputType.reset(RangeInputType::Create(aInputElement, aMemory));
+ break;
+ // DateTime
+ case FormControlType::InputDate:
+ inputType.reset(DateInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputTime:
+ inputType.reset(TimeInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputMonth:
+ inputType.reset(MonthInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputWeek:
+ inputType.reset(WeekInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputDatetimeLocal:
+ inputType.reset(DateTimeLocalInputType::Create(aInputElement, aMemory));
+ break;
+ // Others
+ case FormControlType::InputColor:
+ inputType.reset(ColorInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputFile:
+ inputType.reset(FileInputType::Create(aInputElement, aMemory));
+ break;
+ case FormControlType::InputHidden:
+ inputType.reset(HiddenInputType::Create(aInputElement, aMemory));
+ break;
+ default:
+ inputType.reset(TextInputType::Create(aInputElement, aMemory));
+ }
+
+ return inputType;
+}
+
+bool InputType::IsMutable() const { return !mInputElement->IsDisabled(); }
+
+bool InputType::IsValueEmpty() const { return mInputElement->IsValueEmpty(); }
+
+void InputType::GetNonFileValueInternal(nsAString& aValue) const {
+ return mInputElement->GetNonFileValueInternal(aValue);
+}
+
+nsresult InputType::SetValueInternal(const nsAString& aValue,
+ const ValueSetterOptions& aOptions) {
+ RefPtr<HTMLInputElement> inputElement(mInputElement);
+ return inputElement->SetValueInternal(aValue, aOptions);
+}
+
+nsIFrame* InputType::GetPrimaryFrame() const {
+ return mInputElement->GetPrimaryFrame();
+}
+
+void InputType::DropReference() {
+ // Drop our (non ref-counted) reference.
+ mInputElement = nullptr;
+}
+
+bool InputType::IsTooLong() const { return false; }
+
+bool InputType::IsTooShort() const { return false; }
+
+bool InputType::IsValueMissing() const { return false; }
+
+bool InputType::HasTypeMismatch() const { return false; }
+
+Maybe<bool> InputType::HasPatternMismatch() const { return Some(false); }
+
+bool InputType::IsRangeOverflow() const { return false; }
+
+bool InputType::IsRangeUnderflow() const { return false; }
+
+bool InputType::HasStepMismatch() const { return false; }
+
+bool InputType::HasBadInput() const { return false; }
+
+nsresult InputType::GetValidationMessage(
+ nsAString& aValidationMessage,
+ nsIConstraintValidation::ValidityStateType aType) {
+ aValidationMessage.Truncate();
+
+ switch (aType) {
+ case nsIConstraintValidation::VALIDITY_STATE_TOO_LONG: {
+ int32_t maxLength = mInputElement->MaxLength();
+ int32_t textLength = mInputElement->InputTextLength(CallerType::System);
+ nsAutoString strMaxLength;
+ nsAutoString strTextLength;
+
+ strMaxLength.AppendInt(maxLength);
+ strTextLength.AppendInt(textLength);
+
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aValidationMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationTextTooLong", mInputElement->OwnerDoc(), strMaxLength,
+ strTextLength);
+ }
+ case nsIConstraintValidation::VALIDITY_STATE_TOO_SHORT: {
+ int32_t minLength = mInputElement->MinLength();
+ int32_t textLength = mInputElement->InputTextLength(CallerType::System);
+ nsAutoString strMinLength;
+ nsAutoString strTextLength;
+
+ strMinLength.AppendInt(minLength);
+ strTextLength.AppendInt(textLength);
+
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aValidationMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationTextTooShort", mInputElement->OwnerDoc(), strMinLength,
+ strTextLength);
+ }
+ case nsIConstraintValidation::VALIDITY_STATE_VALUE_MISSING:
+ return GetValueMissingMessage(aValidationMessage);
+ case nsIConstraintValidation::VALIDITY_STATE_TYPE_MISMATCH: {
+ return GetTypeMismatchMessage(aValidationMessage);
+ }
+ case nsIConstraintValidation::VALIDITY_STATE_PATTERN_MISMATCH: {
+ nsAutoString title;
+ mInputElement->GetAttr(nsGkAtoms::title, title);
+
+ if (title.IsEmpty()) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationPatternMismatch",
+ mInputElement->OwnerDoc(), aValidationMessage);
+ }
+
+ if (title.Length() >
+ nsIConstraintValidation::sContentSpecifiedMaxLengthMessage) {
+ title.Truncate(
+ nsIConstraintValidation::sContentSpecifiedMaxLengthMessage);
+ }
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aValidationMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationPatternMismatchWithTitle", mInputElement->OwnerDoc(),
+ title);
+ }
+ case nsIConstraintValidation::VALIDITY_STATE_RANGE_OVERFLOW:
+ return GetRangeOverflowMessage(aValidationMessage);
+ case nsIConstraintValidation::VALIDITY_STATE_RANGE_UNDERFLOW:
+ return GetRangeUnderflowMessage(aValidationMessage);
+ case nsIConstraintValidation::VALIDITY_STATE_STEP_MISMATCH: {
+ Decimal value = mInputElement->GetValueAsDecimal();
+ if (MOZ_UNLIKELY(NS_WARN_IF(value.isNaN()))) {
+ // TODO(bug 1651070): This should ideally never happen, but we don't
+ // deal with lang changes correctly, so it could.
+ return GetBadInputMessage(aValidationMessage);
+ }
+
+ Decimal step = mInputElement->GetStep();
+ MOZ_ASSERT(step != kStepAny && step > Decimal(0));
+
+ Decimal stepBase = mInputElement->GetStepBase();
+
+ Decimal valueLow = value - NS_floorModulo(value - stepBase, step);
+ Decimal valueHigh = value + step - NS_floorModulo(value - stepBase, step);
+
+ Decimal maximum = mInputElement->GetMaximum();
+
+ if (maximum.isNaN() || valueHigh <= maximum) {
+ nsAutoString valueLowStr, valueHighStr;
+ ConvertNumberToString(valueLow, valueLowStr);
+ ConvertNumberToString(valueHigh, valueHighStr);
+
+ if (valueLowStr.Equals(valueHighStr)) {
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aValidationMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationStepMismatchOneValue", mInputElement->OwnerDoc(),
+ valueLowStr);
+ }
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aValidationMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationStepMismatch", mInputElement->OwnerDoc(),
+ valueLowStr, valueHighStr);
+ }
+
+ nsAutoString valueLowStr;
+ ConvertNumberToString(valueLow, valueLowStr);
+
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aValidationMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationStepMismatchOneValue", mInputElement->OwnerDoc(),
+ valueLowStr);
+ }
+ case nsIConstraintValidation::VALIDITY_STATE_BAD_INPUT:
+ return GetBadInputMessage(aValidationMessage);
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown validity state");
+ return NS_ERROR_UNEXPECTED;
+ }
+}
+
+nsresult InputType::GetValueMissingMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationValueMissing",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+nsresult InputType::GetTypeMismatchMessage(nsAString& aMessage) {
+ return NS_ERROR_UNEXPECTED;
+}
+
+nsresult InputType::GetRangeOverflowMessage(nsAString& aMessage) {
+ return NS_ERROR_UNEXPECTED;
+}
+
+nsresult InputType::GetRangeUnderflowMessage(nsAString& aMessage) {
+ return NS_ERROR_UNEXPECTED;
+}
+
+nsresult InputType::GetBadInputMessage(nsAString& aMessage) {
+ return NS_ERROR_UNEXPECTED;
+}
+
+auto InputType::ConvertStringToNumber(const nsAString& aValue) const
+ -> StringToNumberResult {
+ NS_WARNING("InputType::ConvertStringToNumber called");
+ return {};
+}
+
+bool InputType::ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const {
+ NS_WARNING("InputType::ConvertNumberToString called");
+
+ return false;
+}
+
+bool InputType::ParseDate(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth, uint32_t* aDay) const {
+ // TODO: move this function and implementation to DateTimeInpuTypeBase when
+ // refactoring is completed. Now we can only call HTMLInputElement::ParseDate
+ // from here, since the method is protected and only InputType is a friend
+ // class.
+ return mInputElement->ParseDate(aValue, aYear, aMonth, aDay);
+}
+
+bool InputType::ParseTime(const nsAString& aValue, uint32_t* aResult) const {
+ // see comment in InputType::ParseDate().
+ return HTMLInputElement::ParseTime(aValue, aResult);
+}
+
+bool InputType::ParseMonth(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth) const {
+ // see comment in InputType::ParseDate().
+ return mInputElement->ParseMonth(aValue, aYear, aMonth);
+}
+
+bool InputType::ParseWeek(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aWeek) const {
+ // see comment in InputType::ParseDate().
+ return mInputElement->ParseWeek(aValue, aYear, aWeek);
+}
+
+bool InputType::ParseDateTimeLocal(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth, uint32_t* aDay,
+ uint32_t* aTime) const {
+ // see comment in InputType::ParseDate().
+ return mInputElement->ParseDateTimeLocal(aValue, aYear, aMonth, aDay, aTime);
+}
+
+int32_t InputType::MonthsSinceJan1970(uint32_t aYear, uint32_t aMonth) const {
+ // see comment in InputType::ParseDate().
+ return mInputElement->MonthsSinceJan1970(aYear, aMonth);
+}
+
+double InputType::DaysSinceEpochFromWeek(uint32_t aYear, uint32_t aWeek) const {
+ // see comment in InputType::ParseDate().
+ return mInputElement->DaysSinceEpochFromWeek(aYear, aWeek);
+}
+
+uint32_t InputType::DayOfWeek(uint32_t aYear, uint32_t aMonth, uint32_t aDay,
+ bool isoWeek) const {
+ // see comment in InputType::ParseDate().
+ return mInputElement->DayOfWeek(aYear, aMonth, aDay, isoWeek);
+}
+
+uint32_t InputType::MaximumWeekInYear(uint32_t aYear) const {
+ // see comment in InputType::ParseDate().
+ return mInputElement->MaximumWeekInYear(aYear);
+}
diff --git a/dom/html/input/InputType.h b/dom/html/input/InputType.h
new file mode 100644
index 0000000000..a3599feadd
--- /dev/null
+++ b/dom/html/input/InputType.h
@@ -0,0 +1,240 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_InputType_h__
+#define mozilla_dom_InputType_h__
+
+#include <stdint.h>
+#include "mozilla/Decimal.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TextControlState.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIConstraintValidation.h"
+#include "nsString.h"
+#include "nsError.h"
+
+// This must come outside of any namespace, or else it won't overload with the
+// double based version in nsMathUtils.h
+inline mozilla::Decimal NS_floorModulo(mozilla::Decimal x, mozilla::Decimal y) {
+ return (x - y * (x / y).floor());
+}
+
+class nsIFrame;
+
+namespace mozilla::dom {
+class HTMLInputElement;
+
+/**
+ * A common superclass for different types of a HTMLInputElement.
+ */
+class InputType {
+ public:
+ using ValueSetterOption = TextControlState::ValueSetterOption;
+ using ValueSetterOptions = TextControlState::ValueSetterOptions;
+
+ // Custom deleter for UniquePtr<InputType> to avoid freeing memory
+ // pre-allocated for InputType, but we still need to call the destructor
+ // explictly.
+ struct DoNotDelete {
+ void operator()(InputType* p) { p->~InputType(); }
+ };
+
+ static UniquePtr<InputType, DoNotDelete> Create(
+ HTMLInputElement* aInputElement, FormControlType, void* aMemory);
+
+ virtual ~InputType() = default;
+
+ // Float value returned by GetStep() when the step attribute is set to 'any'.
+ static constexpr Decimal kStepAny = Decimal(0_d);
+
+ /**
+ * Drop the reference to the input element.
+ */
+ void DropReference();
+
+ virtual bool MinAndMaxLengthApply() const { return false; }
+ virtual bool IsTooLong() const;
+ virtual bool IsTooShort() const;
+ virtual bool IsValueMissing() const;
+ virtual bool HasTypeMismatch() const;
+ // May return Nothing() if the JS engine failed to evaluate the regex.
+ virtual Maybe<bool> HasPatternMismatch() const;
+ virtual bool IsRangeOverflow() const;
+ virtual bool IsRangeUnderflow() const;
+ virtual bool HasStepMismatch() const;
+ virtual bool HasBadInput() const;
+
+ nsresult GetValidationMessage(
+ nsAString& aValidationMessage,
+ nsIConstraintValidation::ValidityStateType aType);
+ virtual nsresult GetValueMissingMessage(nsAString& aMessage);
+ virtual nsresult GetTypeMismatchMessage(nsAString& aMessage);
+ virtual nsresult GetRangeOverflowMessage(nsAString& aMessage);
+ virtual nsresult GetRangeUnderflowMessage(nsAString& aMessage);
+ virtual nsresult GetBadInputMessage(nsAString& aMessage);
+
+ MOZ_CAN_RUN_SCRIPT virtual void MinMaxStepAttrChanged() {}
+
+ /**
+ * Convert a string to a Decimal number in a type specific way,
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#concept-input-value-string-number
+ * ie parse a date string to a timestamp if type=date,
+ * or parse a number string to its value if type=number.
+ * @param aValue the string to be parsed.
+ */
+ struct StringToNumberResult {
+ // The result decimal. Successfully parsed if it's finite.
+ Decimal mResult = Decimal::nan();
+ // Whether the result required reading locale-dependent data (for input
+ // type=number), or the value parses using the regular HTML rules.
+ bool mLocalized = false;
+ };
+ virtual StringToNumberResult ConvertStringToNumber(
+ const nsAString& aValue) const;
+
+ /**
+ * Convert a Decimal to a string in a type specific way, ie convert a
+ * timestamp to a date string if type=date or append the number string
+ * representing the value if type=number.
+ *
+ * @param aValue the Decimal to be converted
+ * @param aResultString [out] the string representing the Decimal
+ * @return whether the function succeeded, it will fail if the current input's
+ * type is not supported or the number can't be converted to a string
+ * as expected by the type.
+ */
+ virtual bool ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const;
+
+ protected:
+ explicit InputType(HTMLInputElement* aInputElement)
+ : mInputElement(aInputElement) {}
+
+ /**
+ * Get the mutable state of the element.
+ * When the element isn't mutable (immutable), the value or checkedness
+ * should not be changed by the user.
+ *
+ * See:
+ * https://html.spec.whatwg.org/multipage/forms.html#the-input-element:concept-fe-mutable
+ */
+ virtual bool IsMutable() const;
+
+ /**
+ * Returns whether the input element's current value is the empty string.
+ * This only makes sense for some input types; does NOT make sense for file
+ * inputs.
+ *
+ * @return whether the input element's current value is the empty string.
+ */
+ bool IsValueEmpty() const;
+
+ // A getter for callers that know we're not dealing with a file input, so they
+ // don't have to think about the caller type.
+ void GetNonFileValueInternal(nsAString& aValue) const;
+
+ /**
+ * Setting the input element's value.
+ *
+ * @param aValue String to set.
+ * @param aOptions See TextControlState::ValueSetterOption.
+ */
+ MOZ_CAN_RUN_SCRIPT nsresult
+ SetValueInternal(const nsAString& aValue, const ValueSetterOptions& aOptions);
+
+ /**
+ * Get the primary frame for the input element.
+ */
+ nsIFrame* GetPrimaryFrame() const;
+
+ /**
+ * Parse a date string of the form yyyy-mm-dd
+ *
+ * @param aValue the string to be parsed.
+ * @return the date in aYear, aMonth, aDay.
+ * @return whether the parsing was successful.
+ */
+ bool ParseDate(const nsAString& aValue, uint32_t* aYear, uint32_t* aMonth,
+ uint32_t* aDay) const;
+
+ /**
+ * Returns the time expressed in milliseconds of |aValue| being parsed as a
+ * time following the HTML specifications:
+ * https://html.spec.whatwg.org/multipage/infrastructure.html#parse-a-time-string
+ *
+ * Note: |aResult| can be null.
+ *
+ * @param aValue the string to be parsed.
+ * @param aResult the time expressed in milliseconds representing the time
+ * [out]
+ * @return whether the parsing was successful.
+ */
+ bool ParseTime(const nsAString& aValue, uint32_t* aResult) const;
+
+ /**
+ * Parse a month string of the form yyyy-mm
+ *
+ * @param the string to be parsed.
+ * @return the year and month in aYear and aMonth.
+ * @return whether the parsing was successful.
+ */
+ bool ParseMonth(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth) const;
+
+ /**
+ * Parse a week string of the form yyyy-Www
+ *
+ * @param the string to be parsed.
+ * @return the year and week in aYear and aWeek.
+ * @return whether the parsing was successful.
+ */
+ bool ParseWeek(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aWeek) const;
+
+ /**
+ * Parse a datetime-local string of the form yyyy-mm-ddThh:mm[:ss.s] or
+ * yyyy-mm-dd hh:mm[:ss.s], where fractions of seconds can be 1 to 3 digits.
+ *
+ * @param the string to be parsed.
+ * @return the date in aYear, aMonth, aDay and time expressed in milliseconds
+ * in aTime.
+ * @return whether the parsing was successful.
+ */
+ bool ParseDateTimeLocal(const nsAString& aValue, uint32_t* aYear,
+ uint32_t* aMonth, uint32_t* aDay,
+ uint32_t* aTime) const;
+
+ /**
+ * This methods returns the number of months between January 1970 and the
+ * given year and month.
+ */
+ int32_t MonthsSinceJan1970(uint32_t aYear, uint32_t aMonth) const;
+
+ /**
+ * This methods returns the number of days since epoch for a given year and
+ * week.
+ */
+ double DaysSinceEpochFromWeek(uint32_t aYear, uint32_t aWeek) const;
+
+ /**
+ * This methods returns the day of the week given a date. If @isoWeek is true,
+ * 7=Sunday, otherwise, 0=Sunday.
+ */
+ uint32_t DayOfWeek(uint32_t aYear, uint32_t aMonth, uint32_t aDay,
+ bool isoWeek) const;
+
+ /**
+ * This methods returns the maximum number of week in a given year, the
+ * result is either 52 or 53.
+ */
+ uint32_t MaximumWeekInYear(uint32_t aYear) const;
+
+ HTMLInputElement* mInputElement;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_InputType_h__ */
diff --git a/dom/html/input/NumericInputTypes.cpp b/dom/html/input/NumericInputTypes.cpp
new file mode 100644
index 0000000000..93941b30f7
--- /dev/null
+++ b/dom/html/input/NumericInputTypes.cpp
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/NumericInputTypes.h"
+
+#include "mozilla/TextControlState.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "ICUUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+bool NumericInputTypeBase::IsRangeOverflow() const {
+ Decimal maximum = mInputElement->GetMaximum();
+ if (maximum.isNaN()) {
+ return false;
+ }
+
+ Decimal value = mInputElement->GetValueAsDecimal();
+ if (value.isNaN()) {
+ return false;
+ }
+
+ return value > maximum;
+}
+
+bool NumericInputTypeBase::IsRangeUnderflow() const {
+ Decimal minimum = mInputElement->GetMinimum();
+ if (minimum.isNaN()) {
+ return false;
+ }
+
+ Decimal value = mInputElement->GetValueAsDecimal();
+ if (value.isNaN()) {
+ return false;
+ }
+
+ return value < minimum;
+}
+
+bool NumericInputTypeBase::HasStepMismatch() const {
+ Decimal value = mInputElement->GetValueAsDecimal();
+ return mInputElement->ValueIsStepMismatch(value);
+}
+
+nsresult NumericInputTypeBase::GetRangeOverflowMessage(nsAString& aMessage) {
+ // We want to show the value as parsed when it's a number
+ Decimal maximum = mInputElement->GetMaximum();
+ MOZ_ASSERT(!maximum.isNaN());
+
+ nsAutoString maxStr;
+ ConvertNumberToString(maximum, maxStr);
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationNumberRangeOverflow", mInputElement->OwnerDoc(), maxStr);
+}
+
+nsresult NumericInputTypeBase::GetRangeUnderflowMessage(nsAString& aMessage) {
+ Decimal minimum = mInputElement->GetMinimum();
+ MOZ_ASSERT(!minimum.isNaN());
+
+ nsAutoString minStr;
+ ConvertNumberToString(minimum, minStr);
+ return nsContentUtils::FormatMaybeLocalizedString(
+ aMessage, nsContentUtils::eDOM_PROPERTIES,
+ "FormValidationNumberRangeUnderflow", mInputElement->OwnerDoc(), minStr);
+}
+
+auto NumericInputTypeBase::ConvertStringToNumber(const nsAString& aValue) const
+ -> StringToNumberResult {
+ return {HTMLInputElement::StringToDecimal(aValue)};
+}
+
+bool NumericInputTypeBase::ConvertNumberToString(
+ Decimal aValue, nsAString& aResultString) const {
+ MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
+
+ aResultString.Truncate();
+
+ char buf[32];
+ bool ok = aValue.toString(buf, ArrayLength(buf));
+ aResultString.AssignASCII(buf);
+ MOZ_ASSERT(ok, "buf not big enough");
+
+ return ok;
+}
+
+/* input type=number */
+
+bool NumberInputType::IsValueMissing() const {
+ if (!mInputElement->IsRequired()) {
+ return false;
+ }
+
+ if (!IsMutable()) {
+ return false;
+ }
+
+ return IsValueEmpty();
+}
+
+bool NumberInputType::HasBadInput() const {
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ return !value.IsEmpty() && mInputElement->GetValueAsDecimal().isNaN();
+}
+
+auto NumberInputType::ConvertStringToNumber(const nsAString& aValue) const
+ -> StringToNumberResult {
+ auto result = NumericInputTypeBase::ConvertStringToNumber(aValue);
+ if (result.mResult.isFinite()) {
+ return result;
+ }
+ // Try to read the localized value from the user.
+ ICUUtils::LanguageTagIterForContent langTagIter(mInputElement);
+ result.mLocalized = true;
+ result.mResult =
+ Decimal::fromDouble(ICUUtils::ParseNumber(aValue, langTagIter));
+ return result;
+}
+
+bool NumberInputType::ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const {
+ MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
+
+ aResultString.Truncate();
+ ICUUtils::LanguageTagIterForContent langTagIter(mInputElement);
+ ICUUtils::LocalizeNumber(aValue.toDouble(), langTagIter, aResultString);
+ return true;
+}
+
+nsresult NumberInputType::GetValueMissingMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationBadInputNumber",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+nsresult NumberInputType::GetBadInputMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationBadInputNumber",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+bool NumberInputType::IsMutable() const {
+ return !mInputElement->IsDisabledOrReadOnly();
+}
+
+/* input type=range */
+void RangeInputType::MinMaxStepAttrChanged() {
+ // The value may need to change when @min/max/step changes since the value may
+ // have been invalid and can now change to a valid value, or vice versa. For
+ // example, consider: <input type=range value=-1 max=1 step=3>. The valid
+ // range is 0 to 1 while the nearest valid steps are -1 and 2 (the max value
+ // having prevented there being a valid step in range). Changing @max to/from
+ // 1 and a number greater than on equal to 3 should change whether we have a
+ // step mismatch or not.
+ // The value may also need to change between a value that results in a step
+ // mismatch and a value that results in overflow. For example, if @max in the
+ // example above were to change from 1 to -1.
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+ SetValueInternal(value, TextControlState::ValueSetterOption::ByInternalAPI);
+}
diff --git a/dom/html/input/NumericInputTypes.h b/dom/html/input/NumericInputTypes.h
new file mode 100644
index 0000000000..a541961f8d
--- /dev/null
+++ b/dom/html/input/NumericInputTypes.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_NumericInputTypes_h__
+#define mozilla_dom_NumericInputTypes_h__
+
+#include "mozilla/dom/InputType.h"
+
+namespace mozilla::dom {
+
+class NumericInputTypeBase : public InputType {
+ public:
+ ~NumericInputTypeBase() override = default;
+
+ bool IsRangeOverflow() const override;
+ bool IsRangeUnderflow() const override;
+ bool HasStepMismatch() const override;
+
+ nsresult GetRangeOverflowMessage(nsAString& aMessage) override;
+ nsresult GetRangeUnderflowMessage(nsAString& aMessage) override;
+
+ StringToNumberResult ConvertStringToNumber(
+ const nsAString& aValue) const override;
+ bool ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const override;
+
+ protected:
+ explicit NumericInputTypeBase(HTMLInputElement* aInputElement)
+ : InputType(aInputElement) {}
+};
+
+// input type=number
+class NumberInputType final : public NumericInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) NumberInputType(aInputElement);
+ }
+
+ bool IsValueMissing() const override;
+ bool HasBadInput() const override;
+
+ nsresult GetValueMissingMessage(nsAString& aMessage) override;
+ nsresult GetBadInputMessage(nsAString& aMessage) override;
+
+ StringToNumberResult ConvertStringToNumber(const nsAString&) const override;
+ bool ConvertNumberToString(Decimal aValue,
+ nsAString& aResultString) const override;
+
+ protected:
+ bool IsMutable() const override;
+
+ private:
+ explicit NumberInputType(HTMLInputElement* aInputElement)
+ : NumericInputTypeBase(aInputElement) {}
+};
+
+// input type=range
+class RangeInputType : public NumericInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) RangeInputType(aInputElement);
+ }
+
+ MOZ_CAN_RUN_SCRIPT void MinMaxStepAttrChanged() override;
+
+ private:
+ explicit RangeInputType(HTMLInputElement* aInputElement)
+ : NumericInputTypeBase(aInputElement) {}
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_NumericInputTypes_h__ */
diff --git a/dom/html/input/SingleLineTextInputTypes.cpp b/dom/html/input/SingleLineTextInputTypes.cpp
new file mode 100644
index 0000000000..18cb12f520
--- /dev/null
+++ b/dom/html/input/SingleLineTextInputTypes.cpp
@@ -0,0 +1,289 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/SingleLineTextInputTypes.h"
+
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/TextUtils.h"
+#include "HTMLSplitOnSpacesTokenizer.h"
+#include "nsContentUtils.h"
+#include "nsCRTGlue.h"
+#include "nsIIDNService.h"
+#include "nsIIOService.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+bool SingleLineTextInputTypeBase::IsMutable() const {
+ return !mInputElement->IsDisabledOrReadOnly();
+}
+
+bool SingleLineTextInputTypeBase::IsTooLong() const {
+ int32_t maxLength = mInputElement->MaxLength();
+
+ // Maxlength of -1 means attribute isn't set or parsing error.
+ if (maxLength == -1) {
+ return false;
+ }
+
+ int32_t textLength = mInputElement->InputTextLength(CallerType::System);
+
+ return textLength > maxLength;
+}
+
+bool SingleLineTextInputTypeBase::IsTooShort() const {
+ int32_t minLength = mInputElement->MinLength();
+
+ // Minlength of -1 means attribute isn't set or parsing error.
+ if (minLength == -1) {
+ return false;
+ }
+
+ int32_t textLength = mInputElement->InputTextLength(CallerType::System);
+
+ return textLength && textLength < minLength;
+}
+
+bool SingleLineTextInputTypeBase::IsValueMissing() const {
+ if (!mInputElement->IsRequired()) {
+ return false;
+ }
+
+ if (!IsMutable()) {
+ return false;
+ }
+
+ return IsValueEmpty();
+}
+
+Maybe<bool> SingleLineTextInputTypeBase::HasPatternMismatch() const {
+ if (!mInputElement->HasPatternAttribute()) {
+ return Some(false);
+ }
+
+ nsAutoString pattern;
+ if (!mInputElement->GetAttr(nsGkAtoms::pattern, pattern)) {
+ return Some(false);
+ }
+
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+
+ if (value.IsEmpty()) {
+ return Some(false);
+ }
+
+ Document* doc = mInputElement->OwnerDoc();
+ Maybe<bool> result = nsContentUtils::IsPatternMatching(
+ value, std::move(pattern), doc,
+ mInputElement->HasAttr(nsGkAtoms::multiple));
+ return result ? Some(!*result) : Nothing();
+}
+
+/* input type=url */
+
+bool URLInputType::HasTypeMismatch() const {
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+
+ if (value.IsEmpty()) {
+ return false;
+ }
+
+ /**
+ * TODO:
+ * The URL is not checked as the HTML5 specifications want it to be because
+ * there is no code to check for a valid URI/IRI according to 3986 and 3987
+ * RFC's at the moment, see bug 561586.
+ *
+ * RFC 3987 (IRI) implementation: bug 42899
+ *
+ * HTML5 specifications:
+ * http://dev.w3.org/html5/spec/infrastructure.html#valid-url
+ */
+ nsCOMPtr<nsIIOService> ioService = do_GetIOService();
+ nsCOMPtr<nsIURI> uri;
+
+ return !NS_SUCCEEDED(ioService->NewURI(NS_ConvertUTF16toUTF8(value), nullptr,
+ nullptr, getter_AddRefs(uri)));
+}
+
+nsresult URLInputType::GetTypeMismatchMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidURL",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+/* input type=email */
+
+bool EmailInputType::HasTypeMismatch() const {
+ nsAutoString value;
+ GetNonFileValueInternal(value);
+
+ if (value.IsEmpty()) {
+ return false;
+ }
+
+ return mInputElement->HasAttr(nsGkAtoms::multiple)
+ ? !IsValidEmailAddressList(value)
+ : !IsValidEmailAddress(value);
+}
+
+bool EmailInputType::HasBadInput() const {
+ // With regards to suffering from bad input the spec says that only the
+ // punycode conversion works, so we don't care whether the email address is
+ // valid or not here. (If the email address is invalid then we will be
+ // suffering from a type mismatch.)
+ nsAutoString value;
+ nsAutoCString unused;
+ uint32_t unused2;
+ GetNonFileValueInternal(value);
+ HTMLSplitOnSpacesTokenizer tokenizer(value, ',');
+ while (tokenizer.hasMoreTokens()) {
+ if (!PunycodeEncodeEmailAddress(tokenizer.nextToken(), unused, &unused2)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+nsresult EmailInputType::GetTypeMismatchMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidEmail",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+nsresult EmailInputType::GetBadInputMessage(nsAString& aMessage) {
+ return nsContentUtils::GetMaybeLocalizedString(
+ nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidEmail",
+ mInputElement->OwnerDoc(), aMessage);
+}
+
+/* static */
+bool EmailInputType::IsValidEmailAddressList(const nsAString& aValue) {
+ HTMLSplitOnSpacesTokenizer tokenizer(aValue, ',');
+
+ while (tokenizer.hasMoreTokens()) {
+ if (!IsValidEmailAddress(tokenizer.nextToken())) {
+ return false;
+ }
+ }
+
+ return !tokenizer.separatorAfterCurrentToken();
+}
+
+/* static */
+bool EmailInputType::IsValidEmailAddress(const nsAString& aValue) {
+ // Email addresses can't be empty and can't end with a '.' or '-'.
+ if (aValue.IsEmpty() || aValue.Last() == '.' || aValue.Last() == '-') {
+ return false;
+ }
+
+ uint32_t atPos;
+ nsAutoCString value;
+ if (!PunycodeEncodeEmailAddress(aValue, value, &atPos) ||
+ atPos == (uint32_t)kNotFound || atPos == 0 ||
+ atPos == value.Length() - 1) {
+ // Could not encode, or "@" was not found, or it was at the start or end
+ // of the input - in all cases, not a valid email address.
+ return false;
+ }
+
+ uint32_t length = value.Length();
+ uint32_t i = 0;
+
+ // Parsing the username.
+ for (; i < atPos; ++i) {
+ char16_t c = value[i];
+
+ // The username characters have to be in this list to be valid.
+ if (!(IsAsciiAlpha(c) || IsAsciiDigit(c) || c == '.' || c == '!' ||
+ c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' ||
+ c == '*' || c == '+' || c == '-' || c == '/' || c == '=' ||
+ c == '?' || c == '^' || c == '_' || c == '`' || c == '{' ||
+ c == '|' || c == '}' || c == '~')) {
+ return false;
+ }
+ }
+
+ // Skip the '@'.
+ ++i;
+
+ // The domain name can't begin with a dot or a dash.
+ if (value[i] == '.' || value[i] == '-') {
+ return false;
+ }
+
+ // Parsing the domain name.
+ for (; i < length; ++i) {
+ char16_t c = value[i];
+
+ if (c == '.') {
+ // A dot can't follow a dot or a dash.
+ if (value[i - 1] == '.' || value[i - 1] == '-') {
+ return false;
+ }
+ } else if (c == '-') {
+ // A dash can't follow a dot.
+ if (value[i - 1] == '.') {
+ return false;
+ }
+ } else if (!(IsAsciiAlpha(c) || IsAsciiDigit(c) || c == '-')) {
+ // The domain characters have to be in this list to be valid.
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/* static */
+bool EmailInputType::PunycodeEncodeEmailAddress(const nsAString& aEmail,
+ nsAutoCString& aEncodedEmail,
+ uint32_t* aIndexOfAt) {
+ nsAutoCString value = NS_ConvertUTF16toUTF8(aEmail);
+ *aIndexOfAt = (uint32_t)value.FindChar('@');
+
+ if (*aIndexOfAt == (uint32_t)kNotFound || *aIndexOfAt == value.Length() - 1) {
+ aEncodedEmail = value;
+ return true;
+ }
+
+ nsCOMPtr<nsIIDNService> idnSrv = do_GetService(NS_IDNSERVICE_CONTRACTID);
+ if (!idnSrv) {
+ NS_ERROR("nsIIDNService isn't present!");
+ return false;
+ }
+
+ uint32_t indexOfDomain = *aIndexOfAt + 1;
+
+ const nsDependentCSubstring domain = Substring(value, indexOfDomain);
+ bool ace;
+ if (NS_SUCCEEDED(idnSrv->IsACE(domain, &ace)) && !ace) {
+ nsAutoCString domainACE;
+ if (NS_FAILED(idnSrv->ConvertUTF8toACE(domain, domainACE))) {
+ return false;
+ }
+
+ // Bug 1788115 removed the 63 character limit from the
+ // IDNService::ConvertUTF8toACE so we check for that limit here as required
+ // by the spec: https://html.spec.whatwg.org/#valid-e-mail-address
+ nsCCharSeparatedTokenizer tokenizer(domainACE, '.');
+ while (tokenizer.hasMoreTokens()) {
+ if (tokenizer.nextToken().Length() > 63) {
+ return false;
+ }
+ }
+
+ value.Replace(indexOfDomain, domain.Length(), domainACE);
+ }
+
+ aEncodedEmail = value;
+ return true;
+}
diff --git a/dom/html/input/SingleLineTextInputTypes.h b/dom/html/input/SingleLineTextInputTypes.h
new file mode 100644
index 0000000000..afacc917a3
--- /dev/null
+++ b/dom/html/input/SingleLineTextInputTypes.h
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SingleLineTextInputTypes_h__
+#define mozilla_dom_SingleLineTextInputTypes_h__
+
+#include "mozilla/dom/InputType.h"
+
+namespace mozilla::dom {
+
+class SingleLineTextInputTypeBase : public InputType {
+ public:
+ ~SingleLineTextInputTypeBase() override = default;
+
+ bool MinAndMaxLengthApply() const final { return true; }
+ bool IsTooLong() const final;
+ bool IsTooShort() const final;
+ bool IsValueMissing() const final;
+ // Can return Nothing() if the JS engine failed to evaluate the pattern.
+ Maybe<bool> HasPatternMismatch() const final;
+
+ protected:
+ explicit SingleLineTextInputTypeBase(HTMLInputElement* aInputElement)
+ : InputType(aInputElement) {}
+
+ bool IsMutable() const override;
+};
+
+// input type=text
+class TextInputType : public SingleLineTextInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) TextInputType(aInputElement);
+ }
+
+ private:
+ explicit TextInputType(HTMLInputElement* aInputElement)
+ : SingleLineTextInputTypeBase(aInputElement) {}
+};
+
+// input type=search
+class SearchInputType : public SingleLineTextInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) SearchInputType(aInputElement);
+ }
+
+ private:
+ explicit SearchInputType(HTMLInputElement* aInputElement)
+ : SingleLineTextInputTypeBase(aInputElement) {}
+};
+
+// input type=tel
+class TelInputType : public SingleLineTextInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) TelInputType(aInputElement);
+ }
+
+ private:
+ explicit TelInputType(HTMLInputElement* aInputElement)
+ : SingleLineTextInputTypeBase(aInputElement) {}
+};
+
+// input type=url
+class URLInputType : public SingleLineTextInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) URLInputType(aInputElement);
+ }
+
+ bool HasTypeMismatch() const override;
+
+ nsresult GetTypeMismatchMessage(nsAString& aMessage) override;
+
+ private:
+ explicit URLInputType(HTMLInputElement* aInputElement)
+ : SingleLineTextInputTypeBase(aInputElement) {}
+};
+
+// input type=email
+class EmailInputType : public SingleLineTextInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) EmailInputType(aInputElement);
+ }
+
+ bool HasTypeMismatch() const override;
+ bool HasBadInput() const override;
+
+ nsresult GetTypeMismatchMessage(nsAString& aMessage) override;
+ nsresult GetBadInputMessage(nsAString& aMessage) override;
+
+ private:
+ explicit EmailInputType(HTMLInputElement* aInputElement)
+ : SingleLineTextInputTypeBase(aInputElement) {}
+
+ /**
+ * This helper method returns true if aValue is a valid email address.
+ * This is following the HTML5 specification:
+ * http://dev.w3.org/html5/spec/forms.html#valid-e-mail-address
+ *
+ * @param aValue the email address to check.
+ * @result whether the given string is a valid email address.
+ */
+ static bool IsValidEmailAddress(const nsAString& aValue);
+
+ /**
+ * This helper method returns true if aValue is a valid email address list.
+ * Email address list is a list of email address separated by comas (,) which
+ * can be surrounded by space charecters.
+ * This is following the HTML5 specification:
+ * http://dev.w3.org/html5/spec/forms.html#valid-e-mail-address-list
+ *
+ * @param aValue the email address list to check.
+ * @result whether the given string is a valid email address list.
+ */
+ static bool IsValidEmailAddressList(const nsAString& aValue);
+
+ /**
+ * Takes aEmail and attempts to convert everything after the first "@"
+ * character (if anything) to punycode before returning the complete result
+ * via the aEncodedEmail out-param. The aIndexOfAt out-param is set to the
+ * index of the "@" character.
+ *
+ * If no "@" is found in aEmail, aEncodedEmail is simply set to aEmail and
+ * the aIndexOfAt out-param is set to kNotFound.
+ *
+ * Returns true in all cases unless an attempt to punycode encode fails. If
+ * false is returned, aEncodedEmail has not been set.
+ *
+ * This function exists because ConvertUTF8toACE() splits on ".", meaning that
+ * for 'user.name@sld.tld' it would treat "name@sld" as a label. We want to
+ * encode the domain part only.
+ */
+ static bool PunycodeEncodeEmailAddress(const nsAString& aEmail,
+ nsAutoCString& aEncodedEmail,
+ uint32_t* aIndexOfAt);
+};
+
+// input type=password
+class PasswordInputType : public SingleLineTextInputTypeBase {
+ public:
+ static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) {
+ return new (aMemory) PasswordInputType(aInputElement);
+ }
+
+ private:
+ explicit PasswordInputType(HTMLInputElement* aInputElement)
+ : SingleLineTextInputTypeBase(aInputElement) {}
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_SingleLineTextInputTypes_h__ */
diff --git a/dom/html/input/moz.build b/dom/html/input/moz.build
new file mode 100644
index 0000000000..fa468f11e7
--- /dev/null
+++ b/dom/html/input/moz.build
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom += [
+ "ButtonInputTypes.h",
+ "CheckableInputTypes.h",
+ "ColorInputType.h",
+ "DateTimeInputTypes.h",
+ "FileInputType.h",
+ "HiddenInputType.h",
+ "InputType.h",
+ "NumericInputTypes.h",
+ "SingleLineTextInputTypes.h",
+]
+
+UNIFIED_SOURCES += [
+ "CheckableInputTypes.cpp",
+ "DateTimeInputTypes.cpp",
+ "FileInputType.cpp",
+ "InputType.cpp",
+ "NumericInputTypes.cpp",
+ "SingleLineTextInputTypes.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/dom/html",
+ "/layout/forms",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/html/moz.build b/dom/html/moz.build
new file mode 100644
index 0000000000..f3fdd30917
--- /dev/null
+++ b/dom/html/moz.build
@@ -0,0 +1,247 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Core & HTML")
+
+DIRS += ["input"]
+
+MOCHITEST_MANIFESTS += [
+ "test/dialog/mochitest.toml",
+ "test/forms/mochitest.toml",
+ "test/forms/without_selectionchange/mochitest.toml",
+ "test/mochitest.toml",
+]
+
+MOCHITEST_CHROME_MANIFESTS += [
+ "test/chrome.toml",
+ "test/forms/chrome.toml",
+]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.toml"]
+
+EXPORTS += [
+ "nsGenericHTMLElement.h",
+ "nsGenericHTMLFrameElement.h",
+ "nsHTMLDocument.h",
+ "nsIConstraintValidation.h",
+ "nsIFormControl.h",
+ "nsIHTMLCollection.h",
+ "nsIRadioVisitor.h",
+]
+
+EXPORTS.mozilla += [
+ "TextControlElement.h",
+ "TextControlState.h",
+ "TextInputListener.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "ConstraintValidation.h",
+ "CustomStateSet.h",
+ "ElementInternals.h",
+ "FetchPriority.h",
+ "HTMLAllCollection.h",
+ "HTMLAnchorElement.h",
+ "HTMLAreaElement.h",
+ "HTMLAudioElement.h",
+ "HTMLBodyElement.h",
+ "HTMLBRElement.h",
+ "HTMLButtonElement.h",
+ "HTMLCanvasElement.h",
+ "HTMLDataElement.h",
+ "HTMLDataListElement.h",
+ "HTMLDetailsElement.h",
+ "HTMLDialogElement.h",
+ "HTMLDivElement.h",
+ "HTMLDNSPrefetch.h",
+ "HTMLElement.h",
+ "HTMLEmbedElement.h",
+ "HTMLFieldSetElement.h",
+ "HTMLFontElement.h",
+ "HTMLFormControlsCollection.h",
+ "HTMLFormElement.h",
+ "HTMLFormSubmission.h",
+ "HTMLFrameElement.h",
+ "HTMLFrameSetElement.h",
+ "HTMLHeadingElement.h",
+ "HTMLHRElement.h",
+ "HTMLIFrameElement.h",
+ "HTMLImageElement.h",
+ "HTMLInputElement.h",
+ "HTMLLabelElement.h",
+ "HTMLLegendElement.h",
+ "HTMLLIElement.h",
+ "HTMLLinkElement.h",
+ "HTMLMapElement.h",
+ "HTMLMarqueeElement.h",
+ "HTMLMediaElement.h",
+ "HTMLMenuElement.h",
+ "HTMLMetaElement.h",
+ "HTMLMeterElement.h",
+ "HTMLModElement.h",
+ "HTMLObjectElement.h",
+ "HTMLOptGroupElement.h",
+ "HTMLOptionElement.h",
+ "HTMLOptionsCollection.h",
+ "HTMLOutputElement.h",
+ "HTMLParagraphElement.h",
+ "HTMLPictureElement.h",
+ "HTMLPreElement.h",
+ "HTMLProgressElement.h",
+ "HTMLScriptElement.h",
+ "HTMLSelectElement.h",
+ "HTMLSharedElement.h",
+ "HTMLSharedListElement.h",
+ "HTMLSlotElement.h",
+ "HTMLSourceElement.h",
+ "HTMLSpanElement.h",
+ "HTMLStyleElement.h",
+ "HTMLSummaryElement.h",
+ "HTMLTableCaptionElement.h",
+ "HTMLTableCellElement.h",
+ "HTMLTableColElement.h",
+ "HTMLTableElement.h",
+ "HTMLTableRowElement.h",
+ "HTMLTableSectionElement.h",
+ "HTMLTemplateElement.h",
+ "HTMLTextAreaElement.h",
+ "HTMLTimeElement.h",
+ "HTMLTitleElement.h",
+ "HTMLTrackElement.h",
+ "HTMLUnknownElement.h",
+ "HTMLVideoElement.h",
+ "ImageDocument.h",
+ "MediaDocument.h",
+ "MediaError.h",
+ "nsBrowserElement.h",
+ "PlayPromise.h",
+ "RadioNodeList.h",
+ "TextTrackManager.h",
+ "TimeRanges.h",
+ "ValidityState.h",
+]
+
+UNIFIED_SOURCES += [
+ "ConstraintValidation.cpp",
+ "CustomStateSet.cpp",
+ "ElementInternals.cpp",
+ "FetchPriority.cpp",
+ "HTMLAllCollection.cpp",
+ "HTMLAnchorElement.cpp",
+ "HTMLAreaElement.cpp",
+ "HTMLAudioElement.cpp",
+ "HTMLBodyElement.cpp",
+ "HTMLBRElement.cpp",
+ "HTMLButtonElement.cpp",
+ "HTMLCanvasElement.cpp",
+ "HTMLDataElement.cpp",
+ "HTMLDataListElement.cpp",
+ "HTMLDetailsElement.cpp",
+ "HTMLDialogElement.cpp",
+ "HTMLDivElement.cpp",
+ "HTMLDNSPrefetch.cpp",
+ "HTMLElement.cpp",
+ "HTMLEmbedElement.cpp",
+ "HTMLFieldSetElement.cpp",
+ "HTMLFontElement.cpp",
+ "HTMLFormControlsCollection.cpp",
+ "HTMLFormElement.cpp",
+ "HTMLFormSubmission.cpp",
+ "HTMLFrameElement.cpp",
+ "HTMLFrameSetElement.cpp",
+ "HTMLHeadingElement.cpp",
+ "HTMLHRElement.cpp",
+ "HTMLIFrameElement.cpp",
+ "HTMLImageElement.cpp",
+ "HTMLInputElement.cpp",
+ "HTMLLabelElement.cpp",
+ "HTMLLegendElement.cpp",
+ "HTMLLIElement.cpp",
+ "HTMLLinkElement.cpp",
+ "HTMLMapElement.cpp",
+ "HTMLMarqueeElement.cpp",
+ "HTMLMediaElement.cpp",
+ "HTMLMenuElement.cpp",
+ "HTMLMetaElement.cpp",
+ "HTMLMeterElement.cpp",
+ "HTMLModElement.cpp",
+ "HTMLObjectElement.cpp",
+ "HTMLOptGroupElement.cpp",
+ "HTMLOptionElement.cpp",
+ "HTMLOptionsCollection.cpp",
+ "HTMLOutputElement.cpp",
+ "HTMLParagraphElement.cpp",
+ "HTMLPictureElement.cpp",
+ "HTMLPreElement.cpp",
+ "HTMLProgressElement.cpp",
+ "HTMLScriptElement.cpp",
+ "HTMLSelectElement.cpp",
+ "HTMLSharedElement.cpp",
+ "HTMLSharedListElement.cpp",
+ "HTMLSlotElement.cpp",
+ "HTMLSourceElement.cpp",
+ "HTMLSpanElement.cpp",
+ "HTMLStyleElement.cpp",
+ "HTMLSummaryElement.cpp",
+ "HTMLTableCaptionElement.cpp",
+ "HTMLTableCellElement.cpp",
+ "HTMLTableColElement.cpp",
+ "HTMLTableElement.cpp",
+ "HTMLTableRowElement.cpp",
+ "HTMLTableSectionElement.cpp",
+ "HTMLTemplateElement.cpp",
+ "HTMLTextAreaElement.cpp",
+ "HTMLTimeElement.cpp",
+ "HTMLTitleElement.cpp",
+ "HTMLTrackElement.cpp",
+ "HTMLUnknownElement.cpp",
+ "HTMLVideoElement.cpp",
+ "ImageDocument.cpp",
+ "MediaDocument.cpp",
+ "MediaError.cpp",
+ "nsBrowserElement.cpp",
+ "nsDOMStringMap.cpp",
+ "nsGenericHTMLElement.cpp",
+ "nsGenericHTMLFrameElement.cpp",
+ "nsHTMLContentSink.cpp",
+ "nsHTMLDocument.cpp",
+ "nsIConstraintValidation.cpp",
+ "nsRadioVisitor.cpp",
+ "PlayPromise.cpp",
+ "RadioNodeList.cpp",
+ "TextControlState.cpp",
+ "TextTrackManager.cpp",
+ "TimeRanges.cpp",
+ "ValidityState.cpp",
+ "VideoDocument.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/caps",
+ "/docshell/base",
+ "/dom/base",
+ "/dom/canvas",
+ "/dom/html/input",
+ "/dom/media",
+ "/dom/security",
+ "/dom/xul",
+ "/image",
+ "/layout/forms",
+ "/layout/generic",
+ "/layout/style",
+ "/layout/tables",
+ "/layout/xul",
+ "/netwerk/base",
+ "/parser/htmlparser",
+]
+
+FINAL_LIBRARY = "xul"
+
+if CONFIG["MOZ_ANDROID_HLS_SUPPORT"]:
+ DEFINES["MOZ_ANDROID_HLS_SUPPORT"] = True
diff --git a/dom/html/nsBrowserElement.cpp b/dom/html/nsBrowserElement.cpp
new file mode 100644
index 0000000000..69284e77ab
--- /dev/null
+++ b/dom/html/nsBrowserElement.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsBrowserElement.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ToJSValue.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsFrameLoader.h"
+#include "nsINode.h"
+
+#include "js/Wrapper.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+bool nsBrowserElement::IsBrowserElementOrThrow(ErrorResult& aRv) {
+ if (mBrowserElementAPI) {
+ return true;
+ }
+ aRv.Throw(NS_ERROR_DOM_INVALID_NODE_TYPE_ERR);
+ return false;
+}
+
+void nsBrowserElement::InitBrowserElementAPI() {
+ RefPtr<nsFrameLoader> frameLoader = GetFrameLoader();
+ NS_ENSURE_TRUE_VOID(frameLoader);
+
+ if (!frameLoader->OwnerIsMozBrowserFrame()) {
+ return;
+ }
+
+ if (!mBrowserElementAPI) {
+ mBrowserElementAPI =
+ do_CreateInstance("@mozilla.org/dom/browser-element-api;1");
+ if (NS_WARN_IF(!mBrowserElementAPI)) {
+ return;
+ }
+ }
+ mBrowserElementAPI->SetFrameLoader(frameLoader);
+}
+
+void nsBrowserElement::DestroyBrowserElementFrameScripts() {
+ if (!mBrowserElementAPI) {
+ return;
+ }
+ mBrowserElementAPI->DestroyFrameScripts();
+}
+
+} // namespace mozilla
diff --git a/dom/html/nsBrowserElement.h b/dom/html/nsBrowserElement.h
new file mode 100644
index 0000000000..c81529de33
--- /dev/null
+++ b/dom/html/nsBrowserElement.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsBrowserElement_h
+#define nsBrowserElement_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+
+#include "nsCOMPtr.h"
+#include "nsIBrowserElementAPI.h"
+
+class nsFrameLoader;
+
+namespace mozilla {
+
+namespace dom {
+class Promise;
+} // namespace dom
+
+class ErrorResult;
+
+/**
+ * A helper class for browser-element frames
+ */
+class nsBrowserElement {
+ public:
+ nsBrowserElement() = default;
+ virtual ~nsBrowserElement() = default;
+
+ void SendMouseEvent(const nsAString& aType, uint32_t aX, uint32_t aY,
+ uint32_t aButton, uint32_t aClickCount,
+ uint32_t aModifiers, ErrorResult& aRv);
+ void GoBack(ErrorResult& aRv);
+ void GoForward(ErrorResult& aRv);
+ void Reload(bool aHardReload, ErrorResult& aRv);
+ void Stop(ErrorResult& aRv);
+
+ already_AddRefed<dom::Promise> GetCanGoBack(ErrorResult& aRv);
+ already_AddRefed<dom::Promise> GetCanGoForward(ErrorResult& aRv);
+
+ protected:
+ virtual already_AddRefed<nsFrameLoader> GetFrameLoader() = 0;
+
+ void InitBrowserElementAPI();
+ void DestroyBrowserElementFrameScripts();
+ nsCOMPtr<nsIBrowserElementAPI> mBrowserElementAPI;
+
+ private:
+ bool IsBrowserElementOrThrow(ErrorResult& aRv);
+};
+
+} // namespace mozilla
+
+#endif // nsBrowserElement_h
diff --git a/dom/html/nsDOMStringMap.cpp b/dom/html/nsDOMStringMap.cpp
new file mode 100644
index 0000000000..f8975cc02e
--- /dev/null
+++ b/dom/html/nsDOMStringMap.cpp
@@ -0,0 +1,242 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsDOMStringMap.h"
+
+#include "jsapi.h"
+#include "nsError.h"
+#include "nsGenericHTMLElement.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/DOMStringMapBinding.h"
+#include "mozilla/dom/MutationEventBinding.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsDOMStringMap)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsDOMStringMap)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElement)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsDOMStringMap)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ // Check that mElement exists in case the unlink code is run more than once.
+ if (tmp->mElement) {
+ // Call back to element to null out weak reference to this object.
+ tmp->mElement->ClearDataset();
+ tmp->mElement->RemoveMutationObserver(tmp);
+ tmp->mElement = nullptr;
+ }
+ tmp->mExpandoAndGeneration.OwnerUnlinked();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDOMStringMap)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsDOMStringMap)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsDOMStringMap)
+
+nsDOMStringMap::nsDOMStringMap(Element* aElement)
+ : mElement(aElement), mRemovingProp(false) {
+ mElement->AddMutationObserver(this);
+}
+
+nsDOMStringMap::~nsDOMStringMap() {
+ // Check if element still exists, may have been unlinked by cycle collector.
+ if (mElement) {
+ // Call back to element to null out weak reference to this object.
+ mElement->ClearDataset();
+ mElement->RemoveMutationObserver(this);
+ }
+}
+
+DocGroup* nsDOMStringMap::GetDocGroup() const {
+ return mElement ? mElement->GetDocGroup() : nullptr;
+}
+
+/* virtual */
+JSObject* nsDOMStringMap::WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return DOMStringMap_Binding::Wrap(cx, this, aGivenProto);
+}
+
+void nsDOMStringMap::NamedGetter(const nsAString& aProp, bool& found,
+ DOMString& aResult) const {
+ nsAutoString attr;
+
+ if (!DataPropToAttr(aProp, attr)) {
+ found = false;
+ return;
+ }
+
+ found = mElement->GetAttr(attr, aResult);
+}
+
+void nsDOMStringMap::NamedSetter(const nsAString& aProp,
+ const nsAString& aValue, ErrorResult& rv) {
+ nsAutoString attr;
+ if (!DataPropToAttr(aProp, attr)) {
+ rv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+ return;
+ }
+
+ nsresult res = nsContentUtils::CheckQName(attr, false);
+ if (NS_FAILED(res)) {
+ rv.Throw(res);
+ return;
+ }
+
+ RefPtr<nsAtom> attrAtom = NS_Atomize(attr);
+ MOZ_ASSERT(attrAtom, "Should be infallible");
+
+ res = mElement->SetAttr(kNameSpaceID_None, attrAtom, aValue, true);
+ if (NS_FAILED(res)) {
+ rv.Throw(res);
+ }
+}
+
+void nsDOMStringMap::NamedDeleter(const nsAString& aProp, bool& found) {
+ // Currently removing property, attribute is already removed.
+ if (mRemovingProp) {
+ found = false;
+ return;
+ }
+
+ nsAutoString attr;
+ if (!DataPropToAttr(aProp, attr)) {
+ found = false;
+ return;
+ }
+
+ RefPtr<nsAtom> attrAtom = NS_Atomize(attr);
+ MOZ_ASSERT(attrAtom, "Should be infallible");
+
+ found = mElement->HasAttr(attrAtom);
+
+ if (found) {
+ mRemovingProp = true;
+ mElement->UnsetAttr(kNameSpaceID_None, attrAtom, true);
+ mRemovingProp = false;
+ }
+}
+
+void nsDOMStringMap::GetSupportedNames(nsTArray<nsString>& aNames) {
+ uint32_t attrCount = mElement->GetAttrCount();
+
+ // Iterate through all the attributes and add property
+ // names corresponding to data attributes to return array.
+ for (uint32_t i = 0; i < attrCount; ++i) {
+ const nsAttrName* attrName = mElement->GetAttrNameAt(i);
+ // Skip the ones that are not in the null namespace
+ if (attrName->NamespaceID() != kNameSpaceID_None) {
+ continue;
+ }
+
+ nsAutoString prop;
+ if (!AttrToDataProp(nsDependentAtomString(attrName->LocalName()), prop)) {
+ continue;
+ }
+
+ aNames.AppendElement(prop);
+ }
+}
+
+/**
+ * Converts a dataset property name to the corresponding data attribute name.
+ * (ex. aBigFish to data-a-big-fish).
+ */
+bool nsDOMStringMap::DataPropToAttr(const nsAString& aProp,
+ nsAutoString& aResult) {
+ // aResult is an autostring, so don't worry about setting its capacity:
+ // SetCapacity is slow even when it's a no-op and we already have enough
+ // storage there for most cases, probably.
+ aResult.AppendLiteral("data-");
+
+ // Iterate property by character to form attribute name.
+ // Return syntax error if there is a sequence of "-" followed by a character
+ // in the range "a" to "z".
+ // Replace capital characters with "-" followed by lower case character.
+ // Otherwise, simply append character to attribute name.
+ const char16_t* start = aProp.BeginReading();
+ const char16_t* end = aProp.EndReading();
+ const char16_t* cur = start;
+ for (; cur < end; ++cur) {
+ const char16_t* next = cur + 1;
+ if (char16_t('-') == *cur && next < end && char16_t('a') <= *next &&
+ *next <= char16_t('z')) {
+ // Syntax error if character following "-" is in range "a" to "z".
+ return false;
+ }
+
+ if (char16_t('A') <= *cur && *cur <= char16_t('Z')) {
+ // Append the characters in the range [start, cur)
+ aResult.Append(start, cur - start);
+ // Uncamel-case characters in the range of "A" to "Z".
+ aResult.Append(char16_t('-'));
+ aResult.Append(*cur - 'A' + 'a');
+ start = next; // We've already appended the thing at *cur
+ }
+ }
+
+ aResult.Append(start, cur - start);
+
+ return true;
+}
+
+/**
+ * Converts a data attribute name to the corresponding dataset property name.
+ * (ex. data-a-big-fish to aBigFish).
+ */
+bool nsDOMStringMap::AttrToDataProp(const nsAString& aAttr,
+ nsAutoString& aResult) {
+ // If the attribute name does not begin with "data-" then it can not be
+ // a data attribute.
+ if (!StringBeginsWith(aAttr, u"data-"_ns)) {
+ return false;
+ }
+
+ // Start reading attribute from first character after "data-".
+ const char16_t* cur = aAttr.BeginReading() + 5;
+ const char16_t* end = aAttr.EndReading();
+
+ // Don't try to mess with aResult's capacity: the probably-no-op SetCapacity()
+ // call is not that fast.
+
+ // Iterate through attrName by character to form property name.
+ // If there is a sequence of "-" followed by a character in the range "a" to
+ // "z" then replace with upper case letter.
+ // Otherwise append character to property name.
+ for (; cur < end; ++cur) {
+ const char16_t* next = cur + 1;
+ if (char16_t('-') == *cur && next < end && char16_t('a') <= *next &&
+ *next <= char16_t('z')) {
+ // Upper case the lower case letters that follow a "-".
+ aResult.Append(*next - 'a' + 'A');
+ // Consume character to account for "-" character.
+ ++cur;
+ } else {
+ // Simply append character if camel case is not necessary.
+ aResult.Append(*cur);
+ }
+ }
+
+ return true;
+}
+
+void nsDOMStringMap::AttributeChanged(Element* aElement, int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ if ((aModType == MutationEvent_Binding::ADDITION ||
+ aModType == MutationEvent_Binding::REMOVAL) &&
+ aNameSpaceID == kNameSpaceID_None &&
+ StringBeginsWith(nsDependentAtomString(aAttribute), u"data-"_ns)) {
+ ++mExpandoAndGeneration.generation;
+ }
+}
diff --git a/dom/html/nsDOMStringMap.h b/dom/html/nsDOMStringMap.h
new file mode 100644
index 0000000000..a5b3a20832
--- /dev/null
+++ b/dom/html/nsDOMStringMap.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsDOMStringMap_h
+#define nsDOMStringMap_h
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsStubMutationObserver.h"
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+#include "js/friend/DOMProxy.h" // JS::ExpandoAndGeneration
+#include "js/RootingAPI.h" // JS::Handle
+
+// XXX Avoid including this here by moving function bodies to the cpp file
+#include "mozilla/dom/Element.h"
+
+namespace mozilla {
+class ErrorResult;
+namespace dom {
+class DOMString;
+class DocGroup;
+} // namespace dom
+} // namespace mozilla
+
+class nsDOMStringMap : public nsStubMutationObserver, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsDOMStringMap)
+
+ NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
+
+ nsINode* GetParentObject() { return mElement; }
+
+ mozilla::dom::DocGroup* GetDocGroup() const;
+
+ explicit nsDOMStringMap(mozilla::dom::Element* aElement);
+
+ // WebIDL API
+ virtual JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ void NamedGetter(const nsAString& aProp, bool& found,
+ mozilla::dom::DOMString& aResult) const;
+ void NamedSetter(const nsAString& aProp, const nsAString& aValue,
+ mozilla::ErrorResult& rv);
+ void NamedDeleter(const nsAString& aProp, bool& found);
+ void GetSupportedNames(nsTArray<nsString>& aNames);
+
+ JS::ExpandoAndGeneration mExpandoAndGeneration;
+
+ private:
+ virtual ~nsDOMStringMap();
+
+ protected:
+ RefPtr<mozilla::dom::Element> mElement;
+ // Flag to guard against infinite recursion.
+ bool mRemovingProp;
+ static bool DataPropToAttr(const nsAString& aProp, nsAutoString& aResult);
+ static bool AttrToDataProp(const nsAString& aAttr, nsAutoString& aResult);
+};
+
+#endif
diff --git a/dom/html/nsGenericHTMLElement.cpp b/dom/html/nsGenericHTMLElement.cpp
new file mode 100644
index 0000000000..de29276fdc
--- /dev/null
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -0,0 +1,3623 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EditorBase.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/EventListenerManager.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/IMEContentObserver.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/MappedDeclarationsBuilder.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/StaticPrefs_html5.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/dom/FetchPriority.h"
+#include "mozilla/dom/FormData.h"
+#include "nscore.h"
+#include "nsGenericHTMLElement.h"
+#include "nsCOMPtr.h"
+#include "nsAtom.h"
+#include "nsQueryObject.h"
+#include "mozilla/dom/BindContext.h"
+#include "mozilla/dom/Document.h"
+#include "nsPIDOMWindow.h"
+#include "nsIFrameInlines.h"
+#include "nsIScrollableFrame.h"
+#include "nsView.h"
+#include "nsViewManager.h"
+#include "nsIWidget.h"
+#include "nsRange.h"
+#include "nsPresContext.h"
+#include "nsError.h"
+#include "nsIPrincipal.h"
+#include "nsContainerFrame.h"
+#include "nsStyleUtil.h"
+#include "ReferrerInfo.h"
+
+#include "mozilla/PresState.h"
+#include "nsILayoutHistoryState.h"
+
+#include "nsHTMLParts.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/DirectionalityUtils.h"
+#include "mozilla/dom/DocumentOrShadowRoot.h"
+#include "nsString.h"
+#include "nsGkAtoms.h"
+#include "nsDOMCSSDeclaration.h"
+#include "nsITextControlFrame.h"
+#include "nsIFormControl.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "nsFocusManager.h"
+
+#include "nsDOMStringMap.h"
+#include "nsDOMString.h"
+
+#include "nsLayoutUtils.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "HTMLFieldSetElement.h"
+#include "nsTextNode.h"
+#include "HTMLBRElement.h"
+#include "nsDOMMutationObserver.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/FromParser.h"
+#include "mozilla/dom/Link.h"
+#include "mozilla/dom/ScriptLoader.h"
+
+#include "nsDOMTokenList.h"
+#include "nsThreadUtils.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/ToggleEvent.h"
+#include "mozilla/dom/TouchEvent.h"
+#include "mozilla/dom/InputEvent.h"
+#include "mozilla/dom/InvokeEvent.h"
+#include "mozilla/ErrorResult.h"
+#include "nsHTMLDocument.h"
+#include "nsGlobalWindowInner.h"
+#include "mozilla/dom/HTMLBodyElement.h"
+#include "imgIContainer.h"
+#include "nsComputedDOMStyle.h"
+#include "mozilla/dom/HTMLDialogElement.h"
+#include "mozilla/dom/HTMLLabelElement.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/CustomElementRegistry.h"
+#include "mozilla/dom/ElementBinding.h"
+#include "mozilla/dom/ElementInternals.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static const uint8_t NS_INPUTMODE_NONE = 1;
+static const uint8_t NS_INPUTMODE_TEXT = 2;
+static const uint8_t NS_INPUTMODE_TEL = 3;
+static const uint8_t NS_INPUTMODE_URL = 4;
+static const uint8_t NS_INPUTMODE_EMAIL = 5;
+static const uint8_t NS_INPUTMODE_NUMERIC = 6;
+static const uint8_t NS_INPUTMODE_DECIMAL = 7;
+static const uint8_t NS_INPUTMODE_SEARCH = 8;
+
+static const nsAttrValue::EnumTable kInputmodeTable[] = {
+ {"none", NS_INPUTMODE_NONE},
+ {"text", NS_INPUTMODE_TEXT},
+ {"tel", NS_INPUTMODE_TEL},
+ {"url", NS_INPUTMODE_URL},
+ {"email", NS_INPUTMODE_EMAIL},
+ {"numeric", NS_INPUTMODE_NUMERIC},
+ {"decimal", NS_INPUTMODE_DECIMAL},
+ {"search", NS_INPUTMODE_SEARCH},
+ {nullptr, 0}};
+
+static const uint8_t NS_ENTERKEYHINT_ENTER = 1;
+static const uint8_t NS_ENTERKEYHINT_DONE = 2;
+static const uint8_t NS_ENTERKEYHINT_GO = 3;
+static const uint8_t NS_ENTERKEYHINT_NEXT = 4;
+static const uint8_t NS_ENTERKEYHINT_PREVIOUS = 5;
+static const uint8_t NS_ENTERKEYHINT_SEARCH = 6;
+static const uint8_t NS_ENTERKEYHINT_SEND = 7;
+
+static const nsAttrValue::EnumTable kEnterKeyHintTable[] = {
+ {"enter", NS_ENTERKEYHINT_ENTER},
+ {"done", NS_ENTERKEYHINT_DONE},
+ {"go", NS_ENTERKEYHINT_GO},
+ {"next", NS_ENTERKEYHINT_NEXT},
+ {"previous", NS_ENTERKEYHINT_PREVIOUS},
+ {"search", NS_ENTERKEYHINT_SEARCH},
+ {"send", NS_ENTERKEYHINT_SEND},
+ {nullptr, 0}};
+
+static const uint8_t NS_AUTOCAPITALIZE_NONE = 1;
+static const uint8_t NS_AUTOCAPITALIZE_SENTENCES = 2;
+static const uint8_t NS_AUTOCAPITALIZE_WORDS = 3;
+static const uint8_t NS_AUTOCAPITALIZE_CHARACTERS = 4;
+
+static const nsAttrValue::EnumTable kAutocapitalizeTable[] = {
+ {"none", NS_AUTOCAPITALIZE_NONE},
+ {"sentences", NS_AUTOCAPITALIZE_SENTENCES},
+ {"words", NS_AUTOCAPITALIZE_WORDS},
+ {"characters", NS_AUTOCAPITALIZE_CHARACTERS},
+ {"off", NS_AUTOCAPITALIZE_NONE},
+ {"on", NS_AUTOCAPITALIZE_SENTENCES},
+ {"", 0},
+ {nullptr, 0}};
+
+static const nsAttrValue::EnumTable* kDefaultAutocapitalize =
+ &kAutocapitalizeTable[1];
+
+nsresult nsGenericHTMLElement::CopyInnerTo(Element* aDst) {
+ MOZ_ASSERT(!aDst->GetUncomposedDoc(),
+ "Should not CopyInnerTo an Element in a document");
+
+ auto reparse = aDst->OwnerDoc() == OwnerDoc() ? ReparseAttributes::No
+ : ReparseAttributes::Yes;
+ nsresult rv = Element::CopyInnerTo(aDst, reparse);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // cloning a node must retain its internal nonce slot
+ nsString* nonce = static_cast<nsString*>(GetProperty(nsGkAtoms::nonce));
+ if (nonce) {
+ static_cast<nsGenericHTMLElement*>(aDst)->SetNonce(*nonce);
+ }
+ return NS_OK;
+}
+
+static const nsAttrValue::EnumTable kDirTable[] = {
+ {"ltr", Directionality::Ltr},
+ {"rtl", Directionality::Rtl},
+ {"auto", Directionality::Auto},
+ {nullptr, 0},
+};
+
+namespace {
+// See <https://html.spec.whatwg.org/#the-popover-attribute>.
+enum class PopoverAttributeKeyword : uint8_t { Auto, EmptyString, Manual };
+
+static const char* kPopoverAttributeValueAuto = "auto";
+static const char* kPopoverAttributeValueEmptyString = "";
+static const char* kPopoverAttributeValueManual = "manual";
+
+static const nsAttrValue::EnumTable kPopoverTable[] = {
+ {kPopoverAttributeValueAuto, PopoverAttributeKeyword::Auto},
+ {kPopoverAttributeValueEmptyString, PopoverAttributeKeyword::EmptyString},
+ {kPopoverAttributeValueManual, PopoverAttributeKeyword::Manual},
+ {nullptr, 0}};
+
+// See <https://html.spec.whatwg.org/#the-popover-attribute>.
+static const nsAttrValue::EnumTable* kPopoverTableInvalidValueDefault =
+ &kPopoverTable[2];
+} // namespace
+
+void nsGenericHTMLElement::GetFetchPriority(nsAString& aFetchPriority) const {
+ // <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>.
+ GetEnumAttr(nsGkAtoms::fetchpriority, kFetchPriorityAttributeValueAuto,
+ aFetchPriority);
+}
+
+/* static */
+FetchPriority nsGenericHTMLElement::ToFetchPriority(const nsAString& aValue) {
+ nsAttrValue attrValue;
+ ParseFetchPriority(aValue, attrValue);
+ MOZ_ASSERT(attrValue.Type() == nsAttrValue::eEnum);
+ return FetchPriority(attrValue.GetEnumValue());
+}
+
+namespace {
+// <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>.
+static const nsAttrValue::EnumTable kFetchPriorityEnumTable[] = {
+ {kFetchPriorityAttributeValueHigh, FetchPriority::High},
+ {kFetchPriorityAttributeValueLow, FetchPriority::Low},
+ {kFetchPriorityAttributeValueAuto, FetchPriority::Auto},
+ {nullptr, 0}};
+
+// <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>.
+static const nsAttrValue::EnumTable*
+ kFetchPriorityEnumTableInvalidValueDefault = &kFetchPriorityEnumTable[2];
+} // namespace
+
+FetchPriority nsGenericHTMLElement::GetFetchPriority() const {
+ const nsAttrValue* fetchpriorityAttribute =
+ GetParsedAttr(nsGkAtoms::fetchpriority);
+ if (fetchpriorityAttribute) {
+ MOZ_ASSERT(fetchpriorityAttribute->Type() == nsAttrValue::eEnum);
+ return FetchPriority(fetchpriorityAttribute->GetEnumValue());
+ }
+
+ return FetchPriority::Auto;
+}
+
+/* static */
+void nsGenericHTMLElement::ParseFetchPriority(const nsAString& aValue,
+ nsAttrValue& aResult) {
+ aResult.ParseEnumValue(aValue, kFetchPriorityEnumTable,
+ false /* aCaseSensitive */,
+ kFetchPriorityEnumTableInvalidValueDefault);
+}
+
+void nsGenericHTMLElement::AddToNameTable(nsAtom* aName) {
+ MOZ_ASSERT(HasName(), "Node doesn't have name?");
+ Document* doc = GetUncomposedDoc();
+ if (doc && !IsInNativeAnonymousSubtree()) {
+ doc->AddToNameTable(this, aName);
+ }
+}
+
+void nsGenericHTMLElement::RemoveFromNameTable() {
+ if (HasName() && CanHaveName(NodeInfo()->NameAtom())) {
+ if (Document* doc = GetUncomposedDoc()) {
+ doc->RemoveFromNameTable(this,
+ GetParsedAttr(nsGkAtoms::name)->GetAtomValue());
+ }
+ }
+}
+
+void nsGenericHTMLElement::GetAccessKeyLabel(nsString& aLabel) {
+ nsAutoString suffix;
+ GetAccessKey(suffix);
+ if (!suffix.IsEmpty()) {
+ EventStateManager::GetAccessKeyLabelPrefix(this, aLabel);
+ aLabel.Append(suffix);
+ }
+}
+
+static bool IsOffsetParent(nsIFrame* aFrame) {
+ LayoutFrameType frameType = aFrame->Type();
+
+ if (frameType == LayoutFrameType::TableCell ||
+ frameType == LayoutFrameType::TableWrapper) {
+ // Per the IDL for Element, only td, th, and table are acceptable
+ // offsetParents apart from body or positioned elements; we need to check
+ // the content type as well as the frame type so we ignore anonymous tables
+ // created by an element with display: table-cell with no actual table
+ nsIContent* content = aFrame->GetContent();
+
+ return content->IsAnyOfHTMLElements(nsGkAtoms::table, nsGkAtoms::td,
+ nsGkAtoms::th);
+ }
+ return false;
+}
+
+struct OffsetResult {
+ Element* mParent = nullptr;
+ CSSIntRect mRect;
+};
+
+static OffsetResult GetUnretargetedOffsetsFor(const Element& aElement) {
+ nsIFrame* frame = aElement.GetPrimaryFrame();
+ if (!frame) {
+ return {};
+ }
+
+ nsIFrame* styleFrame = nsLayoutUtils::GetStyleFrame(frame);
+
+ nsIFrame* parent = frame->GetParent();
+ nsPoint origin(0, 0);
+
+ nsIContent* offsetParent = nullptr;
+ Element* docElement = aElement.GetComposedDoc()->GetRootElement();
+ nsIContent* content = frame->GetContent();
+
+ if (content &&
+ (content->IsHTMLElement(nsGkAtoms::body) || content == docElement)) {
+ parent = frame;
+ } else {
+ const bool isPositioned = styleFrame->IsAbsPosContainingBlock();
+ const bool isAbsolutelyPositioned = frame->IsAbsolutelyPositioned();
+ origin += frame->GetPositionIgnoringScrolling();
+
+ for (; parent; parent = parent->GetParent()) {
+ content = parent->GetContent();
+
+ // Stop at the first ancestor that is positioned.
+ if (parent->IsAbsPosContainingBlock()) {
+ offsetParent = content;
+ break;
+ }
+
+ // Add the parent's origin to our own to get to the
+ // right coordinate system.
+ const bool isOffsetParent = !isPositioned && IsOffsetParent(parent);
+ if (!isOffsetParent) {
+ origin += parent->GetPositionIgnoringScrolling();
+ }
+
+ if (content) {
+ // If we've hit the document element, break here.
+ if (content == docElement) {
+ break;
+ }
+
+ // Break if the ancestor frame type makes it suitable as offset parent
+ // and this element is *not* positioned or if we found the body element.
+ if (isOffsetParent || content->IsHTMLElement(nsGkAtoms::body)) {
+ offsetParent = content;
+ break;
+ }
+ }
+ }
+
+ if (isAbsolutelyPositioned && !offsetParent) {
+ // If this element is absolutely positioned, but we don't have
+ // an offset parent it means this element is an absolutely
+ // positioned child that's not nested inside another positioned
+ // element, in this case the element's frame's parent is the
+ // frame for the HTML element so we fail to find the body in the
+ // parent chain. We want the offset parent in this case to be
+ // the body, so we just get the body element from the document.
+ //
+ // We use GetBodyElement() here, not GetBody(), because we don't want to
+ // end up with framesets here.
+ offsetParent = aElement.GetComposedDoc()->GetBodyElement();
+ }
+ }
+
+ // Make the position relative to the padding edge.
+ if (parent) {
+ const nsStyleBorder* border = parent->StyleBorder();
+ origin.x -= border->GetComputedBorderWidth(eSideLeft);
+ origin.y -= border->GetComputedBorderWidth(eSideTop);
+ }
+
+ // Get the union of all rectangles in this and continuation frames.
+ // It doesn't really matter what we use as aRelativeTo here, since
+ // we only care about the size. We just have to use something non-null.
+ nsRect rcFrame = nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame);
+ rcFrame.MoveTo(origin);
+ return {Element::FromNodeOrNull(offsetParent),
+ CSSIntRect::FromAppUnitsRounded(rcFrame)};
+}
+
+static bool ShouldBeRetargeted(const Element& aReferenceElement,
+ const Element& aElementToMaybeRetarget) {
+ ShadowRoot* shadow = aElementToMaybeRetarget.GetContainingShadow();
+ if (!shadow) {
+ return false;
+ }
+ for (ShadowRoot* scope = aReferenceElement.GetContainingShadow(); scope;
+ scope = scope->Host()->GetContainingShadow()) {
+ if (scope == shadow) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) {
+ aRect = CSSIntRect();
+
+ if (!GetPrimaryFrame(FlushType::Layout)) {
+ return nullptr;
+ }
+
+ OffsetResult thisResult = GetUnretargetedOffsetsFor(*this);
+ aRect = thisResult.mRect;
+
+ Element* parent = thisResult.mParent;
+ while (parent && ShouldBeRetargeted(*this, *parent)) {
+ OffsetResult result = GetUnretargetedOffsetsFor(*parent);
+ aRect += result.mRect.TopLeft();
+ parent = result.mParent;
+ }
+
+ return parent;
+}
+
+bool nsGenericHTMLElement::Spellcheck() {
+ // Has the state has been explicitly set?
+ nsIContent* node;
+ for (node = this; node; node = node->GetParent()) {
+ if (node->IsHTMLElement()) {
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::_true,
+ nsGkAtoms::_false, nullptr};
+ switch (node->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::spellcheck, strings, eCaseMatters)) {
+ case 0: // spellcheck = "true"
+ return true;
+ case 1: // spellcheck = "false"
+ return false;
+ }
+ }
+ }
+
+ // contenteditable/designMode are spellchecked by default
+ if (IsEditable()) {
+ return true;
+ }
+
+ // Is this a chrome element?
+ if (nsContentUtils::IsChromeDoc(OwnerDoc())) {
+ return false; // Not spellchecked by default
+ }
+
+ // Anything else that's not a form control is not spellchecked by default
+ nsCOMPtr<nsIFormControl> formControl = do_QueryObject(this);
+ if (!formControl) {
+ return false; // Not spellchecked by default
+ }
+
+ // Is this a multiline plaintext input?
+ auto controlType = formControl->ControlType();
+ if (controlType == FormControlType::Textarea) {
+ return true; // Spellchecked by default
+ }
+
+ // Is this anything other than an input text?
+ // Other inputs are not spellchecked.
+ if (controlType != FormControlType::InputText) {
+ return false; // Not spellchecked by default
+ }
+
+ // Does the user want input text spellchecked by default?
+ // NOTE: Do not reflect a pref value of 0 back to the DOM getter.
+ // The web page should not know if the user has disabled spellchecking.
+ // We'll catch this in the editor itself.
+ int32_t spellcheckLevel = Preferences::GetInt("layout.spellcheckDefault", 1);
+ return spellcheckLevel == 2; // "Spellcheck multi- and single-line"
+}
+
+bool nsGenericHTMLElement::InNavQuirksMode(Document* aDoc) {
+ return aDoc && aDoc->GetCompatibilityMode() == eCompatibility_NavQuirks;
+}
+
+void nsGenericHTMLElement::UpdateEditableState(bool aNotify) {
+ // XXX Should we do this only when in a document?
+ ContentEditableTristate value = GetContentEditableValue();
+ if (value != eInherit) {
+ SetEditableFlag(!!value);
+ UpdateReadOnlyState(aNotify);
+ return;
+ }
+ nsStyledElement::UpdateEditableState(aNotify);
+}
+
+nsresult nsGenericHTMLElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElementBase::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsInComposedDoc()) {
+ RegUnRegAccessKey(true);
+ }
+
+ if (IsInUncomposedDoc()) {
+ if (HasName() && CanHaveName(NodeInfo()->NameAtom())) {
+ aContext.OwnerDoc().AddToNameTable(
+ this, GetParsedAttr(nsGkAtoms::name)->GetAtomValue());
+ }
+ }
+
+ if (HasFlag(NODE_IS_EDITABLE) && GetContentEditableValue() == eTrue &&
+ IsInComposedDoc()) {
+ aContext.OwnerDoc().ChangeContentEditableCount(this, +1);
+ }
+
+ // Hide any nonce from the DOM, but keep the internal value of the
+ // nonce by copying and resetting the internal nonce value.
+ if (HasFlag(NODE_HAS_NONCE_AND_HEADER_CSP) && IsInComposedDoc() &&
+ OwnerDoc()->GetBrowsingContext()) {
+ nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
+ "nsGenericHTMLElement::ResetNonce::Runnable",
+ [self = RefPtr<nsGenericHTMLElement>(this)]() {
+ nsAutoString nonce;
+ self->GetNonce(nonce);
+ self->SetAttr(kNameSpaceID_None, nsGkAtoms::nonce, u""_ns, true);
+ self->SetNonce(nonce);
+ }));
+ }
+
+ // We need to consider a labels element is moved to another subtree
+ // with different root, it needs to update labels list and its root
+ // as well.
+ nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots();
+ if (slots && slots->mLabelsList) {
+ slots->mLabelsList->MaybeResetRoot(SubtreeRoot());
+ }
+
+ return rv;
+}
+
+void nsGenericHTMLElement::UnbindFromTree(bool aNullParent) {
+ if (IsInComposedDoc()) {
+ // https://html.spec.whatwg.org/#dom-trees:hide-popover-algorithm
+ // If removedNode's popover attribute is not in the no popover state, then
+ // run the hide popover algorithm given removedNode, false, false, and
+ // false.
+ if (GetPopoverData()) {
+ HidePopoverWithoutRunningScript();
+ }
+ RegUnRegAccessKey(false);
+ }
+
+ RemoveFromNameTable();
+
+ if (GetContentEditableValue() == eTrue) {
+ if (Document* doc = GetComposedDoc()) {
+ doc->ChangeContentEditableCount(this, -1);
+ }
+ }
+
+ nsStyledElement::UnbindFromTree(aNullParent);
+
+ // Invalidate .labels list. It will be repopulated when used the next time.
+ nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots();
+ if (slots && slots->mLabelsList) {
+ slots->mLabelsList->MaybeResetRoot(SubtreeRoot());
+ }
+}
+
+HTMLFormElement* nsGenericHTMLElement::FindAncestorForm(
+ HTMLFormElement* aCurrentForm) {
+ NS_ASSERTION(!HasAttr(nsGkAtoms::form) || IsHTMLElement(nsGkAtoms::img),
+ "FindAncestorForm should not be called if @form is set!");
+ if (IsInNativeAnonymousSubtree()) {
+ return nullptr;
+ }
+
+ nsIContent* content = this;
+ while (content) {
+ // If the current ancestor is a form, return it as our form
+ if (content->IsHTMLElement(nsGkAtoms::form)) {
+#ifdef DEBUG
+ if (!nsContentUtils::IsInSameAnonymousTree(this, content)) {
+ // It's possible that we started unbinding at |content| or
+ // some ancestor of it, and |content| and |this| used to all be
+ // anonymous. Check for this the hard way.
+ for (nsIContent* child = this; child != content;
+ child = child->GetParent()) {
+ NS_ASSERTION(child->ComputeIndexInParentContent().isSome(),
+ "Walked too far?");
+ }
+ }
+#endif
+ return static_cast<HTMLFormElement*>(content);
+ }
+
+ nsIContent* prevContent = content;
+ content = prevContent->GetParent();
+
+ if (!content && aCurrentForm) {
+ // We got to the root of the subtree we're in, and we're being removed
+ // from the DOM (the only time we get into this method with a non-null
+ // aCurrentForm). Check whether aCurrentForm is in the same subtree. If
+ // it is, we want to return aCurrentForm, since this case means that
+ // we're one of those inputs-in-a-table that have a hacked mForm pointer
+ // and a subtree containing both us and the form got removed from the
+ // DOM.
+ if (aCurrentForm->IsInclusiveDescendantOf(prevContent)) {
+ return aCurrentForm;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+bool nsGenericHTMLElement::CheckHandleEventForAnchorsPreconditions(
+ EventChainVisitor& aVisitor) {
+ MOZ_ASSERT(nsCOMPtr<Link>(do_QueryObject(this)),
+ "should be called only when |this| implements |Link|");
+ // When disconnected, only <a> should navigate away per
+ // https://html.spec.whatwg.org/#cannot-navigate
+ return IsInComposedDoc() || IsHTMLElement(nsGkAtoms::a);
+}
+
+void nsGenericHTMLElement::GetEventTargetParentForAnchors(
+ EventChainPreVisitor& aVisitor) {
+ nsGenericHTMLElementBase::GetEventTargetParent(aVisitor);
+
+ if (!CheckHandleEventForAnchorsPreconditions(aVisitor)) {
+ return;
+ }
+
+ GetEventTargetParentForLinks(aVisitor);
+}
+
+nsresult nsGenericHTMLElement::PostHandleEventForAnchors(
+ EventChainPostVisitor& aVisitor) {
+ if (!CheckHandleEventForAnchorsPreconditions(aVisitor)) {
+ return NS_OK;
+ }
+
+ return PostHandleEventForLinks(aVisitor);
+}
+
+bool nsGenericHTMLElement::IsHTMLLink(nsIURI** aURI) const {
+ MOZ_ASSERT(aURI, "Must provide aURI out param");
+
+ *aURI = GetHrefURIForAnchors().take();
+ // We promise out param is non-null if we return true, so base rv on it
+ return *aURI != nullptr;
+}
+
+already_AddRefed<nsIURI> nsGenericHTMLElement::GetHrefURIForAnchors() const {
+ // This is used by the three Link implementations and
+ // nsHTMLStyleElement.
+
+ // Get href= attribute (relative URI).
+
+ // We use the nsAttrValue's copy of the URI string to avoid copying.
+ nsCOMPtr<nsIURI> uri;
+ GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(uri));
+
+ return uri.forget();
+}
+
+void nsGenericHTMLElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::accesskey) {
+ // Have to unregister before clearing flag. See UnregAccessKey
+ RegUnRegAccessKey(false);
+ if (!aValue) {
+ UnsetFlags(NODE_HAS_ACCESSKEY);
+ }
+ } else if (aName == nsGkAtoms::name) {
+ // Have to do this before clearing flag. See RemoveFromNameTable
+ RemoveFromNameTable();
+ if (!aValue || aValue->IsEmptyString()) {
+ ClearHasName();
+ }
+ } else if (aName == nsGkAtoms::contenteditable) {
+ if (aValue) {
+ // Set this before the attribute is set so that any subclass code that
+ // runs before the attribute is set won't think we're missing a
+ // contenteditable attr when we actually have one.
+ SetMayHaveContentEditableAttr();
+ }
+ }
+ if (!aValue && IsEventAttributeName(aName)) {
+ if (EventListenerManager* manager = GetExistingListenerManager()) {
+ manager->RemoveEventHandler(GetEventNameForAttr(aName));
+ }
+ }
+ }
+
+ return nsGenericHTMLElementBase::BeforeSetAttr(aNamespaceID, aName, aValue,
+ aNotify);
+}
+
+namespace {
+constexpr PopoverAttributeState ToPopoverAttributeState(
+ PopoverAttributeKeyword aPopoverAttributeKeyword) {
+ // See <https://html.spec.whatwg.org/#the-popover-attribute>.
+ switch (aPopoverAttributeKeyword) {
+ case PopoverAttributeKeyword::Auto:
+ return PopoverAttributeState::Auto;
+ case PopoverAttributeKeyword::EmptyString:
+ return PopoverAttributeState::Auto;
+ case PopoverAttributeKeyword::Manual:
+ return PopoverAttributeState::Manual;
+ default: {
+ MOZ_ASSERT_UNREACHABLE();
+ return PopoverAttributeState::None;
+ }
+ }
+}
+} // namespace
+
+void nsGenericHTMLElement::AfterSetPopoverAttr() {
+ auto mapPopoverState = [](const nsAttrValue* value) -> PopoverAttributeState {
+ if (value) {
+ MOZ_ASSERT(value->Type() == nsAttrValue::eEnum);
+ const auto popoverAttributeKeyword =
+ static_cast<PopoverAttributeKeyword>(value->GetEnumValue());
+ return ToPopoverAttributeState(popoverAttributeKeyword);
+ }
+
+ // The missing value default is the no popover state, see
+ // <https://html.spec.whatwg.org/multipage/popover.html#attr-popover>.
+ return PopoverAttributeState::None;
+ };
+
+ PopoverAttributeState newState =
+ mapPopoverState(GetParsedAttr(nsGkAtoms::popover));
+
+ const PopoverAttributeState oldState = GetPopoverAttributeState();
+
+ if (newState != oldState) {
+ PopoverPseudoStateUpdate(false, true);
+
+ if (IsPopoverOpen()) {
+ HidePopoverInternal(/* aFocusPreviousElement = */ true,
+ /* aFireEvents = */ true, IgnoreErrors());
+ // Event handlers could have removed the popover attribute, or changed
+ // its value.
+ // https://github.com/whatwg/html/issues/9034
+ newState = mapPopoverState(GetParsedAttr(nsGkAtoms::popover));
+ }
+
+ if (newState == PopoverAttributeState::None) {
+ ClearPopoverData();
+ RemoveStates(ElementState::POPOVER_OPEN);
+ } else {
+ // TODO: what if `HidePopoverInternal` called `ShowPopup()`?
+ EnsurePopoverData().SetPopoverAttributeState(newState);
+ }
+ }
+}
+
+void nsGenericHTMLElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (IsEventAttributeName(aName) && aValue) {
+ MOZ_ASSERT(aValue->Type() == nsAttrValue::eString,
+ "Expected string value for script body");
+ SetEventHandler(GetEventNameForAttr(aName), aValue->GetStringValue());
+ } else if (aNotify && aName == nsGkAtoms::spellcheck) {
+ SyncEditorsOnSubtree(this);
+ } else if (aName == nsGkAtoms::popover &&
+ StaticPrefs::dom_element_popover_enabled()) {
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod("nsGenericHTMLElement::AfterSetPopoverAttr", this,
+ &nsGenericHTMLElement::AfterSetPopoverAttr));
+ } else if (aName == nsGkAtoms::popovertarget) {
+ ClearExplicitlySetAttrElement(nsGkAtoms::popovertarget);
+ } else if (aName == nsGkAtoms::dir) {
+ auto dir = Directionality::Ltr;
+ // A boolean tracking whether we need to recompute our directionality.
+ // This needs to happen after we update our internal "dir" attribute
+ // state but before we call SetDirectionalityOnDescendants.
+ bool recomputeDirectionality = false;
+ ElementState dirStates;
+ if (aValue && aValue->Type() == nsAttrValue::eEnum) {
+ SetHasValidDir();
+ dirStates |= ElementState::HAS_DIR_ATTR;
+ auto dirValue = Directionality(aValue->GetEnumValue());
+ if (dirValue == Directionality::Auto) {
+ dirStates |= ElementState::HAS_DIR_ATTR_LIKE_AUTO;
+ } else {
+ dir = dirValue;
+ SetDirectionality(dir, aNotify);
+ if (dirValue == Directionality::Ltr) {
+ dirStates |= ElementState::HAS_DIR_ATTR_LTR;
+ } else {
+ MOZ_ASSERT(dirValue == Directionality::Rtl);
+ dirStates |= ElementState::HAS_DIR_ATTR_RTL;
+ }
+ }
+ } else {
+ if (aValue) {
+ // We have a value, just not a valid one.
+ dirStates |= ElementState::HAS_DIR_ATTR;
+ }
+ ClearHasValidDir();
+ if (NodeInfo()->Equals(nsGkAtoms::bdi)) {
+ dirStates |= ElementState::HAS_DIR_ATTR_LIKE_AUTO;
+ } else {
+ recomputeDirectionality = true;
+ }
+ }
+ // Now figure out what's changed about our dir states.
+ ElementState oldDirStates = State() & ElementState::DIR_ATTR_STATES;
+ ElementState changedStates = dirStates ^ oldDirStates;
+ if (!changedStates.IsEmpty()) {
+ ToggleStates(changedStates, aNotify);
+ }
+ if (recomputeDirectionality) {
+ dir = RecomputeDirectionality(this, aNotify);
+ }
+ SetDirectionalityOnDescendants(this, dir, aNotify);
+ } else if (aName == nsGkAtoms::contenteditable) {
+ int32_t editableCountDelta = 0;
+ if (aOldValue && (aOldValue->Equals(u"true"_ns, eIgnoreCase) ||
+ aOldValue->Equals(u""_ns, eIgnoreCase))) {
+ editableCountDelta = -1;
+ }
+ if (aValue && (aValue->Equals(u"true"_ns, eIgnoreCase) ||
+ aValue->Equals(u""_ns, eIgnoreCase))) {
+ ++editableCountDelta;
+ }
+ ChangeEditableState(editableCountDelta);
+ } else if (aName == nsGkAtoms::accesskey) {
+ if (aValue && !aValue->Equals(u""_ns, eIgnoreCase)) {
+ SetFlags(NODE_HAS_ACCESSKEY);
+ RegUnRegAccessKey(true);
+ }
+ } else if (aName == nsGkAtoms::inert) {
+ if (aValue) {
+ AddStates(ElementState::INERT);
+ } else {
+ RemoveStates(ElementState::INERT);
+ }
+ } else if (aName == nsGkAtoms::name) {
+ if (aValue && !aValue->Equals(u""_ns, eIgnoreCase)) {
+ // This may not be quite right because we can have subclass code run
+ // before here. But in practice subclasses don't care about this flag,
+ // and in particular selector matching does not care. Otherwise we'd
+ // want to handle it like we handle id attributes (in PreIdMaybeChange
+ // and PostIdMaybeChange).
+ SetHasName();
+ if (CanHaveName(NodeInfo()->NameAtom())) {
+ AddToNameTable(aValue->GetAtomValue());
+ }
+ }
+ } else if (aName == nsGkAtoms::inputmode ||
+ aName == nsGkAtoms::enterkeyhint) {
+ if (nsFocusManager::GetFocusedElementStatic() == this) {
+ if (const nsPresContext* presContext =
+ GetPresContext(eForComposedDoc)) {
+ IMEContentObserver* observer =
+ IMEStateManager::GetActiveContentObserver();
+ if (observer && observer->IsObserving(*presContext, this)) {
+ if (RefPtr<EditorBase> editorBase = GetEditorWithoutCreation()) {
+ IMEState newState;
+ editorBase->GetPreferredIMEState(&newState);
+ OwningNonNull<nsGenericHTMLElement> kungFuDeathGrip(*this);
+ IMEStateManager::UpdateIMEState(
+ newState, kungFuDeathGrip, *editorBase,
+ {IMEStateManager::UpdateIMEStateOption::ForceUpdate,
+ IMEStateManager::UpdateIMEStateOption::
+ DontCommitComposition});
+ }
+ }
+ }
+ }
+ }
+
+ // The nonce will be copied over to an internal slot and cleared from the
+ // Element within BindToTree to avoid CSS Selector nonce exfiltration if
+ // the CSP list contains a header-delivered CSP.
+ if (nsGkAtoms::nonce == aName) {
+ if (aValue) {
+ SetNonce(aValue->GetStringValue());
+ if (OwnerDoc()->GetHasCSPDeliveredThroughHeader()) {
+ SetFlags(NODE_HAS_NONCE_AND_HEADER_CSP);
+ }
+ } else {
+ RemoveNonce();
+ }
+ }
+ }
+
+ return nsGenericHTMLElementBase::AfterSetAttr(
+ aNamespaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+EventListenerManager* nsGenericHTMLElement::GetEventListenerManagerForAttr(
+ nsAtom* aAttrName, bool* aDefer) {
+ // Attributes on the body and frameset tags get set on the global object
+ if ((mNodeInfo->Equals(nsGkAtoms::body) ||
+ mNodeInfo->Equals(nsGkAtoms::frameset)) &&
+ // We only forward some event attributes from body/frameset to window
+ (0
+#define EVENT(name_, id_, type_, struct_) /* nothing */
+#define FORWARDED_EVENT(name_, id_, type_, struct_) \
+ || nsGkAtoms::on##name_ == aAttrName
+#define WINDOW_EVENT FORWARDED_EVENT
+#include "mozilla/EventNameList.h" // IWYU pragma: keep
+#undef WINDOW_EVENT
+#undef FORWARDED_EVENT
+#undef EVENT
+ )) {
+ nsPIDOMWindowInner* win;
+
+ // If we have a document, and it has a window, add the event
+ // listener on the window (the inner window). If not, proceed as
+ // normal.
+ // XXXbz sXBL/XBL2 issue: should we instead use GetComposedDoc() here,
+ // override BindToTree for those classes and munge event listeners there?
+ Document* document = OwnerDoc();
+
+ *aDefer = false;
+ if ((win = document->GetInnerWindow())) {
+ nsCOMPtr<EventTarget> piTarget(do_QueryInterface(win));
+
+ return piTarget->GetOrCreateListenerManager();
+ }
+
+ return nullptr;
+ }
+
+ return nsGenericHTMLElementBase::GetEventListenerManagerForAttr(aAttrName,
+ aDefer);
+}
+
+#define EVENT(name_, id_, type_, struct_) /* nothing; handled by nsINode */
+#define FORWARDED_EVENT(name_, id_, type_, struct_) \
+ EventHandlerNonNull* nsGenericHTMLElement::GetOn##name_() { \
+ if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \
+ /* XXXbz note to self: add tests for this! */ \
+ if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \
+ nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \
+ return globalWin->GetOn##name_(); \
+ } \
+ return nullptr; \
+ } \
+ \
+ return nsINode::GetOn##name_(); \
+ } \
+ void nsGenericHTMLElement::SetOn##name_(EventHandlerNonNull* handler) { \
+ if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \
+ if (!win) { \
+ return; \
+ } \
+ \
+ nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \
+ return globalWin->SetOn##name_(handler); \
+ } \
+ \
+ return nsINode::SetOn##name_(handler); \
+ }
+#define ERROR_EVENT(name_, id_, type_, struct_) \
+ already_AddRefed<EventHandlerNonNull> nsGenericHTMLElement::GetOn##name_() { \
+ if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \
+ /* XXXbz note to self: add tests for this! */ \
+ if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \
+ nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \
+ OnErrorEventHandlerNonNull* errorHandler = globalWin->GetOn##name_(); \
+ if (errorHandler) { \
+ RefPtr<EventHandlerNonNull> handler = \
+ new EventHandlerNonNull(errorHandler); \
+ return handler.forget(); \
+ } \
+ } \
+ return nullptr; \
+ } \
+ \
+ RefPtr<EventHandlerNonNull> handler = nsINode::GetOn##name_(); \
+ return handler.forget(); \
+ } \
+ void nsGenericHTMLElement::SetOn##name_(EventHandlerNonNull* handler) { \
+ if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \
+ nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \
+ if (!win) { \
+ return; \
+ } \
+ \
+ nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \
+ RefPtr<OnErrorEventHandlerNonNull> errorHandler; \
+ if (handler) { \
+ errorHandler = new OnErrorEventHandlerNonNull(handler); \
+ } \
+ return globalWin->SetOn##name_(errorHandler); \
+ } \
+ \
+ return nsINode::SetOn##name_(handler); \
+ }
+#include "mozilla/EventNameList.h" // IWYU pragma: keep
+#undef ERROR_EVENT
+#undef FORWARDED_EVENT
+#undef EVENT
+
+void nsGenericHTMLElement::GetBaseTarget(nsAString& aBaseTarget) const {
+ OwnerDoc()->GetBaseTarget(aBaseTarget);
+}
+
+//----------------------------------------------------------------------
+
+bool nsGenericHTMLElement::ParseAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::dir) {
+ return aResult.ParseEnumValue(aValue, kDirTable, false);
+ }
+
+ if (aAttribute == nsGkAtoms::popover &&
+ StaticPrefs::dom_element_popover_enabled()) {
+ return aResult.ParseEnumValue(aValue, kPopoverTable, false,
+ kPopoverTableInvalidValueDefault);
+ }
+
+ if (aAttribute == nsGkAtoms::tabindex) {
+ return aResult.ParseIntValue(aValue);
+ }
+
+ if (aAttribute == nsGkAtoms::referrerpolicy) {
+ return ParseReferrerAttribute(aValue, aResult);
+ }
+
+ if (aAttribute == nsGkAtoms::name) {
+ // Store name as an atom. name="" means that the element has no name,
+ // not that it has an empty string as the name.
+ if (aValue.IsEmpty()) {
+ return false;
+ }
+ aResult.ParseAtom(aValue);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::contenteditable ||
+ aAttribute == nsGkAtoms::translate) {
+ aResult.ParseAtom(aValue);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::rel) {
+ aResult.ParseAtomArray(aValue);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::inputmode) {
+ return aResult.ParseEnumValue(aValue, kInputmodeTable, false);
+ }
+
+ if (aAttribute == nsGkAtoms::enterkeyhint) {
+ return aResult.ParseEnumValue(aValue, kEnterKeyHintTable, false);
+ }
+
+ if (aAttribute == nsGkAtoms::autocapitalize) {
+ return aResult.ParseEnumValue(aValue, kAutocapitalizeTable, false);
+ }
+ }
+
+ return nsGenericHTMLElementBase::ParseAttribute(
+ aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult);
+}
+
+bool nsGenericHTMLElement::ParseBackgroundAttribute(int32_t aNamespaceID,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None &&
+ aAttribute == nsGkAtoms::background && !aValue.IsEmpty()) {
+ // Resolve url to an absolute url
+ Document* doc = OwnerDoc();
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = nsContentUtils::NewURIWithDocumentCharset(
+ getter_AddRefs(uri), aValue, doc, GetBaseURI());
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ aResult.SetTo(uri, &aValue);
+ return true;
+ }
+
+ return false;
+}
+
+bool nsGenericHTMLElement::IsAttributeMapped(const nsAtom* aAttribute) const {
+ static const MappedAttributeEntry* const map[] = {sCommonAttributeMap};
+
+ return FindAttributeDependence(aAttribute, map);
+}
+
+nsMapRuleToAttributesFunc nsGenericHTMLElement::GetAttributeMappingFunction()
+ const {
+ return &MapCommonAttributesInto;
+}
+
+nsIFormControlFrame* nsGenericHTMLElement::GetFormControlFrame(
+ bool aFlushFrames) {
+ auto flushType = aFlushFrames ? FlushType::Frames : FlushType::None;
+ nsIFrame* frame = GetPrimaryFrame(flushType);
+ if (!frame) {
+ return nullptr;
+ }
+
+ if (nsIFormControlFrame* f = do_QueryFrame(frame)) {
+ return f;
+ }
+
+ // If we have generated content, the primary frame will be a wrapper frame...
+ // Our real frame will be in its child list.
+ //
+ // FIXME(emilio): I don't think that's true... See bug 155957 for test-cases
+ // though, we should figure out whether this is still needed.
+ for (nsIFrame* kid : frame->PrincipalChildList()) {
+ if (nsIFormControlFrame* f = do_QueryFrame(kid)) {
+ return f;
+ }
+ }
+
+ return nullptr;
+}
+
+static const nsAttrValue::EnumTable kDivAlignTable[] = {
+ {"left", StyleTextAlign::MozLeft},
+ {"right", StyleTextAlign::MozRight},
+ {"center", StyleTextAlign::MozCenter},
+ {"middle", StyleTextAlign::MozCenter},
+ {"justify", StyleTextAlign::Justify},
+ {nullptr, 0}};
+
+static const nsAttrValue::EnumTable kFrameborderTable[] = {
+ {"yes", FrameBorderProperty::Yes},
+ {"no", FrameBorderProperty::No},
+ {"1", FrameBorderProperty::One},
+ {"0", FrameBorderProperty::Zero},
+ {nullptr, 0}};
+
+// TODO(emilio): Nobody uses the parsed attribute here.
+static const nsAttrValue::EnumTable kScrollingTable[] = {
+ {"yes", ScrollingAttribute::Yes},
+ {"no", ScrollingAttribute::No},
+ {"on", ScrollingAttribute::On},
+ {"off", ScrollingAttribute::Off},
+ {"scroll", ScrollingAttribute::Scroll},
+ {"noscroll", ScrollingAttribute::Noscroll},
+ {"auto", ScrollingAttribute::Auto},
+ {nullptr, 0}};
+
+static const nsAttrValue::EnumTable kTableVAlignTable[] = {
+ {"top", StyleVerticalAlignKeyword::Top},
+ {"middle", StyleVerticalAlignKeyword::Middle},
+ {"bottom", StyleVerticalAlignKeyword::Bottom},
+ {"baseline", StyleVerticalAlignKeyword::Baseline},
+ {nullptr, 0}};
+
+bool nsGenericHTMLElement::ParseAlignValue(const nsAString& aString,
+ nsAttrValue& aResult) {
+ static const nsAttrValue::EnumTable kAlignTable[] = {
+ {"left", StyleTextAlign::Left},
+ {"right", StyleTextAlign::Right},
+
+ {"top", StyleVerticalAlignKeyword::Top},
+ {"middle", StyleVerticalAlignKeyword::MozMiddleWithBaseline},
+
+ // Intentionally not bottom.
+ {"bottom", StyleVerticalAlignKeyword::Baseline},
+
+ {"center", StyleVerticalAlignKeyword::MozMiddleWithBaseline},
+ {"baseline", StyleVerticalAlignKeyword::Baseline},
+
+ {"texttop", StyleVerticalAlignKeyword::TextTop},
+ {"absmiddle", StyleVerticalAlignKeyword::Middle},
+ {"abscenter", StyleVerticalAlignKeyword::Middle},
+ {"absbottom", StyleVerticalAlignKeyword::Bottom},
+ {nullptr, 0}};
+
+ static_assert(uint8_t(StyleTextAlign::Left) !=
+ uint8_t(StyleVerticalAlignKeyword::Top) &&
+ uint8_t(StyleTextAlign::Left) !=
+ uint8_t(StyleVerticalAlignKeyword::MozMiddleWithBaseline) &&
+ uint8_t(StyleTextAlign::Left) !=
+ uint8_t(StyleVerticalAlignKeyword::Baseline) &&
+ uint8_t(StyleTextAlign::Left) !=
+ uint8_t(StyleVerticalAlignKeyword::TextTop) &&
+ uint8_t(StyleTextAlign::Left) !=
+ uint8_t(StyleVerticalAlignKeyword::Middle) &&
+ uint8_t(StyleTextAlign::Left) !=
+ uint8_t(StyleVerticalAlignKeyword::Bottom));
+
+ static_assert(uint8_t(StyleTextAlign::Right) !=
+ uint8_t(StyleVerticalAlignKeyword::Top) &&
+ uint8_t(StyleTextAlign::Right) !=
+ uint8_t(StyleVerticalAlignKeyword::MozMiddleWithBaseline) &&
+ uint8_t(StyleTextAlign::Right) !=
+ uint8_t(StyleVerticalAlignKeyword::Baseline) &&
+ uint8_t(StyleTextAlign::Right) !=
+ uint8_t(StyleVerticalAlignKeyword::TextTop) &&
+ uint8_t(StyleTextAlign::Right) !=
+ uint8_t(StyleVerticalAlignKeyword::Middle) &&
+ uint8_t(StyleTextAlign::Right) !=
+ uint8_t(StyleVerticalAlignKeyword::Bottom));
+
+ return aResult.ParseEnumValue(aString, kAlignTable, false);
+}
+
+//----------------------------------------
+
+static const nsAttrValue::EnumTable kTableHAlignTable[] = {
+ {"left", StyleTextAlign::Left}, {"right", StyleTextAlign::Right},
+ {"center", StyleTextAlign::Center}, {"char", StyleTextAlign::Char},
+ {"justify", StyleTextAlign::Justify}, {nullptr, 0}};
+
+bool nsGenericHTMLElement::ParseTableHAlignValue(const nsAString& aString,
+ nsAttrValue& aResult) {
+ return aResult.ParseEnumValue(aString, kTableHAlignTable, false);
+}
+
+//----------------------------------------
+
+// This table is used for td, th, tr, col, thead, tbody and tfoot.
+static const nsAttrValue::EnumTable kTableCellHAlignTable[] = {
+ {"left", StyleTextAlign::MozLeft},
+ {"right", StyleTextAlign::MozRight},
+ {"center", StyleTextAlign::MozCenter},
+ {"char", StyleTextAlign::Char},
+ {"justify", StyleTextAlign::Justify},
+ {"middle", StyleTextAlign::MozCenter},
+ {"absmiddle", StyleTextAlign::Center},
+ {nullptr, 0}};
+
+bool nsGenericHTMLElement::ParseTableCellHAlignValue(const nsAString& aString,
+ nsAttrValue& aResult) {
+ return aResult.ParseEnumValue(aString, kTableCellHAlignTable, false);
+}
+
+//----------------------------------------
+
+bool nsGenericHTMLElement::ParseTableVAlignValue(const nsAString& aString,
+ nsAttrValue& aResult) {
+ return aResult.ParseEnumValue(aString, kTableVAlignTable, false);
+}
+
+bool nsGenericHTMLElement::ParseDivAlignValue(const nsAString& aString,
+ nsAttrValue& aResult) {
+ return aResult.ParseEnumValue(aString, kDivAlignTable, false);
+}
+
+bool nsGenericHTMLElement::ParseImageAttribute(nsAtom* aAttribute,
+ const nsAString& aString,
+ nsAttrValue& aResult) {
+ if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height ||
+ aAttribute == nsGkAtoms::hspace || aAttribute == nsGkAtoms::vspace) {
+ return aResult.ParseHTMLDimension(aString);
+ }
+ if (aAttribute == nsGkAtoms::border) {
+ return aResult.ParseNonNegativeIntValue(aString);
+ }
+ return false;
+}
+
+bool nsGenericHTMLElement::ParseReferrerAttribute(const nsAString& aString,
+ nsAttrValue& aResult) {
+ using mozilla::dom::ReferrerInfo;
+ static const nsAttrValue::EnumTable kReferrerPolicyTable[] = {
+ {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::No_referrer),
+ static_cast<int16_t>(ReferrerPolicy::No_referrer)},
+ {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Origin),
+ static_cast<int16_t>(ReferrerPolicy::Origin)},
+ {ReferrerInfo::ReferrerPolicyToString(
+ ReferrerPolicy::Origin_when_cross_origin),
+ static_cast<int16_t>(ReferrerPolicy::Origin_when_cross_origin)},
+ {ReferrerInfo::ReferrerPolicyToString(
+ ReferrerPolicy::No_referrer_when_downgrade),
+ static_cast<int16_t>(ReferrerPolicy::No_referrer_when_downgrade)},
+ {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Unsafe_url),
+ static_cast<int16_t>(ReferrerPolicy::Unsafe_url)},
+ {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Strict_origin),
+ static_cast<int16_t>(ReferrerPolicy::Strict_origin)},
+ {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Same_origin),
+ static_cast<int16_t>(ReferrerPolicy::Same_origin)},
+ {ReferrerInfo::ReferrerPolicyToString(
+ ReferrerPolicy::Strict_origin_when_cross_origin),
+ static_cast<int16_t>(ReferrerPolicy::Strict_origin_when_cross_origin)},
+ {nullptr, ReferrerPolicy::_empty}};
+ return aResult.ParseEnumValue(aString, kReferrerPolicyTable, false);
+}
+
+bool nsGenericHTMLElement::ParseFrameborderValue(const nsAString& aString,
+ nsAttrValue& aResult) {
+ return aResult.ParseEnumValue(aString, kFrameborderTable, false);
+}
+
+bool nsGenericHTMLElement::ParseScrollingValue(const nsAString& aString,
+ nsAttrValue& aResult) {
+ return aResult.ParseEnumValue(aString, kScrollingTable, false);
+}
+
+static inline void MapLangAttributeInto(MappedDeclarationsBuilder& aBuilder) {
+ const nsAttrValue* langValue = aBuilder.GetAttr(nsGkAtoms::lang);
+ if (!langValue) {
+ return;
+ }
+ MOZ_ASSERT(langValue->Type() == nsAttrValue::eAtom);
+ aBuilder.SetIdentAtomValueIfUnset(eCSSProperty__x_lang,
+ langValue->GetAtomValue());
+ if (!aBuilder.PropertyIsSet(eCSSProperty_text_emphasis_position)) {
+ const nsAtom* lang = langValue->GetAtomValue();
+ if (nsStyleUtil::MatchesLanguagePrefix(lang, u"zh")) {
+ aBuilder.SetKeywordValue(eCSSProperty_text_emphasis_position,
+ StyleTextEmphasisPosition::UNDER._0);
+ } else if (nsStyleUtil::MatchesLanguagePrefix(lang, u"ja") ||
+ nsStyleUtil::MatchesLanguagePrefix(lang, u"mn")) {
+ // This branch is currently no part of the spec.
+ // See bug 1040668 comment 69 and comment 75.
+ aBuilder.SetKeywordValue(eCSSProperty_text_emphasis_position,
+ StyleTextEmphasisPosition::OVER._0);
+ }
+ }
+}
+
+/**
+ * Handle attributes common to all html elements
+ */
+void nsGenericHTMLElement::MapCommonAttributesIntoExceptHidden(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty__moz_user_modify)) {
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::contenteditable);
+ if (value) {
+ if (value->Equals(nsGkAtoms::_empty, eCaseMatters) ||
+ value->Equals(nsGkAtoms::_true, eIgnoreCase)) {
+ aBuilder.SetKeywordValue(eCSSProperty__moz_user_modify,
+ StyleUserModify::ReadWrite);
+ } else if (value->Equals(nsGkAtoms::_false, eIgnoreCase)) {
+ aBuilder.SetKeywordValue(eCSSProperty__moz_user_modify,
+ StyleUserModify::ReadOnly);
+ }
+ }
+ }
+
+ MapLangAttributeInto(aBuilder);
+}
+
+void nsGenericHTMLElement::MapCommonAttributesInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapCommonAttributesIntoExceptHidden(aBuilder);
+ if (!aBuilder.PropertyIsSet(eCSSProperty_display)) {
+ if (aBuilder.GetAttr(nsGkAtoms::hidden)) {
+ aBuilder.SetKeywordValue(eCSSProperty_display, StyleDisplay::None._0);
+ }
+ }
+}
+
+/* static */
+const nsGenericHTMLElement::MappedAttributeEntry
+ nsGenericHTMLElement::sCommonAttributeMap[] = {{nsGkAtoms::contenteditable},
+ {nsGkAtoms::lang},
+ {nsGkAtoms::hidden},
+ {nullptr}};
+
+/* static */
+const Element::MappedAttributeEntry
+ nsGenericHTMLElement::sImageMarginSizeAttributeMap[] = {{nsGkAtoms::width},
+ {nsGkAtoms::height},
+ {nsGkAtoms::hspace},
+ {nsGkAtoms::vspace},
+ {nullptr}};
+
+/* static */
+const Element::MappedAttributeEntry
+ nsGenericHTMLElement::sImageAlignAttributeMap[] = {{nsGkAtoms::align},
+ {nullptr}};
+
+/* static */
+const Element::MappedAttributeEntry
+ nsGenericHTMLElement::sDivAlignAttributeMap[] = {{nsGkAtoms::align},
+ {nullptr}};
+
+/* static */
+const Element::MappedAttributeEntry
+ nsGenericHTMLElement::sImageBorderAttributeMap[] = {{nsGkAtoms::border},
+ {nullptr}};
+
+/* static */
+const Element::MappedAttributeEntry
+ nsGenericHTMLElement::sBackgroundAttributeMap[] = {
+ {nsGkAtoms::background}, {nsGkAtoms::bgcolor}, {nullptr}};
+
+/* static */
+const Element::MappedAttributeEntry
+ nsGenericHTMLElement::sBackgroundColorAttributeMap[] = {
+ {nsGkAtoms::bgcolor}, {nullptr}};
+
+void nsGenericHTMLElement::MapImageAlignAttributeInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align);
+ if (value && value->Type() == nsAttrValue::eEnum) {
+ int32_t align = value->GetEnumValue();
+ if (!aBuilder.PropertyIsSet(eCSSProperty_float)) {
+ if (align == uint8_t(StyleTextAlign::Left)) {
+ aBuilder.SetKeywordValue(eCSSProperty_float, StyleFloat::Left);
+ } else if (align == uint8_t(StyleTextAlign::Right)) {
+ aBuilder.SetKeywordValue(eCSSProperty_float, StyleFloat::Right);
+ }
+ }
+ if (!aBuilder.PropertyIsSet(eCSSProperty_vertical_align)) {
+ switch (align) {
+ case uint8_t(StyleTextAlign::Left):
+ case uint8_t(StyleTextAlign::Right):
+ break;
+ default:
+ aBuilder.SetKeywordValue(eCSSProperty_vertical_align, align);
+ break;
+ }
+ }
+ }
+}
+
+void nsGenericHTMLElement::MapDivAlignAttributeInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty_text_align)) {
+ // align: enum
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align);
+ if (value && value->Type() == nsAttrValue::eEnum)
+ aBuilder.SetKeywordValue(eCSSProperty_text_align, value->GetEnumValue());
+ }
+}
+
+void nsGenericHTMLElement::MapVAlignAttributeInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty_vertical_align)) {
+ // align: enum
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::valign);
+ if (value && value->Type() == nsAttrValue::eEnum)
+ aBuilder.SetKeywordValue(eCSSProperty_vertical_align,
+ value->GetEnumValue());
+ }
+}
+
+void nsGenericHTMLElement::MapDimensionAttributeInto(
+ MappedDeclarationsBuilder& aBuilder, nsCSSPropertyID aProp,
+ const nsAttrValue& aValue) {
+ MOZ_ASSERT(!aBuilder.PropertyIsSet(aProp),
+ "Why mapping the same property twice?");
+ if (aValue.Type() == nsAttrValue::eInteger) {
+ return aBuilder.SetPixelValue(aProp, aValue.GetIntegerValue());
+ }
+ if (aValue.Type() == nsAttrValue::ePercent) {
+ return aBuilder.SetPercentValue(aProp, aValue.GetPercentValue());
+ }
+ if (aValue.Type() == nsAttrValue::eDoubleValue) {
+ return aBuilder.SetPixelValue(aProp, aValue.GetDoubleValue());
+ }
+}
+
+void nsGenericHTMLElement::MapImageMarginAttributeInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ // hspace: value
+ if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::hspace)) {
+ MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_left, *value);
+ MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_right, *value);
+ }
+
+ // vspace: value
+ if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::vspace)) {
+ MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_top, *value);
+ MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_bottom, *value);
+ }
+}
+
+void nsGenericHTMLElement::MapWidthAttributeInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::width)) {
+ MapDimensionAttributeInto(aBuilder, eCSSProperty_width, *value);
+ }
+}
+
+void nsGenericHTMLElement::MapHeightAttributeInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::height)) {
+ MapDimensionAttributeInto(aBuilder, eCSSProperty_height, *value);
+ }
+}
+
+void nsGenericHTMLElement::DoMapAspectRatio(
+ const nsAttrValue& aWidth, const nsAttrValue& aHeight,
+ MappedDeclarationsBuilder& aBuilder) {
+ Maybe<double> w;
+ if (aWidth.Type() == nsAttrValue::eInteger) {
+ w.emplace(aWidth.GetIntegerValue());
+ } else if (aWidth.Type() == nsAttrValue::eDoubleValue) {
+ w.emplace(aWidth.GetDoubleValue());
+ }
+
+ Maybe<double> h;
+ if (aHeight.Type() == nsAttrValue::eInteger) {
+ h.emplace(aHeight.GetIntegerValue());
+ } else if (aHeight.Type() == nsAttrValue::eDoubleValue) {
+ h.emplace(aHeight.GetDoubleValue());
+ }
+
+ if (w && h) {
+ aBuilder.SetAspectRatio(*w, *h);
+ }
+}
+
+void nsGenericHTMLElement::MapImageSizeAttributesInto(
+ MappedDeclarationsBuilder& aBuilder, MapAspectRatio aMapAspectRatio) {
+ auto* width = aBuilder.GetAttr(nsGkAtoms::width);
+ auto* height = aBuilder.GetAttr(nsGkAtoms::height);
+ if (width) {
+ MapDimensionAttributeInto(aBuilder, eCSSProperty_width, *width);
+ }
+ if (height) {
+ MapDimensionAttributeInto(aBuilder, eCSSProperty_height, *height);
+ }
+ if (aMapAspectRatio == MapAspectRatio::Yes && width && height) {
+ DoMapAspectRatio(*width, *height, aBuilder);
+ }
+}
+
+void nsGenericHTMLElement::MapAspectRatioInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ auto* width = aBuilder.GetAttr(nsGkAtoms::width);
+ auto* height = aBuilder.GetAttr(nsGkAtoms::height);
+ if (width && height) {
+ DoMapAspectRatio(*width, *height, aBuilder);
+ }
+}
+
+void nsGenericHTMLElement::MapImageBorderAttributeInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ // border: pixels
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::border);
+ if (!value) return;
+
+ nscoord val = 0;
+ if (value->Type() == nsAttrValue::eInteger) val = value->GetIntegerValue();
+
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_top_width, (float)val);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_right_width, (float)val);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_bottom_width, (float)val);
+ aBuilder.SetPixelValueIfUnset(eCSSProperty_border_left_width, (float)val);
+
+ aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_top_style,
+ StyleBorderStyle::Solid);
+ aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_right_style,
+ StyleBorderStyle::Solid);
+ aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_bottom_style,
+ StyleBorderStyle::Solid);
+ aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_left_style,
+ StyleBorderStyle::Solid);
+
+ aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_top_color);
+ aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_right_color);
+ aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_bottom_color);
+ aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_left_color);
+}
+
+void nsGenericHTMLElement::MapBackgroundInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ if (!aBuilder.PropertyIsSet(eCSSProperty_background_image)) {
+ // background
+ if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::background)) {
+ aBuilder.SetBackgroundImage(*value);
+ }
+ }
+}
+
+void nsGenericHTMLElement::MapBGColorInto(MappedDeclarationsBuilder& aBuilder) {
+ if (aBuilder.PropertyIsSet(eCSSProperty_background_color)) {
+ return;
+ }
+ const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::bgcolor);
+ nscolor color;
+ if (value && value->GetColorValue(color)) {
+ aBuilder.SetColorValue(eCSSProperty_background_color, color);
+ }
+}
+
+void nsGenericHTMLElement::MapBackgroundAttributesInto(
+ MappedDeclarationsBuilder& aBuilder) {
+ MapBackgroundInto(aBuilder);
+ MapBGColorInto(aBuilder);
+}
+
+//----------------------------------------------------------------------
+
+int32_t nsGenericHTMLElement::GetIntAttr(nsAtom* aAttr,
+ int32_t aDefault) const {
+ const nsAttrValue* attrVal = mAttrs.GetAttr(aAttr);
+ if (attrVal && attrVal->Type() == nsAttrValue::eInteger) {
+ return attrVal->GetIntegerValue();
+ }
+ return aDefault;
+}
+
+nsresult nsGenericHTMLElement::SetIntAttr(nsAtom* aAttr, int32_t aValue) {
+ nsAutoString value;
+ value.AppendInt(aValue);
+
+ return SetAttr(kNameSpaceID_None, aAttr, value, true);
+}
+
+uint32_t nsGenericHTMLElement::GetUnsignedIntAttr(nsAtom* aAttr,
+ uint32_t aDefault) const {
+ const nsAttrValue* attrVal = mAttrs.GetAttr(aAttr);
+ if (!attrVal || attrVal->Type() != nsAttrValue::eInteger) {
+ return aDefault;
+ }
+
+ return attrVal->GetIntegerValue();
+}
+
+uint32_t nsGenericHTMLElement::GetDimensionAttrAsUnsignedInt(
+ nsAtom* aAttr, uint32_t aDefault) const {
+ const nsAttrValue* attrVal = mAttrs.GetAttr(aAttr);
+ if (!attrVal) {
+ return aDefault;
+ }
+
+ if (attrVal->Type() == nsAttrValue::eInteger) {
+ return attrVal->GetIntegerValue();
+ }
+
+ if (attrVal->Type() == nsAttrValue::ePercent) {
+ // This is a nasty hack. When we parsed the value, we stored it as an
+ // ePercent, not eInteger, because there was a '%' after it in the string.
+ // But the spec says to basically re-parse the string as an integer.
+ // Luckily, we can just return the value we have stored. But
+ // GetPercentValue() divides it by 100, so we need to multiply it back.
+ return uint32_t(attrVal->GetPercentValue() * 100.0f);
+ }
+
+ if (attrVal->Type() == nsAttrValue::eDoubleValue) {
+ return uint32_t(attrVal->GetDoubleValue());
+ }
+
+ // Unfortunately, the set of values that are valid dimensions is not a
+ // superset of values that are valid unsigned ints. In particular "+100" is
+ // not a valid dimension, but should parse as the unsigned int "100". So if
+ // we got here and we don't have a valid dimension value, just try re-parsing
+ // the string we have as an integer.
+ nsAutoString val;
+ attrVal->ToString(val);
+ nsContentUtils::ParseHTMLIntegerResultFlags result;
+ int32_t parsedInt = nsContentUtils::ParseHTMLInteger(val, &result);
+ if ((result & nsContentUtils::eParseHTMLInteger_Error) || parsedInt < 0) {
+ return aDefault;
+ }
+
+ return parsedInt;
+}
+
+void nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr,
+ nsAString& aResult) const {
+ nsCOMPtr<nsIURI> uri;
+ bool hadAttr = GetURIAttr(aAttr, aBaseAttr, getter_AddRefs(uri));
+ if (!hadAttr) {
+ aResult.Truncate();
+ return;
+ }
+
+ if (!uri) {
+ // Just return the attr value
+ GetAttr(aAttr, aResult);
+ return;
+ }
+
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+ CopyUTF8toUTF16(spec, aResult);
+}
+
+bool nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr,
+ nsIURI** aURI) const {
+ *aURI = nullptr;
+
+ const nsAttrValue* attr = mAttrs.GetAttr(aAttr);
+ if (!attr) {
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> baseURI = GetBaseURI();
+
+ if (aBaseAttr) {
+ nsAutoString baseAttrValue;
+ if (GetAttr(aBaseAttr, baseAttrValue)) {
+ nsCOMPtr<nsIURI> baseAttrURI;
+ nsresult rv = nsContentUtils::NewURIWithDocumentCharset(
+ getter_AddRefs(baseAttrURI), baseAttrValue, OwnerDoc(), baseURI);
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ baseURI.swap(baseAttrURI);
+ }
+ }
+
+ // Don't care about return value. If it fails, we still want to
+ // return true, and *aURI will be null.
+ nsContentUtils::NewURIWithDocumentCharset(aURI, attr->GetStringValue(),
+ OwnerDoc(), baseURI);
+ return true;
+}
+
+bool nsGenericHTMLElement::IsLabelable() const {
+ return IsAnyOfHTMLElements(nsGkAtoms::progress, nsGkAtoms::meter);
+}
+
+/* static */
+bool nsGenericHTMLElement::MatchLabelsElement(Element* aElement,
+ int32_t aNamespaceID,
+ nsAtom* aAtom, void* aData) {
+ HTMLLabelElement* element = HTMLLabelElement::FromNode(aElement);
+ return element && element->GetControl() == aData;
+}
+
+already_AddRefed<nsINodeList> nsGenericHTMLElement::Labels() {
+ MOZ_ASSERT(IsLabelable(),
+ "Labels() only allow labelable elements to use it.");
+ nsExtendedDOMSlots* slots = ExtendedDOMSlots();
+
+ if (!slots->mLabelsList) {
+ slots->mLabelsList =
+ new nsLabelsNodeList(SubtreeRoot(), MatchLabelsElement, nullptr, this);
+ }
+
+ RefPtr<nsLabelsNodeList> labels = slots->mLabelsList;
+ return labels.forget();
+}
+
+// static
+bool nsGenericHTMLElement::LegacyTouchAPIEnabled(JSContext* aCx,
+ JSObject* aGlobal) {
+ return TouchEvent::LegacyAPIEnabled(aCx, aGlobal);
+}
+
+bool nsGenericHTMLElement::IsFormControlDefaultFocusable(
+ bool aWithMouse) const {
+ if (!aWithMouse) {
+ return true;
+ }
+ switch (StaticPrefs::accessibility_mouse_focuses_formcontrol()) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return !IsInChromeDocument();
+ }
+}
+
+//----------------------------------------------------------------------
+
+nsGenericHTMLFormElement::nsGenericHTMLFormElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElement(std::move(aNodeInfo)) {
+ // We should add the ElementState::ENABLED bit here as needed, but that
+ // depends on our type, which is not initialized yet. So we have to do this
+ // in subclasses. Same for a couple other bits.
+}
+
+void nsGenericHTMLFormElement::ClearForm(bool aRemoveFromForm,
+ bool aUnbindOrDelete) {
+ MOZ_ASSERT(IsFormAssociatedElement());
+
+ HTMLFormElement* form = GetFormInternal();
+ NS_ASSERTION((form != nullptr) == HasFlag(ADDED_TO_FORM),
+ "Form control should have had flag set correctly");
+
+ if (!form) {
+ return;
+ }
+
+ if (aRemoveFromForm) {
+ nsAutoString nameVal, idVal;
+ GetAttr(nsGkAtoms::name, nameVal);
+ GetAttr(nsGkAtoms::id, idVal);
+
+ form->RemoveElement(this, true);
+
+ if (!nameVal.IsEmpty()) {
+ form->RemoveElementFromTable(this, nameVal);
+ }
+
+ if (!idVal.IsEmpty()) {
+ form->RemoveElementFromTable(this, idVal);
+ }
+ }
+
+ UnsetFlags(ADDED_TO_FORM);
+ SetFormInternal(nullptr, false);
+ AfterClearForm(aUnbindOrDelete);
+}
+
+nsresult nsGenericHTMLFormElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsFormAssociatedElement()) {
+ // If @form is set, the element *has* to be in a composed document,
+ // otherwise it wouldn't be possible to find an element with the
+ // corresponding id. If @form isn't set, the element *has* to have a parent,
+ // otherwise it wouldn't be possible to find a form ancestor. We should not
+ // call UpdateFormOwner if none of these conditions are fulfilled.
+ if (HasAttr(nsGkAtoms::form) ? IsInComposedDoc() : aParent.IsContent()) {
+ UpdateFormOwner(true, nullptr);
+ }
+ }
+
+ // Set parent fieldset which should be used for the disabled state.
+ UpdateFieldSet(false);
+ return NS_OK;
+}
+
+void nsGenericHTMLFormElement::UnbindFromTree(bool aNullParent) {
+ // Save state before doing anything else.
+ SaveState();
+
+ if (IsFormAssociatedElement()) {
+ if (HTMLFormElement* form = GetFormInternal()) {
+ // Might need to unset form
+ if (aNullParent) {
+ // No more parent means no more form
+ ClearForm(true, true);
+ } else {
+ // Recheck whether we should still have an form.
+ if (HasAttr(nsGkAtoms::form) || !FindAncestorForm(form)) {
+ ClearForm(true, true);
+ } else {
+ UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT);
+ }
+ }
+ }
+
+ // We have to remove the form id observer if there was one.
+ // We will re-add one later if needed (during bind to tree).
+ if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None,
+ nsGkAtoms::form)) {
+ RemoveFormIdObserver();
+ }
+ }
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+
+ // The element might not have a fieldset anymore.
+ UpdateFieldSet(false);
+}
+
+void nsGenericHTMLFormElement::BeforeSetAttr(int32_t aNameSpaceID,
+ nsAtom* aName,
+ const nsAttrValue* aValue,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && IsFormAssociatedElement()) {
+ nsAutoString tmp;
+ HTMLFormElement* form = GetFormInternal();
+
+ // remove the control from the hashtable as needed
+
+ if (form && (aName == nsGkAtoms::name || aName == nsGkAtoms::id)) {
+ GetAttr(aName, tmp);
+
+ if (!tmp.IsEmpty()) {
+ form->RemoveElementFromTable(this, tmp);
+ }
+ }
+
+ if (form && aName == nsGkAtoms::type) {
+ GetAttr(nsGkAtoms::name, tmp);
+
+ if (!tmp.IsEmpty()) {
+ form->RemoveElementFromTable(this, tmp);
+ }
+
+ GetAttr(nsGkAtoms::id, tmp);
+
+ if (!tmp.IsEmpty()) {
+ form->RemoveElementFromTable(this, tmp);
+ }
+
+ form->RemoveElement(this, false);
+ }
+
+ if (aName == nsGkAtoms::form) {
+ // If @form isn't set or set to the empty string, there were no observer
+ // so we don't have to remove it.
+ if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None,
+ nsGkAtoms::form)) {
+ // The current form id observer is no longer needed.
+ // A new one may be added in AfterSetAttr.
+ RemoveFormIdObserver();
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue,
+ aNotify);
+}
+
+void nsGenericHTMLFormElement::AfterSetAttr(
+ int32_t aNameSpaceID, nsAtom* aName, const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aNameSpaceID == kNameSpaceID_None && IsFormAssociatedElement()) {
+ HTMLFormElement* form = GetFormInternal();
+
+ // add the control to the hashtable as needed
+ if (form && (aName == nsGkAtoms::name || aName == nsGkAtoms::id) &&
+ aValue && !aValue->IsEmptyString()) {
+ MOZ_ASSERT(aValue->Type() == nsAttrValue::eAtom,
+ "Expected atom value for name/id");
+ form->AddElementToTable(this,
+ nsDependentAtomString(aValue->GetAtomValue()));
+ }
+
+ if (form && aName == nsGkAtoms::type) {
+ nsAutoString tmp;
+
+ GetAttr(nsGkAtoms::name, tmp);
+
+ if (!tmp.IsEmpty()) {
+ form->AddElementToTable(this, tmp);
+ }
+
+ GetAttr(nsGkAtoms::id, tmp);
+
+ if (!tmp.IsEmpty()) {
+ form->AddElementToTable(this, tmp);
+ }
+
+ form->AddElement(this, false, aNotify);
+ }
+
+ if (aName == nsGkAtoms::form) {
+ // We need a new form id observer.
+ DocumentOrShadowRoot* docOrShadow =
+ GetUncomposedDocOrConnectedShadowRoot();
+ if (docOrShadow) {
+ Element* formIdElement = nullptr;
+ if (aValue && !aValue->IsEmptyString()) {
+ formIdElement = AddFormIdObserver();
+ }
+
+ // Because we have a new @form value (or no more @form), we have to
+ // update our form owner.
+ UpdateFormOwner(false, formIdElement);
+ }
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void nsGenericHTMLFormElement::ForgetFieldSet(nsIContent* aFieldset) {
+ MOZ_DIAGNOSTIC_ASSERT(IsFormAssociatedElement());
+ if (GetFieldSetInternal() == aFieldset) {
+ SetFieldSetInternal(nullptr);
+ }
+}
+
+Element* nsGenericHTMLFormElement::AddFormIdObserver() {
+ MOZ_ASSERT(IsFormAssociatedElement());
+
+ nsAutoString formId;
+ DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot();
+ GetAttr(nsGkAtoms::form, formId);
+ NS_ASSERTION(!formId.IsEmpty(),
+ "@form value should not be the empty string!");
+ RefPtr<nsAtom> atom = NS_Atomize(formId);
+
+ return docOrShadow->AddIDTargetObserver(atom, FormIdUpdated, this, false);
+}
+
+void nsGenericHTMLFormElement::RemoveFormIdObserver() {
+ MOZ_ASSERT(IsFormAssociatedElement());
+
+ DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot();
+ if (!docOrShadow) {
+ return;
+ }
+
+ nsAutoString formId;
+ GetAttr(nsGkAtoms::form, formId);
+ NS_ASSERTION(!formId.IsEmpty(),
+ "@form value should not be the empty string!");
+ RefPtr<nsAtom> atom = NS_Atomize(formId);
+
+ docOrShadow->RemoveIDTargetObserver(atom, FormIdUpdated, this, false);
+}
+
+/* static */
+bool nsGenericHTMLFormElement::FormIdUpdated(Element* aOldElement,
+ Element* aNewElement,
+ void* aData) {
+ nsGenericHTMLFormElement* element =
+ static_cast<nsGenericHTMLFormElement*>(aData);
+
+ NS_ASSERTION(element->IsHTMLElement(), "aData should be an HTML element");
+
+ element->UpdateFormOwner(false, aNewElement);
+
+ return true;
+}
+
+bool nsGenericHTMLFormElement::IsElementDisabledForEvents(WidgetEvent* aEvent,
+ nsIFrame* aFrame) {
+ MOZ_ASSERT(aEvent);
+
+ // Allow dispatch of CustomEvent and untrusted Events.
+ if (!aEvent->IsTrusted()) {
+ return false;
+ }
+
+ switch (aEvent->mMessage) {
+ case eAnimationStart:
+ case eAnimationEnd:
+ case eAnimationIteration:
+ case eAnimationCancel:
+ case eFormChange:
+ case eMouseMove:
+ case eMouseOver:
+ case eMouseOut:
+ case eMouseEnter:
+ case eMouseLeave:
+ case ePointerMove:
+ case ePointerOver:
+ case ePointerOut:
+ case ePointerEnter:
+ case ePointerLeave:
+ case eTransitionCancel:
+ case eTransitionEnd:
+ case eTransitionRun:
+ case eTransitionStart:
+ case eWheel:
+ case eLegacyMouseLineOrPageScroll:
+ case eLegacyMousePixelScroll:
+ return false;
+ case eFocus:
+ case eBlur:
+ case eFocusIn:
+ case eFocusOut:
+ case eKeyPress:
+ case eKeyUp:
+ case eKeyDown:
+ if (StaticPrefs::dom_forms_always_allow_key_and_focus_events_enabled()) {
+ return false;
+ }
+ [[fallthrough]];
+ case ePointerDown:
+ case ePointerUp:
+ case ePointerCancel:
+ case ePointerGotCapture:
+ case ePointerLostCapture:
+ if (StaticPrefs::dom_forms_always_allow_pointer_events_enabled()) {
+ return false;
+ }
+ [[fallthrough]];
+ default:
+ break;
+ }
+
+ if (aEvent->mSpecifiedEventType == nsGkAtoms::oninput) {
+ return false;
+ }
+
+ // FIXME(emilio): This poking at the style of the frame is slightly bogus
+ // unless we flush before every event, which we don't really want to do.
+ if (aFrame && aFrame->StyleUI()->UserInput() == StyleUserInput::None) {
+ return true;
+ }
+
+ return IsDisabled();
+}
+
+void nsGenericHTMLFormElement::UpdateFormOwner(bool aBindToTree,
+ Element* aFormIdElement) {
+ MOZ_ASSERT(IsFormAssociatedElement());
+ MOZ_ASSERT(!aBindToTree || !aFormIdElement,
+ "aFormIdElement shouldn't be set if aBindToTree is true!");
+
+ HTMLFormElement* form = GetFormInternal();
+ if (!aBindToTree) {
+ ClearForm(true, false);
+ form = nullptr;
+ }
+
+ HTMLFormElement* oldForm = form;
+ if (!form) {
+ // If @form is set, we have to use that to find the form.
+ nsAutoString formId;
+ if (GetAttr(nsGkAtoms::form, formId)) {
+ if (!formId.IsEmpty()) {
+ Element* element = nullptr;
+
+ if (aBindToTree) {
+ element = AddFormIdObserver();
+ } else {
+ element = aFormIdElement;
+ }
+
+ NS_ASSERTION(!IsInComposedDoc() ||
+ element == GetUncomposedDocOrConnectedShadowRoot()
+ ->GetElementById(formId),
+ "element should be equals to the current element "
+ "associated with the id in @form!");
+
+ if (element && element->IsHTMLElement(nsGkAtoms::form) &&
+ nsContentUtils::IsInSameAnonymousTree(this, element)) {
+ form = static_cast<HTMLFormElement*>(element);
+ SetFormInternal(form, aBindToTree);
+ }
+ }
+ } else {
+ // We now have a parent, so we may have picked up an ancestor form. Search
+ // for it. Note that if form is already set we don't want to do this,
+ // because that means someone (probably the content sink) has already set
+ // it to the right value. Also note that even if being bound here didn't
+ // change our parent, we still need to search, since our parent chain
+ // probably changed _somewhere_.
+ form = FindAncestorForm();
+ SetFormInternal(form, aBindToTree);
+ }
+ }
+
+ if (form && !HasFlag(ADDED_TO_FORM)) {
+ // Now we need to add ourselves to the form
+ nsAutoString nameVal, idVal;
+ GetAttr(nsGkAtoms::name, nameVal);
+ GetAttr(nsGkAtoms::id, idVal);
+
+ SetFlags(ADDED_TO_FORM);
+
+ // Notify only if we just found this form.
+ form->AddElement(this, true, oldForm == nullptr);
+
+ if (!nameVal.IsEmpty()) {
+ form->AddElementToTable(this, nameVal);
+ }
+
+ if (!idVal.IsEmpty()) {
+ form->AddElementToTable(this, idVal);
+ }
+ }
+}
+
+void nsGenericHTMLFormElement::UpdateFieldSet(bool aNotify) {
+ if (IsInNativeAnonymousSubtree() || !IsFormAssociatedElement()) {
+ MOZ_ASSERT_IF(IsFormAssociatedElement(), !GetFieldSetInternal());
+ return;
+ }
+
+ nsIContent* parent = nullptr;
+ nsIContent* prev = nullptr;
+ HTMLFieldSetElement* fieldset = GetFieldSetInternal();
+
+ for (parent = GetParent(); parent;
+ prev = parent, parent = parent->GetParent()) {
+ HTMLFieldSetElement* parentFieldset = HTMLFieldSetElement::FromNode(parent);
+ if (parentFieldset && (!prev || parentFieldset->GetFirstLegend() != prev)) {
+ if (fieldset == parentFieldset) {
+ // We already have the right fieldset;
+ return;
+ }
+
+ if (fieldset) {
+ fieldset->RemoveElement(this);
+ }
+ SetFieldSetInternal(parentFieldset);
+ parentFieldset->AddElement(this);
+
+ // The disabled state may have changed
+ FieldSetDisabledChanged(aNotify);
+ return;
+ }
+ }
+
+ // No fieldset found.
+ if (fieldset) {
+ fieldset->RemoveElement(this);
+ SetFieldSetInternal(nullptr);
+ // The disabled state may have changed
+ FieldSetDisabledChanged(aNotify);
+ }
+}
+
+void nsGenericHTMLFormElement::UpdateDisabledState(bool aNotify) {
+ if (!CanBeDisabled()) {
+ return;
+ }
+
+ HTMLFieldSetElement* fieldset = GetFieldSetInternal();
+ const bool isDisabled =
+ HasAttr(nsGkAtoms::disabled) || (fieldset && fieldset->IsDisabled());
+
+ const ElementState disabledStates =
+ isDisabled ? ElementState::DISABLED : ElementState::ENABLED;
+
+ ElementState oldDisabledStates = State() & ElementState::DISABLED_STATES;
+ ElementState changedStates = disabledStates ^ oldDisabledStates;
+
+ if (!changedStates.IsEmpty()) {
+ ToggleStates(changedStates, aNotify);
+ if (DoesReadOnlyApply()) {
+ // :disabled influences :read-only / :read-write.
+ UpdateReadOnlyState(aNotify);
+ }
+ }
+}
+
+bool nsGenericHTMLFormElement::IsReadOnlyInternal() const {
+ if (DoesReadOnlyApply()) {
+ return IsDisabled() || GetBoolAttr(nsGkAtoms::readonly);
+ }
+ return nsGenericHTMLElement::IsReadOnlyInternal();
+}
+
+void nsGenericHTMLFormElement::FieldSetDisabledChanged(bool aNotify) {
+ UpdateDisabledState(aNotify);
+}
+
+void nsGenericHTMLFormElement::SaveSubtreeState() {
+ SaveState();
+
+ nsGenericHTMLElement::SaveSubtreeState();
+}
+
+//----------------------------------------------------------------------
+
+void nsGenericHTMLElement::Click(CallerType aCallerType) {
+ if (HandlingClick()) {
+ return;
+ }
+
+ // There are two notions of disabled.
+ // "disabled":
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-disabled
+ // "actually disabled":
+ // https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled
+ // click() reads the former but IsDisabled() is for the latter. <fieldset> is
+ // included only in the latter, so we exclude it here.
+ // XXX(krosylight): What about <optgroup>? And should we add a separate method
+ // for this?
+ if (IsDisabled() &&
+ !(mNodeInfo->Equals(nsGkAtoms::fieldset) &&
+ StaticPrefs::dom_forms_fieldset_disable_only_descendants_enabled())) {
+ return;
+ }
+
+ // Strong in case the event kills it
+ nsCOMPtr<Document> doc = GetComposedDoc();
+
+ RefPtr<nsPresContext> context;
+ if (doc) {
+ PresShell* presShell = doc->GetPresShell();
+ if (!presShell) {
+ // We need the nsPresContext for dispatching the click event. In some
+ // rare cases we need to flush notifications to force creation of the
+ // nsPresContext here (for example when a script calls button.click()
+ // from script early during page load). We only flush the notifications
+ // if the PresShell hasn't been created yet, to limit the performance
+ // impact.
+ doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames);
+ presShell = doc->GetPresShell();
+ }
+ if (presShell) {
+ context = presShell->GetPresContext();
+ }
+ }
+
+ SetHandlingClick();
+
+ // Mark this event trusted if Click() is called from system code.
+ WidgetMouseEvent event(aCallerType == CallerType::System, eMouseClick,
+ nullptr, WidgetMouseEvent::eReal);
+ event.mFlags.mIsPositionless = true;
+ event.mInputSource = MouseEvent_Binding::MOZ_SOURCE_UNKNOWN;
+
+ EventDispatcher::Dispatch(this, context, &event);
+
+ ClearHandlingClick();
+}
+
+bool nsGenericHTMLElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ MOZ_ASSERT(aIsFocusable);
+ MOZ_ASSERT(aTabIndex);
+ if (ShadowRoot* root = GetShadowRoot()) {
+ if (root->DelegatesFocus()) {
+ *aIsFocusable = false;
+ return true;
+ }
+ }
+
+ if (!IsInComposedDoc() || IsInDesignMode()) {
+ // In designMode documents we only allow focusing the document.
+ *aTabIndex = -1;
+ *aIsFocusable = false;
+ return true;
+ }
+
+ *aTabIndex = TabIndex();
+ bool disabled = false;
+ bool disallowOverridingFocusability = true;
+ Maybe<int32_t> attrVal = GetTabIndexAttrValue();
+ if (IsEditingHost()) {
+ // Editable roots should always be focusable.
+ disallowOverridingFocusability = true;
+
+ // Ignore the disabled attribute in editable contentEditable/designMode
+ // roots.
+ if (attrVal.isNothing()) {
+ // The default value for tabindex should be 0 for editable
+ // contentEditable roots.
+ *aTabIndex = 0;
+ }
+ } else {
+ disallowOverridingFocusability = false;
+
+ // Just check for disabled attribute on form controls
+ disabled = IsDisabled();
+ if (disabled) {
+ *aTabIndex = -1;
+ }
+ }
+
+ // If a tabindex is specified at all, or the default tabindex is 0, we're
+ // focusable.
+ *aIsFocusable = (*aTabIndex >= 0 || (!disabled && attrVal.isSome()));
+ return disallowOverridingFocusability;
+}
+
+Result<bool, nsresult> nsGenericHTMLElement::PerformAccesskey(
+ bool aKeyCausesActivation, bool aIsTrustedEvent) {
+ RefPtr<nsPresContext> presContext = GetPresContext(eForComposedDoc);
+ if (!presContext) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ // It's hard to say what HTML4 wants us to do in all cases.
+ bool focused = true;
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ fm->SetFocus(this, nsIFocusManager::FLAG_BYKEY);
+
+ // Return true if the element became the current focus within its window.
+ nsPIDOMWindowOuter* window = OwnerDoc()->GetWindow();
+ focused = window && window->GetFocusedElement() == this;
+ }
+
+ if (aKeyCausesActivation) {
+ // Click on it if the users prefs indicate to do so.
+ AutoHandlingUserInputStatePusher userInputStatePusher(aIsTrustedEvent);
+ AutoPopupStatePusher popupStatePusher(
+ aIsTrustedEvent ? PopupBlocker::openAllowed : PopupBlocker::openAbused);
+ DispatchSimulatedClick(this, aIsTrustedEvent, presContext);
+ return focused;
+ }
+
+ // If the accesskey won't cause the activation and the focus isn't changed,
+ // either. Return error so EventStateManager would try to find next element
+ // to handle the accesskey.
+ return focused ? Result<bool, nsresult>{focused} : Err(NS_ERROR_ABORT);
+}
+
+void nsGenericHTMLElement::HandleKeyboardActivation(
+ EventChainPostVisitor& aVisitor) {
+ MOZ_ASSERT(aVisitor.mEvent->HasKeyEventMessage());
+ MOZ_ASSERT(aVisitor.mEvent->IsTrusted());
+
+ // If focused element is different from this element, it may be editable.
+ // In that case, associated editor for the element should handle the keyboard
+ // instead. Therefore, if this is not the focused element, we should not
+ // handle the event here. Note that this element may be an editing host,
+ // i.e., focused and editable. In the case, keyboard events should be
+ // handled by the focused element instead of associated editor because
+ // Chrome handles the case so. For compatibility with Chrome, we follow them.
+ if (nsFocusManager::GetFocusedElementStatic() != this) {
+ return;
+ }
+
+ const auto message = aVisitor.mEvent->mMessage;
+ const WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent();
+ if (nsEventStatus_eIgnore != aVisitor.mEventStatus) {
+ if (message == eKeyUp && keyEvent->mKeyCode == NS_VK_SPACE) {
+ // Unset the flag even if the event is default-prevented or something.
+ UnsetFlags(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD);
+ }
+ return;
+ }
+
+ bool shouldActivate = false;
+ switch (message) {
+ case eKeyDown:
+ if (keyEvent->ShouldWorkAsSpaceKey()) {
+ SetFlags(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD);
+ }
+ return;
+ case eKeyPress:
+ shouldActivate = keyEvent->mKeyCode == NS_VK_RETURN;
+ if (keyEvent->ShouldWorkAsSpaceKey()) {
+ // Consume 'space' key to prevent scrolling the page down.
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ break;
+ case eKeyUp:
+ shouldActivate = keyEvent->ShouldWorkAsSpaceKey() &&
+ HasFlag(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD);
+ if (shouldActivate) {
+ UnsetFlags(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD);
+ }
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("why didn't we bail out earlier?");
+ break;
+ }
+
+ if (!shouldActivate) {
+ return;
+ }
+
+ RefPtr<nsPresContext> presContext = aVisitor.mPresContext;
+ DispatchSimulatedClick(this, aVisitor.mEvent->IsTrusted(), presContext);
+ aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
+}
+
+nsresult nsGenericHTMLElement::DispatchSimulatedClick(
+ nsGenericHTMLElement* aElement, bool aIsTrusted,
+ nsPresContext* aPresContext) {
+ WidgetMouseEvent event(aIsTrusted, eMouseClick, nullptr,
+ WidgetMouseEvent::eReal);
+ event.mInputSource = MouseEvent_Binding::MOZ_SOURCE_KEYBOARD;
+ event.mFlags.mIsPositionless = true;
+ return EventDispatcher::Dispatch(aElement, aPresContext, &event);
+}
+
+already_AddRefed<EditorBase> nsGenericHTMLElement::GetAssociatedEditor() {
+ // If contenteditable is ever implemented, it might need to do something
+ // different here?
+
+ RefPtr<TextEditor> textEditor = GetTextEditorInternal();
+ return textEditor.forget();
+}
+
+// static
+void nsGenericHTMLElement::SyncEditorsOnSubtree(nsIContent* content) {
+ /* Sync this node */
+ nsGenericHTMLElement* element = FromNode(content);
+ if (element) {
+ if (RefPtr<EditorBase> editorBase = element->GetAssociatedEditor()) {
+ editorBase->SyncRealTimeSpell();
+ }
+ }
+
+ /* Sync all children */
+ for (nsIContent* child = content->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ SyncEditorsOnSubtree(child);
+ }
+}
+
+static void MakeContentDescendantsEditable(nsIContent* aContent) {
+ // If aContent is not an element, we just need to update its
+ // internal editable state and don't need to notify anyone about
+ // that. For elements, we need to send a ElementStateChanged
+ // notification.
+ if (!aContent->IsElement()) {
+ aContent->UpdateEditableState(false);
+ return;
+ }
+
+ Element* element = aContent->AsElement();
+
+ element->UpdateEditableState(true);
+
+ for (nsIContent* child = aContent->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (!child->IsElement() ||
+ !child->AsElement()->HasAttr(nsGkAtoms::contenteditable)) {
+ MakeContentDescendantsEditable(child);
+ }
+ }
+}
+
+void nsGenericHTMLElement::ChangeEditableState(int32_t aChange) {
+ Document* document = GetComposedDoc();
+ if (!document) {
+ return;
+ }
+
+ Document::EditingState previousEditingState = Document::EditingState::eOff;
+ if (aChange != 0) {
+ document->ChangeContentEditableCount(this, aChange);
+ previousEditingState = document->GetEditingState();
+ }
+
+ // MakeContentDescendantsEditable is going to call ElementStateChanged for
+ // this element and all descendants if editable state has changed.
+ // We might as well wrap it all in one script blocker.
+ nsAutoScriptBlocker scriptBlocker;
+ MakeContentDescendantsEditable(this);
+
+ // If the document already had contenteditable and JS adds new
+ // contenteditable, that might cause changing editing host to current editing
+ // host's ancestor. In such case, HTMLEditor needs to know that
+ // synchronously to update selection limitter.
+ // Additionally, elements in shadow DOM is not editable in the normal cases,
+ // but if its content has `contenteditable`, only in it can be ediable.
+ // So we don't need to notify HTMLEditor of this change only when we're not
+ // in shadow DOM and the composed document is in design mode.
+ if (IsInDesignMode() && !IsInShadowTree() && aChange > 0 &&
+ previousEditingState == Document::EditingState::eContentEditable) {
+ if (HTMLEditor* htmlEditor =
+ nsContentUtils::GetHTMLEditor(document->GetPresContext())) {
+ htmlEditor->NotifyEditingHostMaybeChanged();
+ }
+ }
+}
+
+//----------------------------------------------------------------------
+
+nsGenericHTMLFormControlElement::nsGenericHTMLFormControlElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, FormControlType aType)
+ : nsGenericHTMLFormElement(std::move(aNodeInfo)),
+ nsIFormControl(aType),
+ mForm(nullptr),
+ mFieldSet(nullptr) {}
+
+nsGenericHTMLFormControlElement::~nsGenericHTMLFormControlElement() {
+ if (mFieldSet) {
+ mFieldSet->RemoveElement(this);
+ }
+
+ // Check that this element doesn't know anything about its form at this point.
+ NS_ASSERTION(!mForm, "mForm should be null at this point!");
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsGenericHTMLFormControlElement,
+ nsGenericHTMLFormElement, nsIFormControl)
+
+nsINode* nsGenericHTMLFormControlElement::GetScopeChainParent() const {
+ return mForm ? mForm : nsGenericHTMLElement::GetScopeChainParent();
+}
+
+nsIContent::IMEState nsGenericHTMLFormControlElement::GetDesiredIMEState() {
+ TextEditor* textEditor = GetTextEditorInternal();
+ if (!textEditor) {
+ return nsGenericHTMLFormElement::GetDesiredIMEState();
+ }
+ IMEState state;
+ nsresult rv = textEditor->GetPreferredIMEState(&state);
+ if (NS_FAILED(rv)) {
+ return nsGenericHTMLFormElement::GetDesiredIMEState();
+ }
+ return state;
+}
+
+void nsGenericHTMLFormControlElement::GetAutocapitalize(
+ nsAString& aValue) const {
+ if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None,
+ nsGkAtoms::autocapitalize)) {
+ nsGenericHTMLFormElement::GetAutocapitalize(aValue);
+ return;
+ }
+
+ if (mForm && IsAutocapitalizeInheriting()) {
+ mForm->GetAutocapitalize(aValue);
+ }
+}
+
+bool nsGenericHTMLFormControlElement::IsHTMLFocusable(bool aWithMouse,
+ bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ if (nsGenericHTMLFormElement::IsHTMLFocusable(aWithMouse, aIsFocusable,
+ aTabIndex)) {
+ return true;
+ }
+
+ *aIsFocusable = *aIsFocusable && IsFormControlDefaultFocusable(aWithMouse);
+ return false;
+}
+
+void nsGenericHTMLFormControlElement::GetEventTargetParent(
+ EventChainPreVisitor& aVisitor) {
+ if (aVisitor.mEvent->IsTrusted() && (aVisitor.mEvent->mMessage == eFocus ||
+ aVisitor.mEvent->mMessage == eBlur)) {
+ // We have to handle focus/blur event to change focus states in
+ // PreHandleEvent to prevent it breaks event target chain creation.
+ aVisitor.mWantsPreHandleEvent = true;
+ }
+ nsGenericHTMLFormElement::GetEventTargetParent(aVisitor);
+}
+
+nsresult nsGenericHTMLFormControlElement::PreHandleEvent(
+ EventChainVisitor& aVisitor) {
+ if (aVisitor.mEvent->IsTrusted()) {
+ switch (aVisitor.mEvent->mMessage) {
+ case eFocus: {
+ // Check to see if focus has bubbled up from a form control's
+ // child textfield or button. If that's the case, don't focus
+ // this parent file control -- leave focus on the child.
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(true);
+ if (formControlFrame &&
+ aVisitor.mEvent->mOriginalTarget == static_cast<nsINode*>(this)) {
+ formControlFrame->SetFocus(true, true);
+ }
+ break;
+ }
+ case eBlur: {
+ nsIFormControlFrame* formControlFrame = GetFormControlFrame(true);
+ if (formControlFrame) {
+ formControlFrame->SetFocus(false, false);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return nsGenericHTMLFormElement::PreHandleEvent(aVisitor);
+}
+
+HTMLFieldSetElement* nsGenericHTMLFormControlElement::GetFieldSet() {
+ return GetFieldSetInternal();
+}
+
+void nsGenericHTMLFormControlElement::SetForm(HTMLFormElement* aForm) {
+ MOZ_ASSERT(aForm, "Don't pass null here");
+ NS_ASSERTION(!mForm,
+ "We don't support switching from one non-null form to another.");
+
+ SetFormInternal(aForm, false);
+}
+
+void nsGenericHTMLFormControlElement::ClearForm(bool aRemoveFromForm,
+ bool aUnbindOrDelete) {
+ nsGenericHTMLFormElement::ClearForm(aRemoveFromForm, aUnbindOrDelete);
+}
+
+bool nsGenericHTMLFormControlElement::IsLabelable() const {
+ auto type = ControlType();
+ return (IsInputElement(type) && type != FormControlType::InputHidden) ||
+ IsButtonElement(type) || type == FormControlType::Output ||
+ type == FormControlType::Select || type == FormControlType::Textarea;
+}
+
+bool nsGenericHTMLFormControlElement::CanBeDisabled() const {
+ auto type = ControlType();
+ // It's easier to test the types that _cannot_ be disabled
+ return type != FormControlType::Object && type != FormControlType::Output;
+}
+
+bool nsGenericHTMLFormControlElement::DoesReadOnlyApply() const {
+ auto type = ControlType();
+ if (!IsInputElement(type) && type != FormControlType::Textarea) {
+ return false;
+ }
+
+ switch (type) {
+ case FormControlType::InputHidden:
+ case FormControlType::InputButton:
+ case FormControlType::InputImage:
+ case FormControlType::InputReset:
+ case FormControlType::InputSubmit:
+ case FormControlType::InputRadio:
+ case FormControlType::InputFile:
+ case FormControlType::InputCheckbox:
+ case FormControlType::InputRange:
+ case FormControlType::InputColor:
+ return false;
+#ifdef DEBUG
+ case FormControlType::Textarea:
+ case FormControlType::InputText:
+ case FormControlType::InputPassword:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputEmail:
+ case FormControlType::InputUrl:
+ case FormControlType::InputNumber:
+ case FormControlType::InputDate:
+ case FormControlType::InputTime:
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ case FormControlType::InputDatetimeLocal:
+ return true;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected input type in DoesReadOnlyApply()");
+ return true;
+#else // DEBUG
+ default:
+ return true;
+#endif // DEBUG
+ }
+}
+
+void nsGenericHTMLFormControlElement::SetFormInternal(HTMLFormElement* aForm,
+ bool aBindToTree) {
+ if (aForm) {
+ BeforeSetForm(aForm, aBindToTree);
+ }
+
+ // keep a *weak* ref to the form here
+ mForm = aForm;
+}
+
+HTMLFormElement* nsGenericHTMLFormControlElement::GetFormInternal() const {
+ return mForm;
+}
+
+HTMLFieldSetElement* nsGenericHTMLFormControlElement::GetFieldSetInternal()
+ const {
+ return mFieldSet;
+}
+
+void nsGenericHTMLFormControlElement::SetFieldSetInternal(
+ HTMLFieldSetElement* aFieldset) {
+ mFieldSet = aFieldset;
+}
+
+void nsGenericHTMLFormControlElement::UpdateRequiredState(bool aIsRequired,
+ bool aNotify) {
+#ifdef DEBUG
+ auto type = ControlType();
+#endif
+ MOZ_ASSERT(IsInputElement(type) || type == FormControlType::Select ||
+ type == FormControlType::Textarea,
+ "This should be called only on types that @required applies");
+
+#ifdef DEBUG
+ if (HTMLInputElement* input = HTMLInputElement::FromNode(this)) {
+ MOZ_ASSERT(
+ input->DoesRequiredApply(),
+ "This should be called only on input types that @required applies");
+ }
+#endif
+
+ ElementState requiredStates;
+ if (aIsRequired) {
+ requiredStates |= ElementState::REQUIRED;
+ } else {
+ requiredStates |= ElementState::OPTIONAL_;
+ }
+
+ ElementState oldRequiredStates = State() & ElementState::REQUIRED_STATES;
+ ElementState changedStates = requiredStates ^ oldRequiredStates;
+
+ if (!changedStates.IsEmpty()) {
+ ToggleStates(changedStates, aNotify);
+ }
+}
+
+bool nsGenericHTMLFormControlElement::IsAutocapitalizeInheriting() const {
+ auto type = ControlType();
+ return IsInputElement(type) || IsButtonElement(type) ||
+ type == FormControlType::Fieldset || type == FormControlType::Output ||
+ type == FormControlType::Select || type == FormControlType::Textarea;
+}
+
+nsresult nsGenericHTMLFormControlElement::SubmitDirnameDir(
+ FormData* aFormData) {
+ // Submit dirname=dir if element has non-empty dirname attribute
+ if (HasAttr(nsGkAtoms::dirname)) {
+ nsAutoString dirname;
+ GetAttr(nsGkAtoms::dirname, dirname);
+ if (!dirname.IsEmpty()) {
+ const Directionality dir = GetDirectionality();
+ MOZ_ASSERT(dir == Directionality::Ltr || dir == Directionality::Rtl,
+ "The directionality of an element is either ltr or rtl");
+ return aFormData->AddNameValuePair(
+ dirname, dir == Directionality::Ltr ? u"ltr"_ns : u"rtl"_ns);
+ }
+ }
+ return NS_OK;
+}
+
+//----------------------------------------------------------------------
+
+static const nsAttrValue::EnumTable kPopoverTargetActionTable[] = {
+ {"toggle", PopoverTargetAction::Toggle},
+ {"show", PopoverTargetAction::Show},
+ {"hide", PopoverTargetAction::Hide},
+ {nullptr, 0}};
+
+static const nsAttrValue::EnumTable* kPopoverTargetActionDefault =
+ &kPopoverTargetActionTable[0];
+
+nsGenericHTMLFormControlElementWithState::
+ nsGenericHTMLFormControlElementWithState(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser, FormControlType aType)
+ : nsGenericHTMLFormControlElement(std::move(aNodeInfo), aType),
+ mControlNumber(!!(aFromParser & FROM_PARSER_NETWORK)
+ ? OwnerDoc()->GetNextControlNumber()
+ : -1) {
+ mStateKey.SetIsVoid(true);
+}
+
+bool nsGenericHTMLFormControlElementWithState::ParseAttribute(
+ int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (StaticPrefs::dom_element_popover_enabled()) {
+ if (aAttribute == nsGkAtoms::popovertargetaction) {
+ return aResult.ParseEnumValue(aValue, kPopoverTargetActionTable, false,
+ kPopoverTargetActionDefault);
+ }
+ if (aAttribute == nsGkAtoms::popovertarget) {
+ aResult.ParseAtom(aValue);
+ return true;
+ }
+ }
+
+ if (StaticPrefs::dom_element_invokers_enabled()) {
+ if (aAttribute == nsGkAtoms::invokeaction) {
+ aResult.ParseAtom(aValue);
+ return true;
+ }
+ if (aAttribute == nsGkAtoms::invoketarget) {
+ aResult.ParseAtom(aValue);
+ return true;
+ }
+ }
+ }
+
+ return nsGenericHTMLFormControlElement::ParseAttribute(
+ aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult);
+}
+
+mozilla::dom::Element*
+nsGenericHTMLFormControlElementWithState::GetPopoverTargetElement() const {
+ return GetAttrAssociatedElement(nsGkAtoms::popovertarget);
+}
+
+void nsGenericHTMLFormControlElementWithState::SetPopoverTargetElement(
+ mozilla::dom::Element* aElement) {
+ ExplicitlySetAttrElement(nsGkAtoms::popovertarget, aElement);
+}
+
+void nsGenericHTMLFormControlElementWithState::HandlePopoverTargetAction() {
+ RefPtr<nsGenericHTMLElement> target = GetEffectivePopoverTargetElement();
+ if (!target) {
+ return;
+ }
+
+ auto action = PopoverTargetAction::Toggle;
+ if (const nsAttrValue* value =
+ GetParsedAttr(nsGkAtoms::popovertargetaction)) {
+ MOZ_ASSERT(value->Type() == nsAttrValue::eEnum);
+ action = static_cast<PopoverTargetAction>(value->GetEnumValue());
+ }
+
+ bool canHide = action == PopoverTargetAction::Hide ||
+ action == PopoverTargetAction::Toggle;
+ bool shouldHide = canHide && target->IsPopoverOpen();
+ bool canShow = action == PopoverTargetAction::Show ||
+ action == PopoverTargetAction::Toggle;
+ bool shouldShow = canShow && !target->IsPopoverOpen();
+
+ if (shouldHide) {
+ target->HidePopover(IgnoreErrors());
+ } else if (shouldShow) {
+ target->ShowPopoverInternal(this, IgnoreErrors());
+ }
+}
+
+void nsGenericHTMLFormControlElementWithState::GetInvokeAction(
+ nsAString& aValue) const {
+ GetInvokeAction()->ToString(aValue);
+}
+
+nsAtom* nsGenericHTMLFormControlElementWithState::GetInvokeAction() const {
+ const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::invokeaction);
+ if (attr && attr->GetAtomValue() != nsGkAtoms::_empty) {
+ return attr->GetAtomValue();
+ }
+ return nsGkAtoms::_auto;
+}
+
+mozilla::dom::Element*
+nsGenericHTMLFormControlElementWithState::GetInvokeTargetElement() const {
+ if (StaticPrefs::dom_element_invokers_enabled()) {
+ return GetAttrAssociatedElement(nsGkAtoms::invoketarget);
+ }
+ return nullptr;
+}
+
+void nsGenericHTMLFormControlElementWithState::SetInvokeTargetElement(
+ mozilla::dom::Element* aElement) {
+ ExplicitlySetAttrElement(nsGkAtoms::invoketarget, aElement);
+}
+
+void nsGenericHTMLFormControlElementWithState::HandleInvokeTargetAction() {
+ // 1. Let invokee be node's invoke target element.
+ RefPtr<Element> invokee = GetInvokeTargetElement();
+
+ // 2. If invokee is null, then return.
+ if (!invokee) {
+ return;
+ }
+
+ // 3. Let action be node's invokeaction attribute
+ // 4. If action is null or empty, then let action be the string "auto".
+ RefPtr<nsAtom> aAction = GetInvokeAction();
+ MOZ_ASSERT(!aAction->IsEmpty(), "Action should not be empty");
+
+ // 5. Let notCancelled be the result of firing an event named invoke at
+ // invokee with its action set to action, its invoker set to node,
+ // and its cancelable attribute initialized to true.
+ InvokeEventInit init;
+ aAction->ToString(init.mAction);
+ init.mInvoker = this;
+ init.mCancelable = true;
+ init.mComposed = true;
+ RefPtr<Event> event = InvokeEvent::Constructor(this, u"invoke"_ns, init);
+ event->SetTrusted(true);
+ event->SetTarget(invokee);
+
+ EventDispatcher::DispatchDOMEvent(invokee, nullptr, event, nullptr, nullptr);
+
+ // 6. If notCancelled is true and invokee has an associated invocation action
+ // algorithm then run the invokee's invocation action algorithm given action.
+ if (event->DefaultPrevented()) {
+ return;
+ }
+
+ invokee->HandleInvokeInternal(aAction, IgnoreErrors());
+}
+
+void nsGenericHTMLFormControlElementWithState::GenerateStateKey() {
+ // Keep the key if already computed
+ if (!mStateKey.IsVoid()) {
+ return;
+ }
+
+ Document* doc = GetUncomposedDoc();
+ if (!doc) {
+ mStateKey.Truncate();
+ return;
+ }
+
+ // Generate the state key
+ nsContentUtils::GenerateStateKey(this, doc, mStateKey);
+
+ // If the state key is blank, this is anonymous content or for whatever
+ // reason we are not supposed to save/restore state: keep it as such.
+ if (!mStateKey.IsEmpty()) {
+ // Add something unique to content so layout doesn't muck us up.
+ mStateKey += "-C";
+ }
+}
+
+PresState* nsGenericHTMLFormControlElementWithState::GetPrimaryPresState() {
+ if (mStateKey.IsEmpty()) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(false);
+
+ if (!history) {
+ return nullptr;
+ }
+
+ // Get the pres state for this key, if it doesn't exist, create one.
+ PresState* result = history->GetState(mStateKey);
+ if (!result) {
+ UniquePtr<PresState> newState = NewPresState();
+ result = newState.get();
+ history->AddState(mStateKey, std::move(newState));
+ }
+
+ return result;
+}
+
+already_AddRefed<nsILayoutHistoryState>
+nsGenericHTMLFormElement::GetLayoutHistory(bool aRead) {
+ nsCOMPtr<Document> doc = GetUncomposedDoc();
+ if (!doc) {
+ return nullptr;
+ }
+
+ //
+ // Get the history
+ //
+ nsCOMPtr<nsILayoutHistoryState> history = doc->GetLayoutHistoryState();
+ if (!history) {
+ return nullptr;
+ }
+
+ if (aRead && !history->HasStates()) {
+ return nullptr;
+ }
+
+ return history.forget();
+}
+
+bool nsGenericHTMLFormControlElementWithState::RestoreFormControlState() {
+ MOZ_ASSERT(!mStateKey.IsVoid(),
+ "GenerateStateKey must already have been called");
+
+ if (mStateKey.IsEmpty()) {
+ return false;
+ }
+
+ nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(true);
+ if (!history) {
+ return false;
+ }
+
+ // Get the pres state for this key
+ PresState* state = history->GetState(mStateKey);
+ if (state) {
+ bool result = RestoreState(state);
+ history->RemoveState(mStateKey);
+ return result;
+ }
+
+ return false;
+}
+
+void nsGenericHTMLFormControlElementWithState::NodeInfoChanged(
+ Document* aOldDoc) {
+ nsGenericHTMLFormControlElement::NodeInfoChanged(aOldDoc);
+
+ // We need to regenerate the state key now we're in a new document. Clearing
+ // mControlNumber means we stop considering this control to be parser
+ // inserted, and we'll generate a state key based on its position in the
+ // document rather than the order it was inserted into the document.
+ mControlNumber = -1;
+ mStateKey.SetIsVoid(true);
+}
+
+void nsGenericHTMLFormControlElementWithState::GetFormAction(nsString& aValue) {
+ auto type = ControlType();
+ if (!IsInputElement(type) && !IsButtonElement(type)) {
+ return;
+ }
+
+ if (!GetAttr(nsGkAtoms::formaction, aValue) || aValue.IsEmpty()) {
+ Document* document = OwnerDoc();
+ nsIURI* docURI = document->GetDocumentURI();
+ if (docURI) {
+ nsAutoCString spec;
+ nsresult rv = docURI->GetSpec(spec);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ CopyUTF8toUTF16(spec, aValue);
+ }
+ } else {
+ GetURIAttr(nsGkAtoms::formaction, nullptr, aValue);
+ }
+}
+
+bool nsGenericHTMLElement::IsEventAttributeNameInternal(nsAtom* aName) {
+ return nsContentUtils::IsEventAttributeName(aName, EventNameType_HTML);
+}
+
+/**
+ * Construct a URI from a string, as an element.src attribute
+ * would be set to. Helper for the media elements.
+ */
+nsresult nsGenericHTMLElement::NewURIFromString(const nsAString& aURISpec,
+ nsIURI** aURI) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ *aURI = nullptr;
+
+ nsCOMPtr<Document> doc = OwnerDoc();
+
+ nsresult rv = nsContentUtils::NewURIWithDocumentCharset(aURI, aURISpec, doc,
+ GetBaseURI());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool equal;
+ if (aURISpec.IsEmpty() && doc->GetDocumentURI() &&
+ NS_SUCCEEDED(doc->GetDocumentURI()->Equals(*aURI, &equal)) && equal) {
+ // Assume an element can't point to a fragment of its embedding
+ // document. Fail here instead of returning the recursive URI
+ // and waiting for the subsequent load to fail.
+ NS_RELEASE(*aURI);
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ return NS_OK;
+}
+
+void nsGenericHTMLElement::GetInnerText(mozilla::dom::DOMString& aValue,
+ mozilla::ErrorResult& aError) {
+ // innerText depends on layout. For example, white space processing is
+ // something that happens during reflow and which must be reflected by
+ // innerText. So for:
+ //
+ // <div style="white-space:normal"> A B C </div>
+ //
+ // innerText should give "A B C".
+ //
+ // The approach taken here to avoid the expense of reflow is to flush style
+ // and then see whether it's necessary to flush layout afterwards. Flushing
+ // layout can be skipped if we can detect that the element or its descendants
+ // are not dirty.
+
+ // Obtain the composed doc to handle elements in Shadow DOM.
+ Document* doc = GetComposedDoc();
+ if (doc) {
+ doc->FlushPendingNotifications(FlushType::Style);
+ }
+
+ // Elements with `display: content` will not have a frame. To handle Shadow
+ // DOM, walk the flattened tree looking for parent frame.
+ nsIFrame* frame = GetPrimaryFrame();
+ if (IsDisplayContents()) {
+ for (Element* parent = GetFlattenedTreeParentElement(); parent;
+ parent = parent->GetFlattenedTreeParentElement()) {
+ frame = parent->GetPrimaryFrame();
+ if (frame) {
+ break;
+ }
+ }
+ }
+
+ // Check for dirty reflow roots in the subtree from targetFrame; this requires
+ // a reflow flush.
+ bool dirty = frame && frame->PresShell()->FrameIsAncestorOfDirtyRoot(frame);
+
+ // The way we do that is by checking whether the element has either of the two
+ // dirty bits (NS_FRAME_IS_DIRTY or NS_FRAME_HAS_DIRTY_DESCENDANTS) or if any
+ // ancestor has NS_FRAME_IS_DIRTY. We need to check for NS_FRAME_IS_DIRTY on
+ // ancestors since that is something that implies NS_FRAME_IS_DIRTY on all
+ // descendants.
+ dirty |= frame && frame->HasAnyStateBits(NS_FRAME_HAS_DIRTY_CHILDREN);
+ while (!dirty && frame) {
+ dirty |= frame->HasAnyStateBits(NS_FRAME_IS_DIRTY);
+ frame = frame->GetInFlowParent();
+ }
+
+ // Flush layout if we determined a reflow is required.
+ if (dirty && doc) {
+ doc->FlushPendingNotifications(FlushType::Layout);
+ }
+
+ if (!IsRendered()) {
+ GetTextContentInternal(aValue, aError);
+ } else {
+ nsRange::GetInnerTextNoFlush(aValue, aError, this);
+ }
+}
+
+static already_AddRefed<nsINode> TextToNode(const nsAString& aString,
+ nsNodeInfoManager* aNim) {
+ nsString str;
+ const char16_t* s = aString.BeginReading();
+ const char16_t* end = aString.EndReading();
+ RefPtr<DocumentFragment> fragment;
+ while (true) {
+ if (s != end && *s == '\r' && s + 1 != end && s[1] == '\n') {
+ // a \r\n pair should only generate one <br>, so just skip the \r
+ ++s;
+ }
+ if (s == end || *s == '\r' || *s == '\n') {
+ if (!str.IsEmpty()) {
+ RefPtr<nsTextNode> textContent = new (aNim) nsTextNode(aNim);
+ textContent->SetText(str, true);
+ if (!fragment) {
+ if (s == end) {
+ return textContent.forget();
+ }
+ fragment = new (aNim) DocumentFragment(aNim);
+ }
+ fragment->AppendChildTo(textContent, true, IgnoreErrors());
+ }
+ if (s == end) {
+ break;
+ }
+ str.Truncate();
+ RefPtr<NodeInfo> ni = aNim->GetNodeInfo(
+ nsGkAtoms::br, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+ auto* nim = ni->NodeInfoManager();
+ RefPtr<HTMLBRElement> br = new (nim) HTMLBRElement(ni.forget());
+ if (!fragment) {
+ if (s + 1 == end) {
+ return br.forget();
+ }
+ fragment = new (aNim) DocumentFragment(aNim);
+ }
+ fragment->AppendChildTo(br, true, IgnoreErrors());
+ } else {
+ str.Append(*s);
+ }
+ ++s;
+ }
+ return fragment.forget();
+}
+
+void nsGenericHTMLElement::SetInnerText(const nsAString& aValue) {
+ RefPtr<nsINode> node = TextToNode(aValue, NodeInfo()->NodeInfoManager());
+ ReplaceChildren(node, IgnoreErrors());
+}
+
+// https://html.spec.whatwg.org/#merge-with-the-next-text-node
+static void MergeWithNextTextNode(Text& aText, ErrorResult& aRv) {
+ RefPtr<Text> nextSibling = Text::FromNodeOrNull(aText.GetNextSibling());
+ if (!nextSibling) {
+ return;
+ }
+ nsAutoString data;
+ nextSibling->GetData(data);
+ aText.AppendData(data, aRv);
+ nextSibling->Remove();
+}
+
+// https://html.spec.whatwg.org/#dom-outertext
+void nsGenericHTMLElement::SetOuterText(const nsAString& aValue,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsINode> parent = GetParentNode();
+ if (!parent) {
+ return aRv.ThrowNoModificationAllowedError("Element has no parent");
+ }
+
+ RefPtr<nsINode> next = GetNextSibling();
+ RefPtr<nsINode> previous = GetPreviousSibling();
+
+ // Batch possible DOMSubtreeModified events.
+ mozAutoSubtreeModified subtree(OwnerDoc(), nullptr);
+
+ nsNodeInfoManager* nim = NodeInfo()->NodeInfoManager();
+ RefPtr<nsINode> node = TextToNode(aValue, nim);
+ if (!node) {
+ // This doesn't match the spec, see
+ // https://github.com/whatwg/html/issues/7508
+ node = new (nim) nsTextNode(nim);
+ }
+ parent->ReplaceChild(*node, *this, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (next) {
+ if (RefPtr<Text> text = Text::FromNodeOrNull(next->GetPreviousSibling())) {
+ MergeWithNextTextNode(*text, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ }
+ }
+ if (auto* text = Text::FromNodeOrNull(previous)) {
+ MergeWithNextTextNode(*text, aRv);
+ }
+}
+
+// This should be true when `:open` should match.
+bool nsGenericHTMLElement::PopoverOpen() const {
+ if (PopoverData* popoverData = GetPopoverData()) {
+ return popoverData->GetPopoverVisibilityState() ==
+ PopoverVisibilityState::Showing;
+ }
+ return false;
+}
+
+// https://html.spec.whatwg.org/#check-popover-validity
+bool nsGenericHTMLElement::CheckPopoverValidity(
+ PopoverVisibilityState aExpectedState, Document* aExpectedDocument,
+ ErrorResult& aRv) {
+ if (GetPopoverAttributeState() == PopoverAttributeState::None) {
+ aRv.ThrowNotSupportedError("Element is in the no popover state");
+ return false;
+ }
+
+ if (GetPopoverData()->GetPopoverVisibilityState() != aExpectedState) {
+ return false;
+ }
+
+ if (!IsInComposedDoc()) {
+ aRv.ThrowInvalidStateError("Element is not connected");
+ return false;
+ }
+
+ if (aExpectedDocument && aExpectedDocument != OwnerDoc()) {
+ aRv.ThrowInvalidStateError("Element is moved to other document");
+ return false;
+ }
+
+ if (auto* dialog = HTMLDialogElement::FromNode(this)) {
+ if (dialog->IsInTopLayer()) {
+ aRv.ThrowInvalidStateError("Element is a modal <dialog> element");
+ return false;
+ }
+ }
+
+ if (State().HasState(ElementState::FULLSCREEN)) {
+ aRv.ThrowInvalidStateError("Element is fullscreen");
+ return false;
+ }
+
+ return true;
+}
+
+PopoverAttributeState nsGenericHTMLElement::GetPopoverAttributeState() const {
+ return GetPopoverData() ? GetPopoverData()->GetPopoverAttributeState()
+ : PopoverAttributeState::None;
+}
+
+void nsGenericHTMLElement::PopoverPseudoStateUpdate(bool aOpen, bool aNotify) {
+ SetStates(ElementState::POPOVER_OPEN, aOpen, aNotify);
+}
+
+already_AddRefed<ToggleEvent> nsGenericHTMLElement::CreateToggleEvent(
+ const nsAString& aEventType, const nsAString& aOldState,
+ const nsAString& aNewState, Cancelable aCancelable) {
+ ToggleEventInit init;
+ init.mBubbles = false;
+ init.mOldState = aOldState;
+ init.mNewState = aNewState;
+ init.mCancelable = aCancelable == Cancelable::eYes;
+ RefPtr<ToggleEvent> event = ToggleEvent::Constructor(this, aEventType, init);
+ event->SetTrusted(true);
+ event->SetTarget(this);
+ return event.forget();
+}
+
+bool nsGenericHTMLElement::FireToggleEvent(PopoverVisibilityState aOldState,
+ PopoverVisibilityState aNewState,
+ const nsAString& aType) {
+ auto stringForState = [](PopoverVisibilityState state) {
+ return state == PopoverVisibilityState::Hidden ? u"closed"_ns : u"open"_ns;
+ };
+ const auto cancelable = aType == u"beforetoggle"_ns &&
+ aNewState == PopoverVisibilityState::Showing
+ ? Cancelable::eYes
+ : Cancelable::eNo;
+ RefPtr event = CreateToggleEvent(aType, stringForState(aOldState),
+ stringForState(aNewState), cancelable);
+ EventDispatcher::DispatchDOMEvent(this, nullptr, event, nullptr, nullptr);
+ return event->DefaultPrevented();
+}
+
+// https://html.spec.whatwg.org/#queue-a-popover-toggle-event-task
+void nsGenericHTMLElement::QueuePopoverEventTask(
+ PopoverVisibilityState aOldState) {
+ auto* data = GetPopoverData();
+ MOZ_ASSERT(data, "Should have popover data");
+
+ if (auto* queuedToggleEventTask = data->GetToggleEventTask()) {
+ aOldState = queuedToggleEventTask->GetOldState();
+ }
+
+ auto task =
+ MakeRefPtr<PopoverToggleEventTask>(do_GetWeakReference(this), aOldState);
+ data->SetToggleEventTask(task);
+ OwnerDoc()->Dispatch(task.forget());
+}
+
+void nsGenericHTMLElement::RunPopoverToggleEventTask(
+ PopoverToggleEventTask* aTask, PopoverVisibilityState aOldState) {
+ auto* data = GetPopoverData();
+ if (!data) {
+ return;
+ }
+
+ auto* popoverToggleEventTask = data->GetToggleEventTask();
+ if (!popoverToggleEventTask || aTask != popoverToggleEventTask) {
+ return;
+ }
+ data->ClearToggleEventTask();
+ // Intentionally ignore the return value here as only on open event the
+ // cancelable attribute is initialized to true for beforetoggle event.
+ FireToggleEvent(aOldState, data->GetPopoverVisibilityState(), u"toggle"_ns);
+}
+
+// https://html.spec.whatwg.org/#dom-showpopover
+void nsGenericHTMLElement::ShowPopover(ErrorResult& aRv) {
+ return ShowPopoverInternal(nullptr, aRv);
+}
+void nsGenericHTMLElement::ShowPopoverInternal(Element* aInvoker,
+ ErrorResult& aRv) {
+ if (!CheckPopoverValidity(PopoverVisibilityState::Hidden, nullptr, aRv)) {
+ return;
+ }
+ RefPtr<Document> document = OwnerDoc();
+
+ MOZ_ASSERT(!GetPopoverData() || !GetPopoverData()->GetInvoker());
+ MOZ_ASSERT(!OwnerDoc()->TopLayerContains(*this));
+
+ bool wasShowingOrHiding = GetPopoverData()->IsShowingOrHiding();
+ GetPopoverData()->SetIsShowingOrHiding(true);
+ auto cleanupShowingFlag = MakeScopeExit([&]() {
+ if (auto* popoverData = GetPopoverData()) {
+ popoverData->SetIsShowingOrHiding(wasShowingOrHiding);
+ }
+ });
+
+ // Fire beforetoggle event and re-check popover validity.
+ if (FireToggleEvent(PopoverVisibilityState::Hidden,
+ PopoverVisibilityState::Showing, u"beforetoggle"_ns)) {
+ return;
+ }
+ if (!CheckPopoverValidity(PopoverVisibilityState::Hidden, document, aRv)) {
+ return;
+ }
+
+ bool shouldRestoreFocus = false;
+ nsWeakPtr originallyFocusedElement;
+ if (IsAutoPopover()) {
+ auto originalState = GetPopoverAttributeState();
+ RefPtr<nsINode> ancestor = GetTopmostPopoverAncestor(aInvoker, true);
+ if (!ancestor) {
+ ancestor = document;
+ }
+ document->HideAllPopoversUntil(*ancestor, false,
+ /* aFireEvents = */ !wasShowingOrHiding);
+ if (GetPopoverAttributeState() != originalState) {
+ aRv.ThrowInvalidStateError(
+ "The value of the popover attribute was changed while hiding the "
+ "popover.");
+ return;
+ }
+
+ // TODO: Handle if document changes, see
+ // https://github.com/whatwg/html/issues/9177
+ if (!IsAutoPopover() ||
+ !CheckPopoverValidity(PopoverVisibilityState::Hidden, document, aRv)) {
+ return;
+ }
+
+ shouldRestoreFocus = !document->GetTopmostAutoPopover();
+ // Let originallyFocusedElement be document's focused area of the document's
+ // DOM anchor.
+ if (nsIContent* unretargetedFocus =
+ document->GetUnretargetedFocusedContent()) {
+ originallyFocusedElement =
+ do_GetWeakReference(unretargetedFocus->AsElement());
+ }
+ }
+
+ document->AddPopoverToTopLayer(*this);
+
+ PopoverPseudoStateUpdate(true, true);
+
+ {
+ auto* popoverData = GetPopoverData();
+ popoverData->SetPopoverVisibilityState(PopoverVisibilityState::Showing);
+ popoverData->SetInvoker(aInvoker);
+ }
+
+ // Run the popover focusing steps given element.
+ FocusPopover();
+ if (shouldRestoreFocus &&
+ GetPopoverAttributeState() != PopoverAttributeState::None) {
+ GetPopoverData()->SetPreviouslyFocusedElement(originallyFocusedElement);
+ }
+
+ // Queue popover toggle event task.
+ QueuePopoverEventTask(PopoverVisibilityState::Hidden);
+}
+
+void nsGenericHTMLElement::HidePopoverWithoutRunningScript() {
+ HidePopoverInternal(/* aFocusPreviousElement = */ false,
+ /* aFireEvents = */ false, IgnoreErrors());
+}
+
+// https://html.spec.whatwg.org/#dom-hidepopover
+void nsGenericHTMLElement::HidePopover(ErrorResult& aRv) {
+ HidePopoverInternal(/* aFocusPreviousElement = */ true,
+ /* aFireEvents = */ true, aRv);
+}
+
+void nsGenericHTMLElement::HidePopoverInternal(bool aFocusPreviousElement,
+ bool aFireEvents,
+ ErrorResult& aRv) {
+ OwnerDoc()->HidePopover(*this, aFocusPreviousElement, aFireEvents, aRv);
+}
+
+void nsGenericHTMLElement::ForgetPreviouslyFocusedElementAfterHidingPopover() {
+ auto* data = GetPopoverData();
+ MOZ_ASSERT(data, "Should have popover data");
+ data->SetPreviouslyFocusedElement(nullptr);
+}
+
+void nsGenericHTMLElement::FocusPreviousElementAfterHidingPopover() {
+ auto* data = GetPopoverData();
+ MOZ_ASSERT(data, "Should have popover data");
+
+ RefPtr<Element> control =
+ do_QueryReferent(data->GetPreviouslyFocusedElement().get());
+ data->SetPreviouslyFocusedElement(nullptr);
+
+ if (!control) {
+ return;
+ }
+
+ // Step 14.2 at
+ // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm
+ // If focusPreviousElement is true and document's focused area of the
+ // document's DOM anchor is a shadow-including inclusive descendant of
+ // element, then run the focusing steps for previouslyFocusedElement;
+ nsIContent* currentFocus = OwnerDoc()->GetUnretargetedFocusedContent();
+ if (currentFocus &&
+ currentFocus->IsShadowIncludingInclusiveDescendantOf(this)) {
+ FocusOptions options;
+ options.mPreventScroll = true;
+ control->Focus(options, CallerType::NonSystem, IgnoreErrors());
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/popover.html#dom-togglepopover
+bool nsGenericHTMLElement::TogglePopover(const Optional<bool>& aForce,
+ ErrorResult& aRv) {
+ if (PopoverOpen() && (!aForce.WasPassed() || !aForce.Value())) {
+ HidePopover(aRv);
+ } else if (!aForce.WasPassed() || aForce.Value()) {
+ ShowPopover(aRv);
+ } else {
+ CheckPopoverValidity(GetPopoverData()
+ ? GetPopoverData()->GetPopoverVisibilityState()
+ : PopoverVisibilityState::Showing,
+ nullptr, aRv);
+ }
+
+ return PopoverOpen();
+}
+
+// https://html.spec.whatwg.org/multipage/popover.html#popover-focusing-steps
+void nsGenericHTMLElement::FocusPopover() {
+ if (auto* dialog = HTMLDialogElement::FromNode(this)) {
+ return MOZ_KnownLive(dialog)->FocusDialog();
+ }
+
+ if (RefPtr<Document> doc = GetComposedDoc()) {
+ doc->FlushPendingNotifications(FlushType::Frames);
+ }
+
+ RefPtr<Element> control = GetBoolAttr(nsGkAtoms::autofocus)
+ ? this
+ : GetAutofocusDelegate(false /* aWithMouse */);
+
+ if (!control) {
+ return;
+ }
+ FocusCandidate(control, false /* aClearUpFocus */);
+}
+
+void nsGenericHTMLElement::FocusCandidate(Element* aControl,
+ bool aClearUpFocus) {
+ // 1) Run the focusing steps given control.
+ IgnoredErrorResult rv;
+ if (RefPtr<Element> elementToFocus = nsFocusManager::GetTheFocusableArea(
+ aControl, nsFocusManager::ProgrammaticFocusFlags(FocusOptions()))) {
+ elementToFocus->Focus(FocusOptions(), CallerType::NonSystem, rv);
+ if (rv.Failed()) {
+ return;
+ }
+ } else if (aClearUpFocus) {
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ // Clear the focus which ends up making the body gets focused
+ nsCOMPtr<nsPIDOMWindowOuter> outerWindow = OwnerDoc()->GetWindow();
+ fm->ClearFocus(outerWindow);
+ }
+ }
+
+ // 2) Let topDocument be the active document of control's node document's
+ // browsing context's top-level browsing context.
+ // 3) If control's node document's origin is not the same as the origin of
+ // topDocument, then return.
+ BrowsingContext* bc = aControl->OwnerDoc()->GetBrowsingContext();
+ if (bc && bc->IsInProcess() && bc->SameOriginWithTop()) {
+ if (nsCOMPtr<nsIDocShell> docShell = bc->Top()->GetDocShell()) {
+ if (Document* topDocument = docShell->GetExtantDocument()) {
+ // 4) Empty topDocument's autofocus candidates.
+ // 5) Set topDocument's autofocus processed flag to true.
+ topDocument->SetAutoFocusFired();
+ }
+ }
+ }
+}
+
+already_AddRefed<ElementInternals> nsGenericHTMLElement::AttachInternals(
+ ErrorResult& aRv) {
+ // ElementInternals is only available on autonomous custom element, so throws
+ // an error by default. The spec steps are implemented in HTMLElement because
+ // ElementInternals needs to hold a pointer to HTMLElement in order to forward
+ // form operation to it.
+ aRv.ThrowNotSupportedError(nsPrintfCString(
+ "Cannot attach ElementInternals to a customized built-in or non-custom "
+ "element "
+ "'%s'",
+ NS_ConvertUTF16toUTF8(NodeInfo()->NameAtom()->GetUTF16String()).get()));
+ return nullptr;
+}
+
+ElementInternals* nsGenericHTMLElement::GetInternals() const {
+ if (CustomElementData* data = GetCustomElementData()) {
+ return data->GetElementInternals();
+ }
+ return nullptr;
+}
+
+bool nsGenericHTMLElement::IsFormAssociatedCustomElements() const {
+ if (CustomElementData* data = GetCustomElementData()) {
+ return data->IsFormAssociated();
+ }
+ return false;
+}
+
+void nsGenericHTMLElement::GetAutocapitalize(nsAString& aValue) const {
+ GetEnumAttr(nsGkAtoms::autocapitalize, nullptr, kDefaultAutocapitalize->tag,
+ aValue);
+}
+
+bool nsGenericHTMLElement::Translate() const {
+ if (const nsAttrValue* attr = mAttrs.GetAttr(nsGkAtoms::translate)) {
+ if (attr->IsEmptyString() || attr->Equals(nsGkAtoms::yes, eIgnoreCase)) {
+ return true;
+ }
+ if (attr->Equals(nsGkAtoms::no, eIgnoreCase)) {
+ return false;
+ }
+ }
+ return nsGenericHTMLElementBase::Translate();
+}
+
+void nsGenericHTMLElement::GetPopover(nsString& aPopover) const {
+ GetHTMLEnumAttr(nsGkAtoms::popover, aPopover);
+ if (aPopover.IsEmpty() && !DOMStringIsNull(aPopover)) {
+ aPopover.Assign(NS_ConvertUTF8toUTF16(kPopoverAttributeValueAuto));
+ }
+}
diff --git a/dom/html/nsGenericHTMLElement.h b/dom/html/nsGenericHTMLElement.h
new file mode 100644
index 0000000000..f6e7d2415d
--- /dev/null
+++ b/dom/html/nsGenericHTMLElement.h
@@ -0,0 +1,1461 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef nsGenericHTMLElement_h___
+#define nsGenericHTMLElement_h___
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "nsNameSpaceManager.h" // for kNameSpaceID_None
+#include "nsIFormControl.h"
+#include "nsGkAtoms.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsStyledElement.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/DOMRect.h"
+#include "mozilla/dom/ValidityState.h"
+#include "mozilla/dom/PopoverData.h"
+#include "mozilla/dom/ToggleEvent.h"
+
+#include <cstdint>
+
+class nsDOMTokenList;
+class nsIFormControlFrame;
+class nsIFrame;
+class nsILayoutHistoryState;
+class nsIURI;
+struct nsSize;
+
+enum nsCSSPropertyID : int32_t;
+
+namespace mozilla {
+class EditorBase;
+class ErrorResult;
+class EventChainPostVisitor;
+class EventChainPreVisitor;
+class EventChainVisitor;
+class EventListenerManager;
+class PresState;
+namespace dom {
+class ElementInternals;
+class HTMLFormElement;
+enum class FetchPriority : uint8_t;
+} // namespace dom
+} // namespace mozilla
+
+using nsGenericHTMLElementBase = nsStyledElement;
+
+/**
+ * A common superclass for HTML elements
+ */
+class nsGenericHTMLElement : public nsGenericHTMLElementBase {
+ public:
+ using Element::Focus;
+ using Element::SetTabIndex;
+ explicit nsGenericHTMLElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
+ : nsGenericHTMLElementBase(std::move(aNodeInfo)) {
+ NS_ASSERTION(mNodeInfo->NamespaceID() == kNameSpaceID_XHTML,
+ "Unexpected namespace");
+ AddStatesSilently(mozilla::dom::ElementState::LTR);
+ }
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(nsGenericHTMLElement,
+ nsGenericHTMLElementBase)
+
+ NS_IMPL_FROMNODE(nsGenericHTMLElement, kNameSpaceID_XHTML)
+
+ // From Element
+ nsresult CopyInnerTo(mozilla::dom::Element* aDest);
+
+ void GetTitle(mozilla::dom::DOMString& aTitle) {
+ GetHTMLAttr(nsGkAtoms::title, aTitle);
+ }
+ void SetTitle(const nsAString& aTitle) {
+ SetHTMLAttr(nsGkAtoms::title, aTitle);
+ }
+ void GetLang(mozilla::dom::DOMString& aLang) {
+ GetHTMLAttr(nsGkAtoms::lang, aLang);
+ }
+ void SetLang(const nsAString& aLang) { SetHTMLAttr(nsGkAtoms::lang, aLang); }
+ bool Translate() const override;
+ void SetTranslate(bool aTranslate, mozilla::ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::translate, aTranslate ? u"yes"_ns : u"no"_ns,
+ aError);
+ }
+ void GetDir(nsAString& aDir) { GetHTMLEnumAttr(nsGkAtoms::dir, aDir); }
+ void SetDir(const nsAString& aDir, mozilla::ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::dir, aDir, aError);
+ }
+ void GetPopover(nsString& aPopover) const;
+ void SetPopover(const nsAString& aPopover, mozilla::ErrorResult& aError) {
+ SetOrRemoveNullableStringAttr(nsGkAtoms::popover, aPopover, aError);
+ }
+ bool Hidden() const { return GetBoolAttr(nsGkAtoms::hidden); }
+ void SetHidden(bool aHidden, mozilla::ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::hidden, aHidden, aError);
+ }
+ bool Inert() const { return GetBoolAttr(nsGkAtoms::inert); }
+ void SetInert(bool aInert, mozilla::ErrorResult& aError) {
+ SetHTMLBoolAttr(nsGkAtoms::inert, aInert, aError);
+ }
+ MOZ_CAN_RUN_SCRIPT void Click(mozilla::dom::CallerType aCallerType);
+ void GetAccessKey(nsString& aAccessKey) {
+ GetHTMLAttr(nsGkAtoms::accesskey, aAccessKey);
+ }
+ void SetAccessKey(const nsAString& aAccessKey, mozilla::ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::accesskey, aAccessKey, aError);
+ }
+ void GetAccessKeyLabel(nsString& aAccessKeyLabel);
+ virtual bool Draggable() const {
+ return AttrValueIs(kNameSpaceID_None, nsGkAtoms::draggable,
+ nsGkAtoms::_true, eIgnoreCase);
+ }
+ void SetDraggable(bool aDraggable, mozilla::ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::draggable, aDraggable ? u"true"_ns : u"false"_ns,
+ aError);
+ }
+ void GetContentEditable(nsString& aContentEditable) {
+ ContentEditableTristate value = GetContentEditableValue();
+ if (value == eTrue) {
+ aContentEditable.AssignLiteral("true");
+ } else if (value == eFalse) {
+ aContentEditable.AssignLiteral("false");
+ } else {
+ aContentEditable.AssignLiteral("inherit");
+ }
+ }
+ void SetContentEditable(const nsAString& aContentEditable,
+ mozilla::ErrorResult& aError) {
+ if (aContentEditable.LowerCaseEqualsLiteral("inherit")) {
+ UnsetHTMLAttr(nsGkAtoms::contenteditable, aError);
+ } else if (aContentEditable.LowerCaseEqualsLiteral("true")) {
+ SetHTMLAttr(nsGkAtoms::contenteditable, u"true"_ns, aError);
+ } else if (aContentEditable.LowerCaseEqualsLiteral("false")) {
+ SetHTMLAttr(nsGkAtoms::contenteditable, u"false"_ns, aError);
+ } else {
+ aError.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+ }
+ }
+ bool IsContentEditable() {
+ for (nsIContent* node = this; node; node = node->GetParent()) {
+ nsGenericHTMLElement* element = FromNode(node);
+ if (element) {
+ ContentEditableTristate value = element->GetContentEditableValue();
+ if (value != eInherit) {
+ return value == eTrue;
+ }
+ }
+ }
+ return false;
+ }
+
+ mozilla::dom::PopoverAttributeState GetPopoverAttributeState() const;
+ void PopoverPseudoStateUpdate(bool aOpen, bool aNotify);
+ bool PopoverOpen() const;
+ bool CheckPopoverValidity(mozilla::dom::PopoverVisibilityState aExpectedState,
+ Document* aExpectedDocument, ErrorResult& aRv);
+ already_AddRefed<mozilla::dom::ToggleEvent> CreateToggleEvent(
+ const nsAString& aEventType, const nsAString& aOldState,
+ const nsAString& aNewState, mozilla::Cancelable);
+ /** Returns true if the event has been cancelled. */
+ MOZ_CAN_RUN_SCRIPT bool FireToggleEvent(
+ mozilla::dom::PopoverVisibilityState aOldState,
+ mozilla::dom::PopoverVisibilityState aNewState, const nsAString& aType);
+ MOZ_CAN_RUN_SCRIPT void QueuePopoverEventTask(
+ mozilla::dom::PopoverVisibilityState aOldState);
+ MOZ_CAN_RUN_SCRIPT void RunPopoverToggleEventTask(
+ mozilla::dom::PopoverToggleEventTask* aTask,
+ mozilla::dom::PopoverVisibilityState aOldState);
+ MOZ_CAN_RUN_SCRIPT void ShowPopover(ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void ShowPopoverInternal(Element* aInvoker,
+ ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void HidePopoverWithoutRunningScript();
+ MOZ_CAN_RUN_SCRIPT void HidePopoverInternal(bool aFocusPreviousElement,
+ bool aFireEvents,
+ ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void HidePopover(ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT bool TogglePopover(
+ const mozilla::dom::Optional<bool>& aForce, ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void FocusPopover();
+ void ForgetPreviouslyFocusedElementAfterHidingPopover();
+ MOZ_CAN_RUN_SCRIPT void FocusPreviousElementAfterHidingPopover();
+
+ MOZ_CAN_RUN_SCRIPT void FocusCandidate(Element*, bool aClearUpFocus);
+
+ void SetNonce(const nsAString& aNonce) {
+ SetProperty(nsGkAtoms::nonce, new nsString(aNonce),
+ nsINode::DeleteProperty<nsString>, /* aTransfer = */ true);
+ }
+ void RemoveNonce() { RemoveProperty(nsGkAtoms::nonce); }
+ void GetNonce(nsAString& aNonce) const {
+ nsString* cspNonce = static_cast<nsString*>(GetProperty(nsGkAtoms::nonce));
+ if (cspNonce) {
+ aNonce = *cspNonce;
+ }
+ }
+
+ /** Returns whether a form control should be default-focusable. */
+ bool IsFormControlDefaultFocusable(bool aWithMouse) const;
+
+ /**
+ * Returns the count of descendants (inclusive of this node) in
+ * the uncomposed document that are explicitly set as editable.
+ */
+ uint32_t EditableInclusiveDescendantCount();
+
+ bool Spellcheck();
+ void SetSpellcheck(bool aSpellcheck, mozilla::ErrorResult& aError) {
+ SetHTMLAttr(nsGkAtoms::spellcheck, aSpellcheck ? u"true"_ns : u"false"_ns,
+ aError);
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ void GetInnerText(mozilla::dom::DOMString& aValue, ErrorResult& aError);
+ MOZ_CAN_RUN_SCRIPT
+ void GetOuterText(mozilla::dom::DOMString& aValue, ErrorResult& aError) {
+ return GetInnerText(aValue, aError);
+ }
+ MOZ_CAN_RUN_SCRIPT void SetInnerText(const nsAString& aValue);
+ MOZ_CAN_RUN_SCRIPT void SetOuterText(const nsAString& aValue,
+ ErrorResult& aRv);
+
+ void GetInputMode(nsAString& aValue) {
+ GetEnumAttr(nsGkAtoms::inputmode, nullptr, aValue);
+ }
+ void SetInputMode(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::inputmode, aValue, aRv);
+ }
+ virtual void GetAutocapitalize(nsAString& aValue) const;
+ void SetAutocapitalize(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::autocapitalize, aValue, aRv);
+ }
+
+ void GetEnterKeyHint(nsAString& aValue) const {
+ GetEnumAttr(nsGkAtoms::enterkeyhint, nullptr, aValue);
+ }
+ void SetEnterKeyHint(const nsAString& aValue, ErrorResult& aRv) {
+ SetHTMLAttr(nsGkAtoms::enterkeyhint, aValue, aRv);
+ }
+
+ /**
+ * Determine whether an attribute is an event (onclick, etc.)
+ * @param aName the attribute
+ * @return whether the name is an event handler name
+ */
+ bool IsEventAttributeNameInternal(nsAtom* aName) override;
+
+#define EVENT(name_, id_, type_, struct_) /* nothing; handled by nsINode */
+// The using nsINode::Get/SetOn* are to avoid warnings about shadowing the XPCOM
+// getter and setter on nsINode.
+#define FORWARDED_EVENT(name_, id_, type_, struct_) \
+ using nsINode::GetOn##name_; \
+ using nsINode::SetOn##name_; \
+ mozilla::dom::EventHandlerNonNull* GetOn##name_(); \
+ void SetOn##name_(mozilla::dom::EventHandlerNonNull* handler);
+#define ERROR_EVENT(name_, id_, type_, struct_) \
+ using nsINode::GetOn##name_; \
+ using nsINode::SetOn##name_; \
+ already_AddRefed<mozilla::dom::EventHandlerNonNull> GetOn##name_(); \
+ void SetOn##name_(mozilla::dom::EventHandlerNonNull* handler);
+#include "mozilla/EventNameList.h" // IWYU pragma: keep
+#undef ERROR_EVENT
+#undef FORWARDED_EVENT
+#undef EVENT
+ mozilla::dom::Element* GetOffsetParent() {
+ mozilla::CSSIntRect rcFrame;
+ return GetOffsetRect(rcFrame);
+ }
+ int32_t OffsetTop() {
+ mozilla::CSSIntRect rcFrame;
+ GetOffsetRect(rcFrame);
+
+ return rcFrame.y;
+ }
+ int32_t OffsetLeft() {
+ mozilla::CSSIntRect rcFrame;
+ GetOffsetRect(rcFrame);
+
+ return rcFrame.x;
+ }
+ int32_t OffsetWidth() {
+ mozilla::CSSIntRect rcFrame;
+ GetOffsetRect(rcFrame);
+
+ return rcFrame.Width();
+ }
+ int32_t OffsetHeight() {
+ mozilla::CSSIntRect rcFrame;
+ GetOffsetRect(rcFrame);
+
+ return rcFrame.Height();
+ }
+
+ // These methods are already implemented in nsIContent but we want something
+ // faster for HTMLElements ignoring the namespace checking.
+ // This is safe because we already know that we are in the HTML namespace.
+ inline bool IsHTMLElement() const { return true; }
+
+ inline bool IsHTMLElement(nsAtom* aTag) const {
+ return mNodeInfo->Equals(aTag);
+ }
+
+ template <typename First, typename... Args>
+ inline bool IsAnyOfHTMLElements(First aFirst, Args... aArgs) const {
+ return IsNodeInternal(aFirst, aArgs...);
+ }
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#dom-attachinternals
+ virtual already_AddRefed<mozilla::dom::ElementInternals> AttachInternals(
+ ErrorResult& aRv);
+
+ mozilla::dom::ElementInternals* GetInternals() const;
+
+ bool IsFormAssociatedCustomElements() const;
+
+ // Returns true if the event should not be handled from GetEventTargetParent.
+ virtual bool IsDisabledForEvents(mozilla::WidgetEvent* aEvent) {
+ return false;
+ }
+
+ bool Autofocus() const { return GetBoolAttr(nsGkAtoms::autofocus); }
+ void SetAutofocus(bool aVal, ErrorResult& aRv) {
+ SetHTMLBoolAttr(nsGkAtoms::autofocus, aVal, aRv);
+ }
+
+ protected:
+ virtual ~nsGenericHTMLElement() = default;
+
+ public:
+ // Implementation for nsIContent
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ Focusable IsFocusableWithoutStyle(bool aWithMouse) override {
+ Focusable result;
+ IsHTMLFocusable(aWithMouse, &result.mFocusable, &result.mTabIndex);
+ return result;
+ }
+ /**
+ * Returns true if a subclass is not allowed to override the value returned
+ * in aIsFocusable.
+ */
+ virtual bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex);
+ MOZ_CAN_RUN_SCRIPT
+ mozilla::Result<bool, nsresult> PerformAccesskey(
+ bool aKeyCausesActivation, bool aIsTrustedEvent) override;
+
+ /**
+ * Check if an event for an anchor can be handled
+ * @return true if the event can be handled, false otherwise
+ */
+ bool CheckHandleEventForAnchorsPreconditions(
+ mozilla::EventChainVisitor& aVisitor);
+ void GetEventTargetParentForAnchors(mozilla::EventChainPreVisitor& aVisitor);
+ MOZ_CAN_RUN_SCRIPT
+ nsresult PostHandleEventForAnchors(mozilla::EventChainPostVisitor& aVisitor);
+ bool IsHTMLLink(nsIURI** aURI) const;
+
+ // HTML element methods
+ void Compact() { mAttrs.Compact(); }
+
+ void UpdateEditableState(bool aNotify) override;
+
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ bool ParseBackgroundAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue, nsAttrValue& aResult);
+
+ NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
+ nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
+
+ /**
+ * Get the base target for any links within this piece
+ * of content. Generally, this is the document's base target,
+ * but certain content carries a local base for backward
+ * compatibility.
+ *
+ * @param aBaseTarget the base target [OUT]
+ */
+ void GetBaseTarget(nsAString& aBaseTarget) const;
+
+ /**
+ * Get the primary form control frame for this element. Same as
+ * GetPrimaryFrame(), except it QI's to nsIFormControlFrame.
+ *
+ * @param aFlush whether to flush out frames so that they're up to date.
+ * @return the primary frame as nsIFormControlFrame
+ */
+ nsIFormControlFrame* GetFormControlFrame(bool aFlushFrames);
+
+ //----------------------------------------
+
+ /**
+ * Parse an alignment attribute (top/middle/bottom/baseline)
+ *
+ * @param aString the string to parse
+ * @param aResult the resulting HTMLValue
+ * @return whether the value was parsed
+ */
+ static bool ParseAlignValue(const nsAString& aString, nsAttrValue& aResult);
+
+ /**
+ * Parse a div align string to value (left/right/center/middle/justify)
+ *
+ * @param aString the string to parse
+ * @param aResult the resulting HTMLValue
+ * @return whether the value was parsed
+ */
+ static bool ParseDivAlignValue(const nsAString& aString,
+ nsAttrValue& aResult);
+
+ /**
+ * Convert a table halign string to value (left/right/center/char/justify)
+ *
+ * @param aString the string to parse
+ * @param aResult the resulting HTMLValue
+ * @return whether the value was parsed
+ */
+ static bool ParseTableHAlignValue(const nsAString& aString,
+ nsAttrValue& aResult);
+
+ /**
+ * Convert a table cell halign string to value
+ *
+ * @param aString the string to parse
+ * @param aResult the resulting HTMLValue
+ * @return whether the value was parsed
+ */
+ static bool ParseTableCellHAlignValue(const nsAString& aString,
+ nsAttrValue& aResult);
+
+ /**
+ * Convert a table valign string to value (left/right/center/char/justify/
+ * abscenter/absmiddle/middle)
+ *
+ * @param aString the string to parse
+ * @param aResult the resulting HTMLValue
+ * @return whether the value was parsed
+ */
+ static bool ParseTableVAlignValue(const nsAString& aString,
+ nsAttrValue& aResult);
+
+ /**
+ * Convert an image attribute to value (width, height, hspace, vspace, border)
+ *
+ * @param aAttribute the attribute to parse
+ * @param aString the string to parse
+ * @param aResult the resulting HTMLValue
+ * @return whether the value was parsed
+ */
+ static bool ParseImageAttribute(nsAtom* aAttribute, const nsAString& aString,
+ nsAttrValue& aResult);
+
+ static bool ParseReferrerAttribute(const nsAString& aString,
+ nsAttrValue& aResult);
+
+ /**
+ * Convert a frameborder string to value (yes/no/1/0)
+ *
+ * @param aString the string to parse
+ * @param aResult the resulting HTMLValue
+ * @return whether the value was parsed
+ */
+ static bool ParseFrameborderValue(const nsAString& aString,
+ nsAttrValue& aResult);
+
+ /**
+ * Convert a scrolling string to value (yes/no/on/off/scroll/noscroll/auto)
+ *
+ * @param aString the string to parse
+ * @param aResult the resulting HTMLValue
+ * @return whether the value was parsed
+ */
+ static bool ParseScrollingValue(const nsAString& aString,
+ nsAttrValue& aResult);
+
+ /*
+ * Attribute Mapping Helpers
+ */
+
+ /**
+ * A style attribute mapping function for the most common attributes, to be
+ * called by subclasses' attribute mapping functions. Currently handles
+ * dir, lang and hidden, could handle others.
+ *
+ * @param aAttributes the list of attributes to map
+ * @param aData the returned rule data [INOUT]
+ * @see GetAttributeMappingFunction
+ */
+ static void MapCommonAttributesInto(mozilla::MappedDeclarationsBuilder&);
+ /**
+ * Same as MapCommonAttributesInto except that it does not handle hidden.
+ * @see GetAttributeMappingFunction
+ */
+ static void MapCommonAttributesIntoExceptHidden(
+ mozilla::MappedDeclarationsBuilder&);
+
+ static const MappedAttributeEntry sCommonAttributeMap[];
+ static const MappedAttributeEntry sImageMarginSizeAttributeMap[];
+ static const MappedAttributeEntry sImageBorderAttributeMap[];
+ static const MappedAttributeEntry sImageAlignAttributeMap[];
+ static const MappedAttributeEntry sDivAlignAttributeMap[];
+ static const MappedAttributeEntry sBackgroundAttributeMap[];
+ static const MappedAttributeEntry sBackgroundColorAttributeMap[];
+
+ /**
+ * Helper to map the align attribute.
+ * @see GetAttributeMappingFunction
+ */
+ static void MapImageAlignAttributeInto(mozilla::MappedDeclarationsBuilder&);
+
+ /**
+ * Helper to map the align attribute for things like <div>, <h1>, etc.
+ * @see GetAttributeMappingFunction
+ */
+ static void MapDivAlignAttributeInto(mozilla::MappedDeclarationsBuilder&);
+
+ /**
+ * Helper to map the valign attribute for things like <col>, <tr>, <section>.
+ * @see GetAttributeMappingFunction
+ */
+ static void MapVAlignAttributeInto(mozilla::MappedDeclarationsBuilder&);
+
+ /**
+ * Helper to map the image border attribute.
+ * @see GetAttributeMappingFunction
+ */
+ static void MapImageBorderAttributeInto(mozilla::MappedDeclarationsBuilder&);
+ /**
+ * Helper to map the image margin attribute into a style struct.
+ *
+ * @param aAttributes the list of attributes to map
+ * @param aData the returned rule data [INOUT]
+ * @see GetAttributeMappingFunction
+ */
+ static void MapImageMarginAttributeInto(mozilla::MappedDeclarationsBuilder&);
+
+ /**
+ * Helper to map a given dimension (width/height) into the declaration
+ * block, handling percentages and numbers.
+ */
+ static void MapDimensionAttributeInto(mozilla::MappedDeclarationsBuilder&,
+ nsCSSPropertyID, const nsAttrValue&);
+
+ /**
+ * Maps the aspect ratio given width and height attributes.
+ */
+ static void DoMapAspectRatio(const nsAttrValue& aWidth,
+ const nsAttrValue& aHeight,
+ mozilla::MappedDeclarationsBuilder&);
+
+ // Whether to map the width and height attributes to aspect-ratio.
+ enum class MapAspectRatio { No, Yes };
+
+ /**
+ * Helper to map the image position attribute into a style struct.
+ */
+ static void MapImageSizeAttributesInto(mozilla::MappedDeclarationsBuilder&,
+ MapAspectRatio = MapAspectRatio::No);
+
+ /**
+ * Helper to map the width and height attributes into the aspect-ratio
+ * property.
+ *
+ * If you also map the width/height attributes to width/height (as you should
+ * for any HTML element that isn't <canvas>) then you should use
+ * MapImageSizeAttributesInto instead, passing MapAspectRatio::Yes instead, as
+ * that'd be faster.
+ */
+ static void MapAspectRatioInto(mozilla::MappedDeclarationsBuilder&);
+
+ /**
+ * Helper to map `width` attribute into a style struct.
+ *
+ * @param aAttributes the list of attributes to map
+ * @param aData the returned rule data [INOUT]
+ * @see GetAttributeMappingFunction
+ */
+ static void MapWidthAttributeInto(mozilla::MappedDeclarationsBuilder&);
+
+ /**
+ * Helper to map `height` attribute.
+ * @see GetAttributeMappingFunction
+ */
+ static void MapHeightAttributeInto(mozilla::MappedDeclarationsBuilder&);
+ /**
+ * Helper to map the background attribute
+ * @see GetAttributeMappingFunction
+ */
+ static void MapBackgroundInto(mozilla::MappedDeclarationsBuilder&);
+ /**
+ * Helper to map the bgcolor attribute
+ * @see GetAttributeMappingFunction
+ */
+ static void MapBGColorInto(mozilla::MappedDeclarationsBuilder&);
+ /**
+ * Helper to map the background attributes (currently background and bgcolor)
+ * @see GetAttributeMappingFunction
+ */
+ static void MapBackgroundAttributesInto(mozilla::MappedDeclarationsBuilder&);
+ /**
+ * Helper to map the scrolling attribute on FRAME and IFRAME.
+ * @see GetAttributeMappingFunction
+ */
+ static void MapScrollingAttributeInto(mozilla::MappedDeclarationsBuilder&);
+
+ // Form Helper Routines
+ /**
+ * Find an ancestor of this content node which is a form (could be null)
+ * @param aCurrentForm the current form for this node. If this is
+ * non-null, and no ancestor form is found, and the current form is in
+ * a connected subtree with the node, the current form will be
+ * returned. This is needed to handle cases when HTML elements have a
+ * current form that they're not descendants of.
+ * @note This method should not be called if the element has a form attribute.
+ */
+ mozilla::dom::HTMLFormElement* FindAncestorForm(
+ mozilla::dom::HTMLFormElement* aCurrentForm = nullptr);
+
+ /**
+ * See if the document being tested has nav-quirks mode enabled.
+ * @param doc the document
+ */
+ static bool InNavQuirksMode(Document*);
+
+ /**
+ * Gets the absolute URI value of an attribute, by resolving any relative
+ * URIs in the attribute against the baseuri of the element. If the attribute
+ * isn't a relative URI the value of the attribute is returned as is. Only
+ * works for attributes in null namespace.
+ *
+ * @param aAttr name of attribute.
+ * @param aBaseAttr name of base attribute.
+ * @param aResult result value [out]
+ */
+ void GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, nsAString& aResult) const;
+
+ /**
+ * Gets the absolute URI values of an attribute, by resolving any relative
+ * URIs in the attribute against the baseuri of the element. If a substring
+ * isn't a relative URI, the substring is returned as is. Only works for
+ * attributes in null namespace.
+ */
+ bool GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, nsIURI** aURI) const;
+
+ bool IsHidden() const { return HasAttr(nsGkAtoms::hidden); }
+
+ bool IsLabelable() const override;
+
+ static bool MatchLabelsElement(Element* aElement, int32_t aNamespaceID,
+ nsAtom* aAtom, void* aData);
+
+ already_AddRefed<nsINodeList> Labels();
+
+ static bool LegacyTouchAPIEnabled(JSContext* aCx, JSObject* aObj);
+
+ static inline bool CanHaveName(nsAtom* aTag) {
+ return aTag == nsGkAtoms::img || aTag == nsGkAtoms::form ||
+ aTag == nsGkAtoms::embed || aTag == nsGkAtoms::object;
+ }
+ static inline bool ShouldExposeNameAsHTMLDocumentProperty(Element* aElement) {
+ return aElement->IsHTMLElement() &&
+ CanHaveName(aElement->NodeInfo()->NameAtom());
+ }
+ static inline bool ShouldExposeIdAsHTMLDocumentProperty(Element* aElement) {
+ if (aElement->IsHTMLElement(nsGkAtoms::object)) {
+ return true;
+ }
+
+ // Per spec, <img> is exposed by id only if it also has a nonempty
+ // name (which doesn't have to match the id or anything).
+ // HasName() is true precisely when name is nonempty.
+ return aElement->IsHTMLElement(nsGkAtoms::img) && aElement->HasName();
+ }
+
+ virtual inline void ResultForDialogSubmit(nsAString& aResult) {
+ GetAttr(nsGkAtoms::value, aResult);
+ }
+
+ // <https://html.spec.whatwg.org/#fetch-priority-attribute>.
+ static mozilla::dom::FetchPriority ToFetchPriority(const nsAString& aValue);
+
+ void GetFetchPriority(nsAString& aFetchPriority) const;
+
+ void SetFetchPriority(const nsAString& aFetchPriority) {
+ SetHTMLAttr(nsGkAtoms::fetchpriority, aFetchPriority);
+ }
+
+ protected:
+ mozilla::dom::FetchPriority GetFetchPriority() const;
+
+ static void ParseFetchPriority(const nsAString& aValue, nsAttrValue& aResult);
+
+ private:
+ /**
+ * Add/remove this element to the documents name cache
+ */
+ void AddToNameTable(nsAtom* aName);
+ void RemoveFromNameTable();
+
+ /**
+ * Register or unregister an access key to this element based on the
+ * accesskey attribute.
+ */
+ void RegUnRegAccessKey(bool aDoReg) override {
+ if (!HasFlag(NODE_HAS_ACCESSKEY)) {
+ return;
+ }
+
+ nsStyledElement::RegUnRegAccessKey(aDoReg);
+ }
+
+ protected:
+ void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+ // TODO: Convert AfterSetAttr to MOZ_CAN_RUN_SCRIPT and get rid of
+ // kungFuDeathGrip in it.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void AfterSetAttr(
+ int32_t aNamespaceID, nsAtom* aName, const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+
+ MOZ_CAN_RUN_SCRIPT void AfterSetPopoverAttr();
+
+ mozilla::EventListenerManager* GetEventListenerManagerForAttr(
+ nsAtom* aAttrName, bool* aDefer) override;
+
+ /**
+ * Handles dispatching a simulated click on `this` on space or enter.
+ * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void HandleKeyboardActivation(
+ mozilla::EventChainPostVisitor&);
+
+ /** Dispatch a simulated mouse click by keyboard to the given element. */
+ MOZ_CAN_RUN_SCRIPT static nsresult DispatchSimulatedClick(
+ nsGenericHTMLElement* aElement, bool aIsTrusted,
+ nsPresContext* aPresContext);
+
+ /**
+ * Create a URI for the given aURISpec string.
+ * Returns INVALID_STATE_ERR and nulls *aURI if aURISpec is empty
+ * and the document's URI matches the element's base URI.
+ */
+ nsresult NewURIFromString(const nsAString& aURISpec, nsIURI** aURI);
+
+ void GetHTMLAttr(nsAtom* aName, nsAString& aResult) const {
+ GetAttr(aName, aResult);
+ }
+ void GetHTMLAttr(nsAtom* aName, mozilla::dom::DOMString& aResult) const {
+ GetAttr(aName, aResult);
+ }
+ void GetHTMLEnumAttr(nsAtom* aName, nsAString& aResult) const {
+ GetEnumAttr(aName, nullptr, aResult);
+ }
+ void GetHTMLURIAttr(nsAtom* aName, nsAString& aResult) const {
+ GetURIAttr(aName, nullptr, aResult);
+ }
+
+ void SetHTMLAttr(nsAtom* aName, const nsAString& aValue) {
+ SetAttr(kNameSpaceID_None, aName, aValue, true);
+ }
+ void SetHTMLAttr(nsAtom* aName, const nsAString& aValue,
+ mozilla::ErrorResult& aError) {
+ SetAttr(aName, aValue, aError);
+ }
+ void SetHTMLAttr(nsAtom* aName, const nsAString& aValue,
+ nsIPrincipal* aTriggeringPrincipal,
+ mozilla::ErrorResult& aError) {
+ SetAttr(aName, aValue, aTriggeringPrincipal, aError);
+ }
+ void UnsetHTMLAttr(nsAtom* aName, mozilla::ErrorResult& aError) {
+ UnsetAttr(aName, aError);
+ }
+ void SetHTMLBoolAttr(nsAtom* aName, bool aValue,
+ mozilla::ErrorResult& aError) {
+ if (aValue) {
+ SetHTMLAttr(aName, u""_ns, aError);
+ } else {
+ UnsetHTMLAttr(aName, aError);
+ }
+ }
+ template <typename T>
+ void SetHTMLIntAttr(nsAtom* aName, T aValue, mozilla::ErrorResult& aError) {
+ nsAutoString value;
+ value.AppendInt(aValue);
+
+ SetHTMLAttr(aName, value, aError);
+ }
+
+ /**
+ * Gets the integer-value of an attribute, returns specified default value
+ * if the attribute isn't set or isn't set to an integer. Only works for
+ * attributes in null namespace.
+ *
+ * @param aAttr name of attribute.
+ * @param aDefault default-value to return if attribute isn't set.
+ */
+ int32_t GetIntAttr(nsAtom* aAttr, int32_t aDefault) const;
+
+ /**
+ * Sets value of attribute to specified integer. Only works for attributes
+ * in null namespace.
+ *
+ * @param aAttr name of attribute.
+ * @param aValue Integer value of attribute.
+ */
+ nsresult SetIntAttr(nsAtom* aAttr, int32_t aValue);
+
+ /**
+ * Gets the unsigned integer-value of an attribute, returns specified default
+ * value if the attribute isn't set or isn't set to an integer. Only works for
+ * attributes in null namespace.
+ *
+ * @param aAttr name of attribute.
+ * @param aDefault default-value to return if attribute isn't set.
+ */
+ uint32_t GetUnsignedIntAttr(nsAtom* aAttr, uint32_t aDefault) const;
+
+ /**
+ * Sets value of attribute to specified unsigned integer. Only works for
+ * attributes in null namespace.
+ *
+ * @param aAttr name of attribute.
+ * @param aValue Integer value of attribute.
+ * @param aDefault Default value (in case value is out of range). If the spec
+ * doesn't provide one, should be 1 if the value is limited to
+ * nonzero values, and 0 otherwise.
+ */
+ void SetUnsignedIntAttr(nsAtom* aName, uint32_t aValue, uint32_t aDefault,
+ mozilla::ErrorResult& aError) {
+ nsAutoString value;
+ if (aValue > INT32_MAX) {
+ value.AppendInt(aDefault);
+ } else {
+ value.AppendInt(aValue);
+ }
+
+ SetHTMLAttr(aName, value, aError);
+ }
+
+ /**
+ * Gets the unsigned integer-value of an attribute that is stored as a
+ * dimension (i.e. could be an integer or a percentage), returns specified
+ * default value if the attribute isn't set or isn't set to a dimension. Only
+ * works for attributes in null namespace.
+ *
+ * @param aAttr name of attribute.
+ * @param aDefault default-value to return if attribute isn't set.
+ */
+ uint32_t GetDimensionAttrAsUnsignedInt(nsAtom* aAttr,
+ uint32_t aDefault) const;
+
+ enum class Reflection {
+ Unlimited,
+ OnlyPositive,
+ };
+
+ /**
+ * Sets value of attribute to specified double. Only works for attributes
+ * in null namespace.
+ *
+ * Implements
+ * https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-double
+ *
+ * @param aAttr name of attribute.
+ * @param aValue Double value of attribute.
+ */
+ template <Reflection Limited = Reflection::Unlimited>
+ void SetDoubleAttr(nsAtom* aAttr, double aValue, mozilla::ErrorResult& aRv) {
+ // 1. If the reflected IDL attribute is limited to only positive numbers and
+ // the given value is not greater than 0, then return.
+ if (Limited == Reflection::OnlyPositive && aValue <= 0) {
+ return;
+ }
+
+ // 2. Run this's set the content attribute with the given value, converted
+ // to the best representation of the number as a floating-point number.
+ nsAutoString value;
+ value.AppendFloat(aValue);
+
+ SetHTMLAttr(aAttr, value, aRv);
+ }
+
+ /**
+ * Locates the EditorBase associated with this node. In general this is
+ * equivalent to GetEditorInternal(), but for designmode or contenteditable,
+ * this may need to get an editor that's not actually on this element's
+ * associated TextControlFrame. This is used by the spellchecking routines
+ * to get the editor affected by changing the spellcheck attribute on this
+ * node.
+ */
+ virtual already_AddRefed<mozilla::EditorBase> GetAssociatedEditor();
+
+ /**
+ * Get the frame's offset information for offsetTop/Left/Width/Height.
+ * Returns the parent the offset is relative to.
+ * @note This method flushes pending notifications (FlushType::Layout).
+ * @param aRect the offset information [OUT]
+ */
+ mozilla::dom::Element* GetOffsetRect(mozilla::CSSIntRect& aRect);
+
+ /**
+ * Ensures all editors associated with a subtree are synced, for purposes of
+ * spellchecking.
+ */
+ static void SyncEditorsOnSubtree(nsIContent* content);
+
+ enum ContentEditableTristate { eInherit = -1, eFalse = 0, eTrue = 1 };
+
+ /**
+ * Returns eTrue if the element has a contentEditable attribute and its value
+ * is "true" or an empty string. Returns eFalse if the element has a
+ * contentEditable attribute and its value is "false". Otherwise returns
+ * eInherit.
+ */
+ ContentEditableTristate GetContentEditableValue() const {
+ static const Element::AttrValuesArray values[] = {
+ nsGkAtoms::_false, nsGkAtoms::_true, nsGkAtoms::_empty, nullptr};
+
+ if (!MayHaveContentEditableAttr()) return eInherit;
+
+ int32_t value = FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::contenteditable, values, eIgnoreCase);
+
+ return value > 0 ? eTrue : (value == 0 ? eFalse : eInherit);
+ }
+
+ // Used by A, AREA, LINK, and STYLE.
+ already_AddRefed<nsIURI> GetHrefURIForAnchors() const;
+
+ private:
+ void ChangeEditableState(int32_t aChange);
+};
+
+namespace mozilla::dom {
+class HTMLFieldSetElement;
+} // namespace mozilla::dom
+
+#define HTML_ELEMENT_FLAG_BIT(n_) \
+ NODE_FLAG_BIT(ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + (n_))
+
+// HTMLElement specific bits
+enum {
+ // Used to handle keyboard activation.
+ HTML_ELEMENT_ACTIVE_FOR_KEYBOARD = HTML_ELEMENT_FLAG_BIT(0),
+ // Similar to HTMLInputElement's mInhibitRestoration, used to prevent
+ // form-associated custom elements not created by a network parser from
+ // being restored.
+ HTML_ELEMENT_INHIBIT_RESTORATION = HTML_ELEMENT_FLAG_BIT(1),
+
+ // Remaining bits are type specific.
+ HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET =
+ ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 2,
+};
+
+ASSERT_NODE_FLAGS_SPACE(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET);
+
+#define FORM_ELEMENT_FLAG_BIT(n_) \
+ NODE_FLAG_BIT(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + (n_))
+
+// Form element specific bits
+enum {
+ // If this flag is set on an nsGenericHTMLFormElement or an HTMLImageElement,
+ // that means that we have added ourselves to our mForm. It's possible to
+ // have a non-null mForm, but not have this flag set. That happens when the
+ // form is set via the content sink.
+ ADDED_TO_FORM = FORM_ELEMENT_FLAG_BIT(0),
+
+ // If this flag is set on an nsGenericHTMLFormElement or an HTMLImageElement,
+ // that means that its form is in the process of being unbound from the tree,
+ // and this form element hasn't re-found its form in
+ // nsGenericHTMLFormElement::UnbindFromTree yet.
+ MAYBE_ORPHAN_FORM_ELEMENT = FORM_ELEMENT_FLAG_BIT(1),
+
+ // If this flag is set on an nsGenericHTMLElement or an HTMLImageElement, then
+ // the element might be in the past names map of its form.
+ MAY_BE_IN_PAST_NAMES_MAP = FORM_ELEMENT_FLAG_BIT(2)
+};
+
+// NOTE: I don't think it's possible to have both ADDED_TO_FORM and
+// MAYBE_ORPHAN_FORM_ELEMENT set at the same time, so if it becomes an issue we
+// can probably merge them into the same bit. --bz
+
+ASSERT_NODE_FLAGS_SPACE(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 3);
+
+#undef FORM_ELEMENT_FLAG_BIT
+
+/**
+ * A helper class for form elements that can contain children
+ */
+class nsGenericHTMLFormElement : public nsGenericHTMLElement {
+ public:
+ nsGenericHTMLFormElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
+
+ // nsIContent
+ void SaveSubtreeState() override;
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+
+ /**
+ * This callback is called by a fieldest on all its elements whenever its
+ * disabled attribute is changed so the element knows its disabled state
+ * might have changed.
+ *
+ * @note Classes redefining this method should not do any content
+ * state updates themselves but should just make sure to call into
+ * nsGenericHTMLFormElement::FieldSetDisabledChanged.
+ */
+ virtual void FieldSetDisabledChanged(bool aNotify);
+
+ void FieldSetFirstLegendChanged(bool aNotify) { UpdateFieldSet(aNotify); }
+
+ /**
+ * This callback is called by a fieldset on all it's elements when it's being
+ * destroyed. When called, the elements should check that aFieldset is there
+ * first parent fieldset and null mFieldset in that case only.
+ *
+ * @param aFieldSet The fieldset being removed.
+ */
+ void ForgetFieldSet(nsIContent* aFieldset);
+
+ void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete);
+
+ /**
+ * Get the layout history object for a particular piece of content.
+ *
+ * @param aRead if true, won't return a layout history state if the
+ * layout history state is empty.
+ * @return the history state object
+ */
+ already_AddRefed<nsILayoutHistoryState> GetLayoutHistory(bool aRead);
+
+ // Sets the user-interacted flag in
+ // https://html.spec.whatwg.org/#user-interacted, if it applies.
+ virtual void SetUserInteracted(bool aNotify) {}
+
+ protected:
+ virtual ~nsGenericHTMLFormElement() = default;
+
+ void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValue* aValue, bool aNotify) override;
+
+ void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue, const nsAttrValue* aOldValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) override;
+
+ virtual void BeforeSetForm(mozilla::dom::HTMLFormElement* aForm,
+ bool aBindToTree) {}
+
+ virtual void AfterClearForm(bool aUnbindOrDelete) {}
+
+ /**
+ * Check our disabled content attribute and fieldset's (if it exists) disabled
+ * state to decide whether our disabled flag should be toggled.
+ */
+ virtual void UpdateDisabledState(bool aNotify);
+ bool IsReadOnlyInternal() const final;
+
+ virtual void SetFormInternal(mozilla::dom::HTMLFormElement* aForm,
+ bool aBindToTree) {}
+
+ virtual mozilla::dom::HTMLFormElement* GetFormInternal() const {
+ return nullptr;
+ }
+
+ virtual mozilla::dom::HTMLFieldSetElement* GetFieldSetInternal() const {
+ return nullptr;
+ }
+
+ virtual void SetFieldSetInternal(
+ mozilla::dom::HTMLFieldSetElement* aFieldset) {}
+
+ /**
+ * This method will update the form owner, using @form or looking to a parent.
+ *
+ * @param aBindToTree Whether the element is being attached to the tree.
+ * @param aFormIdElement The element associated with the id in @form. If
+ * aBindToTree is false, aFormIdElement *must* contain the element associated
+ * with the id in @form. Otherwise, it *must* be null.
+ *
+ * @note Callers of UpdateFormOwner have to be sure the element is in a
+ * document (GetUncomposedDoc() != nullptr).
+ */
+ virtual void UpdateFormOwner(bool aBindToTree, Element* aFormIdElement);
+
+ /**
+ * This method will update mFieldset and set it to the first fieldset parent.
+ */
+ void UpdateFieldSet(bool aNotify);
+
+ /**
+ * Add a form id observer which will observe when the element with the id in
+ * @form will change.
+ *
+ * @return The element associated with the current id in @form (may be null).
+ */
+ Element* AddFormIdObserver();
+
+ /**
+ * Remove the form id observer.
+ */
+ void RemoveFormIdObserver();
+
+ /**
+ * This method is a a callback for IDTargetObserver (from Document).
+ * It will be called each time the element associated with the id in @form
+ * changes.
+ */
+ static bool FormIdUpdated(Element* aOldElement, Element* aNewElement,
+ void* aData);
+
+ // Returns true if the event should not be handled from GetEventTargetParent
+ bool IsElementDisabledForEvents(mozilla::WidgetEvent* aEvent,
+ nsIFrame* aFrame);
+
+ /**
+ * Returns if the control can be disabled.
+ */
+ virtual bool CanBeDisabled() const { return false; }
+
+ /**
+ * Returns if the readonly attribute applies.
+ */
+ virtual bool DoesReadOnlyApply() const { return false; }
+
+ /**
+ * Returns true if the element is a form associated element.
+ * See https://html.spec.whatwg.org/#form-associated-element.
+ */
+ virtual bool IsFormAssociatedElement() const { return false; }
+
+ /**
+ * Save to presentation state. The form element will determine whether it
+ * has anything to save and if so, create an entry in the layout history for
+ * its pres context.
+ */
+ virtual void SaveState() {}
+};
+
+class nsGenericHTMLFormControlElement : public nsGenericHTMLFormElement,
+ public nsIFormControl {
+ public:
+ nsGenericHTMLFormControlElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, FormControlType);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_IMPL_FROMNODE_HELPER(nsGenericHTMLFormControlElement,
+ IsHTMLFormControlElement())
+
+ // nsINode
+ nsINode* GetScopeChainParent() const override;
+ bool IsHTMLFormControlElement() const final { return true; }
+
+ // nsIContent
+ IMEState GetDesiredIMEState() override;
+
+ // nsGenericHTMLElement
+ // autocapitalize attribute support
+ void GetAutocapitalize(nsAString& aValue) const override;
+ bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+
+ // EventTarget
+ void GetEventTargetParent(mozilla::EventChainPreVisitor& aVisitor) override;
+ nsresult PreHandleEvent(mozilla::EventChainVisitor& aVisitor) override;
+
+ // nsIFormControl
+ mozilla::dom::HTMLFieldSetElement* GetFieldSet() override;
+ mozilla::dom::HTMLFormElement* GetForm() const override { return mForm; }
+ void SetForm(mozilla::dom::HTMLFormElement* aForm) override;
+ void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) override;
+
+ protected:
+ virtual ~nsGenericHTMLFormControlElement();
+
+ // Element
+ bool IsLabelable() const override;
+
+ // nsGenericHTMLFormElement
+ bool CanBeDisabled() const override;
+ bool DoesReadOnlyApply() const override;
+ void SetFormInternal(mozilla::dom::HTMLFormElement* aForm,
+ bool aBindToTree) override;
+ mozilla::dom::HTMLFormElement* GetFormInternal() const override;
+ mozilla::dom::HTMLFieldSetElement* GetFieldSetInternal() const override;
+ void SetFieldSetInternal(
+ mozilla::dom::HTMLFieldSetElement* aFieldset) override;
+ bool IsFormAssociatedElement() const override { return true; }
+
+ /**
+ * Update our required/optional flags to match the given aIsRequired boolean.
+ */
+ void UpdateRequiredState(bool aIsRequired, bool aNotify);
+
+ bool IsAutocapitalizeInheriting() const;
+
+ nsresult SubmitDirnameDir(mozilla::dom::FormData* aFormData);
+
+ /** The form that contains this control */
+ mozilla::dom::HTMLFormElement* mForm;
+
+ /* This is a pointer to our closest fieldset parent if any */
+ mozilla::dom::HTMLFieldSetElement* mFieldSet;
+};
+
+enum class PopoverTargetAction : uint8_t {
+ Toggle,
+ Show,
+ Hide,
+};
+
+class nsGenericHTMLFormControlElementWithState
+ : public nsGenericHTMLFormControlElement {
+ public:
+ nsGenericHTMLFormControlElementWithState(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser, FormControlType);
+
+ bool IsGenericHTMLFormControlElementWithState() const final { return true; }
+ NS_IMPL_FROMNODE_HELPER(nsGenericHTMLFormControlElementWithState,
+ IsGenericHTMLFormControlElementWithState())
+
+ // Element
+ bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute,
+ const nsAString& aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ nsAttrValue& aResult) override;
+
+ // PopoverInvokerElement
+ mozilla::dom::Element* GetPopoverTargetElement() const;
+ void SetPopoverTargetElement(mozilla::dom::Element*);
+ void GetPopoverTargetAction(nsAString& aValue) const {
+ GetHTMLEnumAttr(nsGkAtoms::popovertargetaction, aValue);
+ }
+ void SetPopoverTargetAction(const nsAString& aValue) {
+ SetHTMLAttr(nsGkAtoms::popovertargetaction, aValue);
+ }
+
+ // InvokerElement
+ mozilla::dom::Element* GetInvokeTargetElement() const;
+ void SetInvokeTargetElement(mozilla::dom::Element*);
+ void GetInvokeAction(nsAString& aValue) const;
+ nsAtom* GetInvokeAction() const;
+ void SetInvokeAction(const nsAString& aValue) {
+ SetHTMLAttr(nsGkAtoms::invokeaction, aValue);
+ }
+
+ /**
+ * https://html.spec.whatwg.org/#popover-target-attribute-activation-behavior
+ */
+ MOZ_CAN_RUN_SCRIPT void HandlePopoverTargetAction();
+
+ MOZ_CAN_RUN_SCRIPT void HandleInvokeTargetAction();
+
+ /**
+ * Get the presentation state for a piece of content, or create it if it does
+ * not exist. Generally used by SaveState().
+ */
+ mozilla::PresState* GetPrimaryPresState();
+
+ /**
+ * Called when we have been cloned and adopted, and the information of the
+ * node has been changed.
+ */
+ void NodeInfoChanged(Document* aOldDoc) override;
+
+ void GetFormAction(nsString& aValue);
+
+ protected:
+ /**
+ * Restore from presentation state. You pass in the presentation state for
+ * this form control (generated with GenerateStateKey() + "-C") and the form
+ * control will grab its state from there.
+ *
+ * @param aState the pres state to use to restore the control
+ * @return true if the form control was a checkbox and its
+ * checked state was restored, false otherwise.
+ */
+ virtual bool RestoreState(mozilla::PresState* aState) { return false; }
+
+ /**
+ * Restore the state for a form control in response to the element being
+ * inserted into the document by the parser. Ends up calling RestoreState().
+ *
+ * GenerateStateKey() must already have been called.
+ *
+ * @return false if RestoreState() was not called, the return
+ * value of RestoreState() otherwise.
+ */
+ bool RestoreFormControlState();
+
+ /* Generates the state key for saving the form state in the session if not
+ computed already. The result is stored in mStateKey. */
+ void GenerateStateKey();
+
+ int32_t GetParserInsertedControlNumberForStateKey() const override {
+ return mControlNumber;
+ }
+
+ /* Used to store the key to that element in the session. Is void until
+ GenerateStateKey has been used */
+ nsCString mStateKey;
+
+ // A number for this form control that is unique within its owner document.
+ // This is only set to a number for elements inserted into the document by
+ // the parser from the network. Otherwise, it is -1.
+ int32_t mControlNumber;
+};
+
+#define NS_INTERFACE_MAP_ENTRY_IF_TAG(_interface, _tag) \
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(_interface, \
+ mNodeInfo->Equals(nsGkAtoms::_tag))
+
+namespace mozilla::dom {
+
+using HTMLContentCreatorFunction =
+ nsGenericHTMLElement* (*)(already_AddRefed<mozilla::dom::NodeInfo>&&,
+ mozilla::dom::FromParser);
+
+} // namespace mozilla::dom
+
+/**
+ * A macro to declare the NS_NewHTMLXXXElement() functions.
+ */
+#define NS_DECLARE_NS_NEW_HTML_ELEMENT(_elementName) \
+ namespace mozilla { \
+ namespace dom { \
+ class HTML##_elementName##Element; \
+ } \
+ } \
+ nsGenericHTMLElement* NS_NewHTML##_elementName##Element( \
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, \
+ mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER);
+
+#define NS_DECLARE_NS_NEW_HTML_ELEMENT_AS_SHARED(_elementName) \
+ inline nsGenericHTMLElement* NS_NewHTML##_elementName##Element( \
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, \
+ mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER) { \
+ return NS_NewHTMLSharedElement(std::move(aNodeInfo), aFromParser); \
+ }
+
+/**
+ * A macro to implement the NS_NewHTMLXXXElement() functions.
+ */
+#define NS_IMPL_NS_NEW_HTML_ELEMENT(_elementName) \
+ nsGenericHTMLElement* NS_NewHTML##_elementName##Element( \
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, \
+ mozilla::dom::FromParser aFromParser) { \
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); \
+ auto* nim = nodeInfo->NodeInfoManager(); \
+ MOZ_ASSERT(nim); \
+ return new (nim) \
+ mozilla::dom::HTML##_elementName##Element(nodeInfo.forget()); \
+ }
+
+#define NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(_elementName) \
+ nsGenericHTMLElement* NS_NewHTML##_elementName##Element( \
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, \
+ mozilla::dom::FromParser aFromParser) { \
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); \
+ auto* nim = nodeInfo->NodeInfoManager(); \
+ MOZ_ASSERT(nim); \
+ return new (nim) mozilla::dom::HTML##_elementName##Element( \
+ nodeInfo.forget(), aFromParser); \
+ }
+
+// Here, we expand 'NS_DECLARE_NS_NEW_HTML_ELEMENT()' by hand.
+// (Calling the macro directly (with no args) produces compiler warnings.)
+nsGenericHTMLElement* NS_NewHTMLElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER);
+
+// Distinct from the above in order to have function pointer that compared
+// unequal to a function pointer to the above.
+nsGenericHTMLElement* NS_NewCustomElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER);
+
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Shared)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(SharedList)
+
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Anchor)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Area)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Audio)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(BR)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Body)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Button)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Canvas)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Content)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Mod)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Data)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(DataList)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Details)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Dialog)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Div)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Embed)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(FieldSet)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Font)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Form)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Frame)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(FrameSet)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(HR)
+NS_DECLARE_NS_NEW_HTML_ELEMENT_AS_SHARED(Head)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Heading)
+NS_DECLARE_NS_NEW_HTML_ELEMENT_AS_SHARED(Html)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(IFrame)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Image)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Input)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(LI)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Label)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Legend)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Link)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Marquee)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Map)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Menu)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Meta)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Meter)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Object)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(OptGroup)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Option)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Output)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Paragraph)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Picture)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Pre)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Progress)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Script)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Select)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Slot)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Source)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Span)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Style)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Summary)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(TableCaption)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(TableCell)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(TableCol)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Table)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(TableRow)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(TableSection)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Tbody)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Template)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(TextArea)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Tfoot)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Thead)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Time)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Title)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Track)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Unknown)
+NS_DECLARE_NS_NEW_HTML_ELEMENT(Video)
+
+#endif /* nsGenericHTMLElement_h___ */
diff --git a/dom/html/nsGenericHTMLFrameElement.cpp b/dom/html/nsGenericHTMLFrameElement.cpp
new file mode 100644
index 0000000000..ae2c4dcce5
--- /dev/null
+++ b/dom/html/nsGenericHTMLFrameElement.cpp
@@ -0,0 +1,363 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsGenericHTMLFrameElement.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLIFrameElement.h"
+#include "mozilla/dom/XULFrameElement.h"
+#include "mozilla/dom/BrowserBridgeChild.h"
+#include "mozilla/dom/WindowProxyHolder.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/ErrorResult.h"
+#include "nsAttrValueInlines.h"
+#include "nsContentUtils.h"
+#include "nsIDocShell.h"
+#include "nsIFrame.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIPermissionManager.h"
+#include "nsPresContext.h"
+#include "nsServiceManagerUtils.h"
+#include "nsSubDocumentFrame.h"
+#include "nsAttrValueOrString.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(nsGenericHTMLFrameElement)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsGenericHTMLFrameElement,
+ nsGenericHTMLElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameLoader)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBrowserElementAPI)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsGenericHTMLFrameElement,
+ nsGenericHTMLElement)
+ if (tmp->mFrameLoader) {
+ tmp->mFrameLoader->Destroy();
+ }
+
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mFrameLoader)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBrowserElementAPI)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(
+ nsGenericHTMLFrameElement, nsGenericHTMLElement, nsFrameLoaderOwner,
+ nsIDOMMozBrowserFrame, nsIMozBrowserFrame, nsGenericHTMLFrameElement)
+
+NS_IMETHODIMP
+nsGenericHTMLFrameElement::GetMozbrowser(bool* aValue) {
+ *aValue = GetBoolAttr(nsGkAtoms::mozbrowser);
+ return NS_OK;
+}
+NS_IMETHODIMP
+nsGenericHTMLFrameElement::SetMozbrowser(bool aValue) {
+ return SetBoolAttr(nsGkAtoms::mozbrowser, aValue);
+}
+
+int32_t nsGenericHTMLFrameElement::TabIndexDefault() { return 0; }
+
+nsGenericHTMLFrameElement::~nsGenericHTMLFrameElement() {
+ if (mFrameLoader) {
+ mFrameLoader->Destroy();
+ }
+}
+
+Document* nsGenericHTMLFrameElement::GetContentDocument(
+ nsIPrincipal& aSubjectPrincipal) {
+ RefPtr<BrowsingContext> bc = GetContentWindowInternal();
+ if (!bc) {
+ return nullptr;
+ }
+
+ nsPIDOMWindowOuter* window = bc->GetDOMWindow();
+ if (!window) {
+ // Either our browsing context contents are out-of-process (in which case
+ // clearly this is a cross-origin call and we should return null), or our
+ // browsing context is torn-down enough to no longer have a window or a
+ // document, and we should still return null.
+ return nullptr;
+ }
+ Document* doc = window->GetDoc();
+ if (!doc) {
+ return nullptr;
+ }
+
+ // Return null for cross-origin contentDocument.
+ if (!aSubjectPrincipal.SubsumesConsideringDomain(doc->NodePrincipal())) {
+ return nullptr;
+ }
+ return doc;
+}
+
+BrowsingContext* nsGenericHTMLFrameElement::GetContentWindowInternal() {
+ EnsureFrameLoader();
+
+ if (!mFrameLoader) {
+ return nullptr;
+ }
+
+ if (mFrameLoader->DepthTooGreat()) {
+ // Claim to have no contentWindow
+ return nullptr;
+ }
+
+ RefPtr<BrowsingContext> bc = mFrameLoader->GetBrowsingContext();
+ return bc;
+}
+
+Nullable<WindowProxyHolder> nsGenericHTMLFrameElement::GetContentWindow() {
+ RefPtr<BrowsingContext> bc = GetContentWindowInternal();
+ if (!bc) {
+ return nullptr;
+ }
+ return WindowProxyHolder(bc);
+}
+
+void nsGenericHTMLFrameElement::EnsureFrameLoader() {
+ if (!IsInComposedDoc() || mFrameLoader || OwnerDoc()->IsStaticDocument()) {
+ // If frame loader is there, we just keep it around, cached
+ return;
+ }
+
+ // Strangely enough, this method doesn't actually ensure that the
+ // frameloader exists. It's more of a best-effort kind of thing.
+ mFrameLoader = nsFrameLoader::Create(this, mNetworkCreated);
+}
+
+void nsGenericHTMLFrameElement::SwapFrameLoaders(
+ HTMLIFrameElement& aOtherLoaderOwner, ErrorResult& rv) {
+ if (&aOtherLoaderOwner == this) {
+ // nothing to do
+ return;
+ }
+
+ aOtherLoaderOwner.SwapFrameLoaders(this, rv);
+}
+
+void nsGenericHTMLFrameElement::SwapFrameLoaders(
+ XULFrameElement& aOtherLoaderOwner, ErrorResult& rv) {
+ aOtherLoaderOwner.SwapFrameLoaders(this, rv);
+}
+
+void nsGenericHTMLFrameElement::SwapFrameLoaders(
+ nsFrameLoaderOwner* aOtherLoaderOwner, mozilla::ErrorResult& rv) {
+ if (RefPtr<Document> doc = GetComposedDoc()) {
+ // SwapWithOtherLoader relies on frames being up-to-date.
+ doc->FlushPendingNotifications(FlushType::Frames);
+ }
+
+ RefPtr<nsFrameLoader> loader = GetFrameLoader();
+ RefPtr<nsFrameLoader> otherLoader = aOtherLoaderOwner->GetFrameLoader();
+ if (!loader || !otherLoader) {
+ rv.Throw(NS_ERROR_NOT_IMPLEMENTED);
+ return;
+ }
+
+ rv = loader->SwapWithOtherLoader(otherLoader, this, aOtherLoaderOwner);
+}
+
+void nsGenericHTMLFrameElement::LoadSrc() {
+ // Waiting for lazy load, do nothing.
+ if (mLazyLoading) {
+ return;
+ }
+
+ EnsureFrameLoader();
+
+ if (!mFrameLoader) {
+ return;
+ }
+
+ bool origSrc = !mSrcLoadHappened;
+ mSrcLoadHappened = true;
+ mFrameLoader->LoadFrame(origSrc);
+}
+
+nsresult nsGenericHTMLFrameElement::BindToTree(BindContext& aContext,
+ nsINode& aParent) {
+ nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsInComposedDoc()) {
+ NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(),
+ "Missing a script blocker!");
+
+ AUTO_PROFILER_LABEL("nsGenericHTMLFrameElement::BindToTree", OTHER);
+
+ // We're in a document now. Kick off the frame load.
+ LoadSrc();
+ }
+
+ // We're now in document and scripts may move us, so clear
+ // the mNetworkCreated flag.
+ mNetworkCreated = false;
+ return rv;
+}
+
+void nsGenericHTMLFrameElement::UnbindFromTree(bool aNullParent) {
+ if (mFrameLoader) {
+ // This iframe is being taken out of the document, destroy the
+ // iframe's frame loader (doing that will tear down the window in
+ // this iframe).
+ // XXXbz we really want to only partially destroy the frame
+ // loader... we don't want to tear down the docshell. Food for
+ // later bug.
+ mFrameLoader->Destroy();
+ mFrameLoader = nullptr;
+ }
+
+ nsGenericHTMLElement::UnbindFromTree(aNullParent);
+}
+
+/* static */
+ScrollbarPreference nsGenericHTMLFrameElement::MapScrollingAttribute(
+ const nsAttrValue* aValue) {
+ if (aValue && aValue->Type() == nsAttrValue::eEnum) {
+ auto scrolling = static_cast<ScrollingAttribute>(aValue->GetEnumValue());
+ if (scrolling == ScrollingAttribute::Off ||
+ scrolling == ScrollingAttribute::Noscroll ||
+ scrolling == ScrollingAttribute::No) {
+ return ScrollbarPreference::Never;
+ }
+ }
+ return ScrollbarPreference::Auto;
+}
+
+/* virtual */
+void nsGenericHTMLFrameElement::AfterSetAttr(
+ int32_t aNameSpaceID, nsAtom* aName, const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify) {
+ if (aValue) {
+ nsAttrValueOrString value(aValue);
+ AfterMaybeChangeAttr(aNameSpaceID, aName, &value, aMaybeScriptedPrincipal,
+ aNotify);
+ } else {
+ AfterMaybeChangeAttr(aNameSpaceID, aName, nullptr, aMaybeScriptedPrincipal,
+ aNotify);
+ }
+
+ if (aNameSpaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::scrolling) {
+ if (mFrameLoader) {
+ ScrollbarPreference pref = MapScrollingAttribute(aValue);
+ if (nsDocShell* docshell = mFrameLoader->GetExistingDocShell()) {
+ docshell->SetScrollbarPreference(pref);
+ } else if (auto* child = mFrameLoader->GetBrowserBridgeChild()) {
+ // NOTE(emilio): We intentionally don't deal with the
+ // GetBrowserParent() case, and only deal with the fission iframe
+ // case. We could make it work, but it's a bit of boilerplate for
+ // something that we don't use, and we'd need to think how it
+ // interacts with the scrollbar window flags...
+ child->SendScrollbarPreferenceChanged(pref);
+ }
+ }
+ } else if (aName == nsGkAtoms::mozbrowser) {
+ mReallyIsBrowser = !!aValue && XRE_IsParentProcess() &&
+ NodePrincipal()->IsSystemPrincipal();
+ }
+ }
+
+ return nsGenericHTMLElement::AfterSetAttr(
+ aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+void nsGenericHTMLFrameElement::OnAttrSetButNotChanged(
+ int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue,
+ bool aNotify) {
+ AfterMaybeChangeAttr(aNamespaceID, aName, &aValue, nullptr, aNotify);
+
+ return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName,
+ aValue, aNotify);
+}
+
+void nsGenericHTMLFrameElement::AfterMaybeChangeAttr(
+ int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString* aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal, bool aNotify) {
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aName == nsGkAtoms::src) {
+ mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal(
+ this, aValue ? aValue->String() : u""_ns, aMaybeScriptedPrincipal);
+ if (!IsHTMLElement(nsGkAtoms::iframe) || !HasAttr(nsGkAtoms::srcdoc)) {
+ // Don't propagate error here. The attribute was successfully
+ // set or removed; that's what we should reflect.
+ LoadSrc();
+ }
+ } else if (aName == nsGkAtoms::name) {
+ // Propagate "name" to the browsing context per HTML5.
+ RefPtr<BrowsingContext> bc =
+ mFrameLoader ? mFrameLoader->GetExtantBrowsingContext() : nullptr;
+ if (bc) {
+ MOZ_ALWAYS_SUCCEEDS(bc->SetName(aValue ? aValue->String() : u""_ns));
+ }
+ }
+ }
+}
+
+void nsGenericHTMLFrameElement::DestroyContent() {
+ if (mFrameLoader) {
+ mFrameLoader->Destroy();
+ mFrameLoader = nullptr;
+ }
+
+ nsGenericHTMLElement::DestroyContent();
+}
+
+nsresult nsGenericHTMLFrameElement::CopyInnerTo(Element* aDest) {
+ nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ Document* doc = aDest->OwnerDoc();
+ if (doc->IsStaticDocument() && mFrameLoader) {
+ nsGenericHTMLFrameElement* dest =
+ static_cast<nsGenericHTMLFrameElement*>(aDest);
+ doc->AddPendingFrameStaticClone(dest, mFrameLoader);
+ }
+
+ return rv;
+}
+
+bool nsGenericHTMLFrameElement::IsHTMLFocusable(bool aWithMouse,
+ bool* aIsFocusable,
+ int32_t* aTabIndex) {
+ if (nsGenericHTMLElement::IsHTMLFocusable(aWithMouse, aIsFocusable,
+ aTabIndex)) {
+ return true;
+ }
+
+ *aIsFocusable = true;
+ return false;
+}
+
+/**
+ * Return true if this frame element really is a mozbrowser. (It
+ * needs to have the right attributes, and its creator must have the right
+ * permissions.)
+ */
+/* [infallible] */
+nsresult nsGenericHTMLFrameElement::GetReallyIsBrowser(bool* aOut) {
+ *aOut = mReallyIsBrowser;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsGenericHTMLFrameElement::InitializeBrowserAPI() {
+ MOZ_ASSERT(mFrameLoader);
+ InitBrowserElementAPI();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsGenericHTMLFrameElement::DestroyBrowserFrameScripts() {
+ MOZ_ASSERT(mFrameLoader);
+ DestroyBrowserElementFrameScripts();
+ return NS_OK;
+}
diff --git a/dom/html/nsGenericHTMLFrameElement.h b/dom/html/nsGenericHTMLFrameElement.h
new file mode 100644
index 0000000000..4ac6401721
--- /dev/null
+++ b/dom/html/nsGenericHTMLFrameElement.h
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsGenericHTMLFrameElement_h
+#define nsGenericHTMLFrameElement_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/nsBrowserElement.h"
+
+#include "nsFrameLoader.h"
+#include "nsFrameLoaderOwner.h"
+#include "nsGenericHTMLElement.h"
+#include "nsIMozBrowserFrame.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class BrowserParent;
+template <typename>
+struct Nullable;
+class WindowProxyHolder;
+class XULFrameElement;
+} // namespace dom
+} // namespace mozilla
+
+#define NS_GENERICHTMLFRAMEELEMENT_IID \
+ { \
+ 0x8190db72, 0xdab0, 0x4d72, { \
+ 0x94, 0x26, 0x87, 0x5f, 0x5a, 0x8a, 0x2a, 0xe5 \
+ } \
+ }
+
+/**
+ * A helper class for frame elements
+ */
+class nsGenericHTMLFrameElement : public nsGenericHTMLElement,
+ public nsFrameLoaderOwner,
+ public mozilla::nsBrowserElement,
+ public nsIMozBrowserFrame {
+ public:
+ nsGenericHTMLFrameElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ mozilla::dom::FromParser aFromParser)
+ : nsGenericHTMLElement(std::move(aNodeInfo)),
+ mSrcLoadHappened(false),
+ mNetworkCreated(aFromParser == mozilla::dom::FROM_PARSER_NETWORK),
+ mBrowserFrameListenersRegistered(false),
+ mReallyIsBrowser(false) {}
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_NSIDOMMOZBROWSERFRAME
+ NS_DECL_NSIMOZBROWSERFRAME
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_GENERICHTMLFRAMEELEMENT_IID)
+
+ // nsIContent
+ virtual bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
+ int32_t* aTabIndex) override;
+ virtual nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ virtual void UnbindFromTree(bool aNullParent = true) override;
+ virtual void DestroyContent() override;
+
+ nsresult CopyInnerTo(mozilla::dom::Element* aDest);
+
+ virtual int32_t TabIndexDefault() override;
+
+ virtual nsIMozBrowserFrame* GetAsMozBrowserFrame() override { return this; }
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsGenericHTMLFrameElement,
+ nsGenericHTMLElement)
+
+ void SwapFrameLoaders(mozilla::dom::HTMLIFrameElement& aOtherLoaderOwner,
+ mozilla::ErrorResult& aError);
+
+ void SwapFrameLoaders(mozilla::dom::XULFrameElement& aOtherLoaderOwner,
+ mozilla::ErrorResult& aError);
+
+ void SwapFrameLoaders(nsFrameLoaderOwner* aOtherLoaderOwner,
+ mozilla::ErrorResult& rv);
+
+ /**
+ * Helper method to map a HTML 'scrolling' attribute value (which can be null)
+ * to a ScrollbarPreference value value. scrolling="no" (and its synonyms)
+ * map to Never, and anything else to Auto.
+ */
+ static mozilla::ScrollbarPreference MapScrollingAttribute(const nsAttrValue*);
+
+ nsIPrincipal* GetSrcTriggeringPrincipal() const {
+ return mSrcTriggeringPrincipal;
+ }
+
+ // Needed for nsBrowserElement
+ already_AddRefed<nsFrameLoader> GetFrameLoader() override {
+ return nsFrameLoaderOwner::GetFrameLoader();
+ }
+
+ protected:
+ virtual ~nsGenericHTMLFrameElement();
+
+ // This doesn't really ensure a frame loader in all cases, only when
+ // it makes sense.
+ void EnsureFrameLoader();
+ void LoadSrc();
+ Document* GetContentDocument(nsIPrincipal& aSubjectPrincipal);
+ mozilla::dom::Nullable<mozilla::dom::WindowProxyHolder> GetContentWindow();
+
+ virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+ const nsAttrValue* aValue,
+ const nsAttrValue* aOldValue,
+ nsIPrincipal* aSubjectPrincipal,
+ bool aNotify) override;
+ virtual void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValueOrString& aValue,
+ bool aNotify) override;
+
+ nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal;
+
+ /**
+ * True if we have already loaded the frame's original src
+ */
+ bool mSrcLoadHappened;
+
+ /**
+ * True when the element is created by the parser using the
+ * NS_FROM_PARSER_NETWORK flag.
+ * If the element is modified, it may lose the flag.
+ */
+ bool mNetworkCreated;
+
+ bool mBrowserFrameListenersRegistered;
+ bool mReallyIsBrowser;
+
+ // This flag is only used by <iframe>. See HTMLIFrameElement::
+ // FullscreenFlag() for details. It is placed here so that we
+ // do not bloat any struct.
+ bool mFullscreenFlag = false;
+
+ /**
+ * Represents the iframe is deferred loading until this element gets visible.
+ * We just do not load if set and leave specific elements to set it (see
+ * HTMLIFrameElement).
+ */
+ bool mLazyLoading = false;
+
+ private:
+ void GetManifestURL(nsAString& aOut);
+
+ /**
+ * This function is called by AfterSetAttr and OnAttrSetButNotChanged.
+ * It will be called whether the value is being set or unset.
+ *
+ * @param aNamespaceID the namespace of the attr being set
+ * @param aName the localname of the attribute being set
+ * @param aValue the value being set or null if the value is being unset
+ * @param aNotify Whether we plan to notify document observers.
+ */
+ void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName,
+ const nsAttrValueOrString* aValue,
+ nsIPrincipal* aMaybeScriptedPrincipal,
+ bool aNotify);
+
+ mozilla::dom::BrowsingContext* GetContentWindowInternal();
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsGenericHTMLFrameElement,
+ NS_GENERICHTMLFRAMEELEMENT_IID)
+
+#endif // nsGenericHTMLFrameElement_h
diff --git a/dom/html/nsHTMLContentSink.cpp b/dom/html/nsHTMLContentSink.cpp
new file mode 100644
index 0000000000..0c22b3e9aa
--- /dev/null
+++ b/dom/html/nsHTMLContentSink.cpp
@@ -0,0 +1,937 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file is near-OBSOLETE. It is used for about:blank only and for the
+ * HTML element factory.
+ * Don't bother adding new stuff in this file.
+ */
+
+#include "mozilla/ArrayUtils.h"
+
+#include "nsContentSink.h"
+#include "nsCOMPtr.h"
+#include "nsHTMLTags.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsIHTMLContentSink.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIURI.h"
+#include "mozilla/dom/NodeInfo.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "nsCRT.h"
+#include "prtime.h"
+#include "mozilla/Logging.h"
+#include "nsIContent.h"
+#include "mozilla/dom/CustomElementRegistry.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/MutationObservers.h"
+#include "mozilla/Preferences.h"
+
+#include "nsGenericHTMLElement.h"
+
+#include "nsIScriptElement.h"
+
+#include "nsDocElementCreatedNotificationRunner.h"
+#include "nsGkAtoms.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "mozilla/dom/Document.h"
+#include "nsStubDocumentObserver.h"
+#include "nsHTMLDocument.h"
+#include "nsTArray.h"
+#include "nsTextFragment.h"
+#include "nsIScriptGlobalObject.h"
+#include "nsNameSpaceManager.h"
+
+#include "nsError.h"
+#include "nsContentPolicyUtils.h"
+#include "nsIDocShell.h"
+#include "nsIScriptContext.h"
+
+#include "nsLayoutCID.h"
+
+#include "nsEscape.h"
+#include "nsNodeInfoManager.h"
+#include "nsContentCreatorFunctions.h"
+#include "mozAutoDocUpdate.h"
+#include "nsTextNode.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+//----------------------------------------------------------------------
+
+nsGenericHTMLElement* NS_NewHTMLNOTUSEDElement(
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser) {
+ MOZ_ASSERT_UNREACHABLE("The element ctor should never be called");
+ return nullptr;
+}
+
+#define HTML_TAG(_tag, _classname, _interfacename) \
+ NS_NewHTML##_classname##Element,
+#define HTML_OTHER(_tag) NS_NewHTMLNOTUSEDElement,
+static const HTMLContentCreatorFunction sHTMLContentCreatorFunctions[] = {
+ NS_NewHTMLUnknownElement,
+#include "nsHTMLTagList.h"
+#undef HTML_TAG
+#undef HTML_OTHER
+ NS_NewHTMLUnknownElement};
+
+class SinkContext;
+class HTMLContentSink;
+
+/**
+ * This class is near-OBSOLETE. It is used for about:blank only.
+ * Don't bother adding new stuff in this file.
+ */
+class HTMLContentSink : public nsContentSink, public nsIHTMLContentSink {
+ public:
+ friend class SinkContext;
+
+ HTMLContentSink();
+
+ nsresult Init(Document* aDoc, nsIURI* aURI, nsISupports* aContainer,
+ nsIChannel* aChannel);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLContentSink, nsContentSink)
+
+ // nsIContentSink
+ NS_IMETHOD WillParse(void) override;
+ NS_IMETHOD WillBuildModel(nsDTDMode aDTDMode) override;
+ NS_IMETHOD DidBuildModel(bool aTerminated) override;
+ NS_IMETHOD WillInterrupt(void) override;
+ void WillResume() override;
+ NS_IMETHOD SetParser(nsParserBase* aParser) override;
+ virtual void FlushPendingNotifications(FlushType aType) override;
+ virtual void SetDocumentCharset(NotNull<const Encoding*> aEncoding) override;
+ virtual nsISupports* GetTarget() override;
+ virtual bool IsScriptExecuting() override;
+ virtual bool WaitForPendingSheets() override;
+ virtual void ContinueInterruptedParsingAsync() override;
+
+ // nsIHTMLContentSink
+ NS_IMETHOD OpenContainer(ElementType aNodeType) override;
+ NS_IMETHOD CloseContainer(ElementType aTag) override;
+
+ protected:
+ virtual ~HTMLContentSink();
+
+ RefPtr<nsHTMLDocument> mHTMLDocument;
+
+ // The maximum length of a text run
+ int32_t mMaxTextRun;
+
+ RefPtr<nsGenericHTMLElement> mRoot;
+ RefPtr<nsGenericHTMLElement> mBody;
+ RefPtr<nsGenericHTMLElement> mHead;
+
+ AutoTArray<SinkContext*, 8> mContextStack;
+ SinkContext* mCurrentContext;
+ SinkContext* mHeadContext;
+
+ // Boolean indicating whether we've seen a <head> tag that might have had
+ // attributes once already.
+ bool mHaveSeenHead;
+
+ // Boolean indicating whether we've notified insertion of our root content
+ // yet. We want to make sure to only do this once.
+ bool mNotifiedRootInsertion;
+
+ nsresult FlushTags() override;
+
+ // Routines for tags that require special handling
+ nsresult CloseHTML();
+ nsresult OpenBody();
+ nsresult CloseBody();
+
+ void CloseHeadContext();
+
+ // nsContentSink overrides
+ void UpdateChildCounts() override;
+
+ void NotifyInsert(nsIContent* aContent, nsIContent* aChildContent);
+ void NotifyRootInsertion();
+
+ private:
+ void ContinueInterruptedParsingIfEnabled();
+};
+
+class SinkContext {
+ public:
+ explicit SinkContext(HTMLContentSink* aSink);
+ ~SinkContext();
+
+ nsresult Begin(nsHTMLTag aNodeType, nsGenericHTMLElement* aRoot,
+ uint32_t aNumFlushed, int32_t aInsertionPoint);
+ nsresult OpenBody();
+ nsresult CloseBody();
+ nsresult End();
+
+ nsresult GrowStack();
+ nsresult FlushTags();
+
+ bool IsCurrentContainer(nsHTMLTag aTag) const;
+
+ void DidAddContent(nsIContent* aContent);
+ void UpdateChildCounts();
+
+ private:
+ // Function to check whether we've notified for the current content.
+ // What this actually does is check whether we've notified for all
+ // of the parent's kids.
+ bool HaveNotifiedForCurrentContent() const;
+
+ public:
+ HTMLContentSink* mSink;
+ int32_t mNotifyLevel;
+
+ struct Node {
+ nsHTMLTag mType;
+ nsGenericHTMLElement* mContent;
+ uint32_t mNumFlushed;
+ int32_t mInsertionPoint;
+
+ nsIContent* Add(nsIContent* child);
+ };
+
+ Node* mStack;
+ int32_t mStackSize;
+ int32_t mStackPos;
+};
+
+nsresult NS_NewHTMLElement(Element** aResult,
+ already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser, nsAtom* aIsAtom,
+ mozilla::dom::CustomElementDefinition* aDefinition) {
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo = aNodeInfo;
+
+ NS_ASSERTION(
+ nodeInfo->NamespaceEquals(kNameSpaceID_XHTML),
+ "Trying to create HTML elements that don't have the XHTML namespace");
+
+ return nsContentUtils::NewXULOrHTMLElement(aResult, nodeInfo, aFromParser,
+ aIsAtom, aDefinition);
+}
+
+already_AddRefed<nsGenericHTMLElement> CreateHTMLElement(
+ uint32_t aNodeType, already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
+ FromParser aFromParser) {
+ NS_ASSERTION(
+ aNodeType <= NS_HTML_TAG_MAX || aNodeType == eHTMLTag_userdefined,
+ "aNodeType is out of bounds");
+
+ HTMLContentCreatorFunction cb = sHTMLContentCreatorFunctions[aNodeType];
+
+ NS_ASSERTION(cb != NS_NewHTMLNOTUSEDElement,
+ "Don't know how to construct tag element!");
+
+ RefPtr<nsGenericHTMLElement> result = cb(std::move(aNodeInfo), aFromParser);
+
+ return result.forget();
+}
+
+//----------------------------------------------------------------------
+
+SinkContext::SinkContext(HTMLContentSink* aSink)
+ : mSink(aSink),
+ mNotifyLevel(0),
+ mStack(nullptr),
+ mStackSize(0),
+ mStackPos(0) {
+ MOZ_COUNT_CTOR(SinkContext);
+}
+
+SinkContext::~SinkContext() {
+ MOZ_COUNT_DTOR(SinkContext);
+
+ if (mStack) {
+ for (int32_t i = 0; i < mStackPos; i++) {
+ NS_RELEASE(mStack[i].mContent);
+ }
+ delete[] mStack;
+ }
+}
+
+nsresult SinkContext::Begin(nsHTMLTag aNodeType, nsGenericHTMLElement* aRoot,
+ uint32_t aNumFlushed, int32_t aInsertionPoint) {
+ if (mStackSize < 1) {
+ nsresult rv = GrowStack();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ mStack[0].mType = aNodeType;
+ mStack[0].mContent = aRoot;
+ mStack[0].mNumFlushed = aNumFlushed;
+ mStack[0].mInsertionPoint = aInsertionPoint;
+ NS_ADDREF(aRoot);
+ mStackPos = 1;
+
+ return NS_OK;
+}
+
+bool SinkContext::IsCurrentContainer(nsHTMLTag aTag) const {
+ return aTag == mStack[mStackPos - 1].mType;
+}
+
+void SinkContext::DidAddContent(nsIContent* aContent) {
+ if ((mStackPos == 2) && (mSink->mBody == mStack[1].mContent)) {
+ // We just finished adding something to the body
+ mNotifyLevel = 0;
+ }
+
+ // If we just added content to a node for which
+ // an insertion happen, we need to do an immediate
+ // notification for that insertion.
+ if (0 < mStackPos && mStack[mStackPos - 1].mInsertionPoint != -1 &&
+ mStack[mStackPos - 1].mNumFlushed <
+ mStack[mStackPos - 1].mContent->GetChildCount()) {
+ nsIContent* parent = mStack[mStackPos - 1].mContent;
+ mSink->NotifyInsert(parent, aContent);
+ mStack[mStackPos - 1].mNumFlushed = parent->GetChildCount();
+ } else if (mSink->IsTimeToNotify()) {
+ FlushTags();
+ }
+}
+
+nsresult SinkContext::OpenBody() {
+ if (mStackPos <= 0) {
+ NS_ERROR("container w/o parent");
+
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv;
+ if (mStackPos + 1 > mStackSize) {
+ rv = GrowStack();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo =
+ mSink->mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::body, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+ NS_ENSURE_TRUE(nodeInfo, NS_ERROR_UNEXPECTED);
+
+ // Make the content object
+ RefPtr<nsGenericHTMLElement> body =
+ NS_NewHTMLBodyElement(nodeInfo.forget(), FROM_PARSER_NETWORK);
+ if (!body) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ mStack[mStackPos].mType = eHTMLTag_body;
+ body.forget(&mStack[mStackPos].mContent);
+ mStack[mStackPos].mNumFlushed = 0;
+ mStack[mStackPos].mInsertionPoint = -1;
+ ++mStackPos;
+ mStack[mStackPos - 2].Add(mStack[mStackPos - 1].mContent);
+
+ return NS_OK;
+}
+
+bool SinkContext::HaveNotifiedForCurrentContent() const {
+ if (0 < mStackPos) {
+ nsIContent* parent = mStack[mStackPos - 1].mContent;
+ return mStack[mStackPos - 1].mNumFlushed == parent->GetChildCount();
+ }
+
+ return true;
+}
+
+nsIContent* SinkContext::Node::Add(nsIContent* child) {
+ NS_ASSERTION(mContent, "No parent to insert/append into!");
+ if (mInsertionPoint != -1) {
+ NS_ASSERTION(mNumFlushed == mContent->GetChildCount(),
+ "Inserting multiple children without flushing.");
+ nsCOMPtr<nsIContent> nodeToInsertBefore =
+ mContent->GetChildAt_Deprecated(mInsertionPoint++);
+ mContent->InsertChildBefore(child, nodeToInsertBefore, false,
+ IgnoreErrors());
+ } else {
+ mContent->AppendChildTo(child, false, IgnoreErrors());
+ }
+ return child;
+}
+
+nsresult SinkContext::CloseBody() {
+ NS_ASSERTION(mStackPos > 0, "stack out of bounds. wrong context probably!");
+
+ if (mStackPos <= 0) {
+ return NS_OK; // Fix crash - Ref. bug 45975 or 45007
+ }
+
+ --mStackPos;
+ NS_ASSERTION(mStack[mStackPos].mType == eHTMLTag_body,
+ "Tag mismatch. Closing tag on wrong context or something?");
+
+ nsGenericHTMLElement* content = mStack[mStackPos].mContent;
+
+ content->Compact();
+
+ // If we're in a state where we do append notifications as
+ // we go up the tree, and we're at the level where the next
+ // notification needs to be done, do the notification.
+ if (mNotifyLevel >= mStackPos) {
+ // Check to see if new content has been added after our last
+ // notification
+
+ if (mStack[mStackPos].mNumFlushed < content->GetChildCount()) {
+ mSink->NotifyAppend(content, mStack[mStackPos].mNumFlushed);
+ mStack[mStackPos].mNumFlushed = content->GetChildCount();
+ }
+
+ // Indicate that notification has now happened at this level
+ mNotifyLevel = mStackPos - 1;
+ }
+
+ DidAddContent(content);
+ NS_IF_RELEASE(content);
+
+ return NS_OK;
+}
+
+nsresult SinkContext::End() {
+ for (int32_t i = 0; i < mStackPos; i++) {
+ NS_RELEASE(mStack[i].mContent);
+ }
+
+ mStackPos = 0;
+
+ return NS_OK;
+}
+
+nsresult SinkContext::GrowStack() {
+ int32_t newSize = mStackSize * 2;
+ if (newSize == 0) {
+ newSize = 32;
+ }
+
+ Node* stack = new Node[newSize];
+
+ if (mStackPos != 0) {
+ memcpy(stack, mStack, sizeof(Node) * mStackPos);
+ delete[] mStack;
+ }
+
+ mStack = stack;
+ mStackSize = newSize;
+
+ return NS_OK;
+}
+
+/**
+ * NOTE!! Forked into nsXMLContentSink. Please keep in sync.
+ *
+ * Flush all elements that have been seen so far such that
+ * they are visible in the tree. Specifically, make sure
+ * that they are all added to their respective parents.
+ * Also, do notification at the top for all content that
+ * has been newly added so that the frame tree is complete.
+ */
+nsresult SinkContext::FlushTags() {
+ mSink->mDeferredFlushTags = false;
+ uint32_t oldUpdates = mSink->mUpdatesInNotification;
+
+ ++(mSink->mInNotification);
+ mSink->mUpdatesInNotification = 0;
+ {
+ // Scope so we call EndUpdate before we decrease mInNotification
+ mozAutoDocUpdate updateBatch(mSink->mDocument, true);
+
+ // Start from the base of the stack (growing downward) and do
+ // a notification from the node that is closest to the root of
+ // tree for any content that has been added.
+
+ // Note that we can start at stackPos == 0 here, because it's the caller's
+ // responsibility to handle flushing interactions between contexts (see
+ // HTMLContentSink::BeginContext).
+ int32_t stackPos = 0;
+ bool flushed = false;
+ uint32_t childCount;
+ nsGenericHTMLElement* content;
+
+ while (stackPos < mStackPos) {
+ content = mStack[stackPos].mContent;
+ childCount = content->GetChildCount();
+
+ if (!flushed && (mStack[stackPos].mNumFlushed < childCount)) {
+ if (mStack[stackPos].mInsertionPoint != -1) {
+ // We might have popped the child off our stack already
+ // but not notified on it yet, which is why we have to get it
+ // directly from its parent node.
+
+ int32_t childIndex = mStack[stackPos].mInsertionPoint - 1;
+ nsIContent* child = content->GetChildAt_Deprecated(childIndex);
+ // Child not on stack anymore; can't assert it's correct
+ NS_ASSERTION(!(mStackPos > (stackPos + 1)) ||
+ (child == mStack[stackPos + 1].mContent),
+ "Flushing the wrong child.");
+ mSink->NotifyInsert(content, child);
+ } else {
+ mSink->NotifyAppend(content, mStack[stackPos].mNumFlushed);
+ }
+
+ flushed = true;
+ }
+
+ mStack[stackPos].mNumFlushed = childCount;
+ stackPos++;
+ }
+ mNotifyLevel = mStackPos - 1;
+ }
+ --(mSink->mInNotification);
+
+ if (mSink->mUpdatesInNotification > 1) {
+ UpdateChildCounts();
+ }
+
+ mSink->mUpdatesInNotification = oldUpdates;
+
+ return NS_OK;
+}
+
+/**
+ * NOTE!! Forked into nsXMLContentSink. Please keep in sync.
+ */
+void SinkContext::UpdateChildCounts() {
+ // Start from the top of the stack (growing upwards) and see if any
+ // new content has been appended. If so, we recognize that reflows
+ // have been generated for it and we should make sure that no
+ // further reflows occur. Note that we have to include stackPos == 0
+ // to properly notify on kids of <html>.
+ int32_t stackPos = mStackPos - 1;
+ while (stackPos >= 0) {
+ Node& node = mStack[stackPos];
+ node.mNumFlushed = node.mContent->GetChildCount();
+
+ stackPos--;
+ }
+
+ mNotifyLevel = mStackPos - 1;
+}
+
+nsresult NS_NewHTMLContentSink(nsIHTMLContentSink** aResult, Document* aDoc,
+ nsIURI* aURI, nsISupports* aContainer,
+ nsIChannel* aChannel) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ RefPtr<HTMLContentSink> it = new HTMLContentSink();
+
+ nsresult rv = it->Init(aDoc, aURI, aContainer, aChannel);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aResult = it;
+ NS_ADDREF(*aResult);
+
+ return NS_OK;
+}
+
+HTMLContentSink::HTMLContentSink()
+ : mMaxTextRun(0),
+ mCurrentContext(nullptr),
+ mHeadContext(nullptr),
+ mHaveSeenHead(false),
+ mNotifiedRootInsertion(false) {}
+
+HTMLContentSink::~HTMLContentSink() {
+ if (mNotificationTimer) {
+ mNotificationTimer->Cancel();
+ }
+
+ if (mCurrentContext == mHeadContext && !mContextStack.IsEmpty()) {
+ // Pop off the second html context if it's not done earlier
+ mContextStack.RemoveLastElement();
+ }
+
+ for (int32_t i = 0, numContexts = mContextStack.Length(); i < numContexts;
+ i++) {
+ SinkContext* sc = mContextStack.ElementAt(i);
+ if (sc) {
+ sc->End();
+ if (sc == mCurrentContext) {
+ mCurrentContext = nullptr;
+ }
+
+ delete sc;
+ }
+ }
+
+ if (mCurrentContext == mHeadContext) {
+ mCurrentContext = nullptr;
+ }
+
+ delete mCurrentContext;
+
+ delete mHeadContext;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLContentSink, nsContentSink,
+ mHTMLDocument, mRoot, mBody, mHead)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLContentSink, nsContentSink,
+ nsIContentSink, nsIHTMLContentSink)
+
+nsresult HTMLContentSink::Init(Document* aDoc, nsIURI* aURI,
+ nsISupports* aContainer, nsIChannel* aChannel) {
+ NS_ENSURE_TRUE(aContainer, NS_ERROR_NULL_POINTER);
+
+ nsresult rv = nsContentSink::Init(aDoc, aURI, aContainer, aChannel);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ aDoc->AddObserver(this);
+ mIsDocumentObserver = true;
+ mHTMLDocument = aDoc->AsHTMLDocument();
+
+ NS_ASSERTION(mDocShell, "oops no docshell!");
+
+ // Changed from 8192 to greatly improve page loading performance on
+ // large pages. See bugzilla bug 77540.
+ mMaxTextRun = Preferences::GetInt("content.maxtextrun", 8191);
+
+ RefPtr<mozilla::dom::NodeInfo> nodeInfo;
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::html, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ // Make root part
+ mRoot = NS_NewHTMLHtmlElement(nodeInfo.forget());
+ if (!mRoot) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ NS_ASSERTION(mDocument->GetChildCount() == 0,
+ "Document should have no kids here!");
+ ErrorResult error;
+ mDocument->AppendChildTo(mRoot, false, error);
+ if (error.Failed()) {
+ return error.StealNSResult();
+ }
+
+ // Make head part
+ nodeInfo = mNodeInfoManager->GetNodeInfo(
+ nsGkAtoms::head, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE);
+
+ mHead = NS_NewHTMLHeadElement(nodeInfo.forget());
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ mRoot->AppendChildTo(mHead, false, IgnoreErrors());
+
+ mCurrentContext = new SinkContext(this);
+ mCurrentContext->Begin(eHTMLTag_html, mRoot, 0, -1);
+ mContextStack.AppendElement(mCurrentContext);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLContentSink::WillParse(void) { return WillParseImpl(); }
+
+NS_IMETHODIMP
+HTMLContentSink::WillBuildModel(nsDTDMode aDTDMode) {
+ WillBuildModelImpl();
+
+ mDocument->SetCompatibilityMode(aDTDMode == eDTDMode_full_standards
+ ? eCompatibility_FullStandards
+ : eCompatibility_NavQuirks);
+
+ // Notify document that the load is beginning
+ mDocument->BeginLoad();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLContentSink::DidBuildModel(bool aTerminated) {
+ DidBuildModelImpl(aTerminated);
+
+ // Reflow the last batch of content
+ if (mBody) {
+ mCurrentContext->FlushTags();
+ } else if (!mLayoutStarted) {
+ // We never saw the body, and layout never got started. Force
+ // layout *now*, to get an initial reflow.
+ // NOTE: only force the layout if we are NOT destroying the
+ // docshell. If we are destroying it, then starting layout will
+ // likely cause us to crash, or at best waste a lot of time as we
+ // are just going to tear it down anyway.
+ bool bDestroying = true;
+ if (mDocShell) {
+ mDocShell->IsBeingDestroyed(&bDestroying);
+ }
+
+ if (!bDestroying) {
+ StartLayout(false);
+ }
+ }
+
+ ScrollToRef();
+
+ // Make sure we no longer respond to document mutations. We've flushed all
+ // our notifications out, so there's no need to do anything else here.
+
+ // XXXbz I wonder whether we could End() our contexts here too, or something,
+ // just to make sure we no longer notify... Or is the mIsDocumentObserver
+ // thing sufficient?
+ mDocument->RemoveObserver(this);
+ mIsDocumentObserver = false;
+
+ mDocument->EndLoad();
+
+ DropParserAndPerfHint();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLContentSink::SetParser(nsParserBase* aParser) {
+ MOZ_ASSERT(aParser, "Should have a parser here!");
+ mParser = aParser;
+ return NS_OK;
+}
+
+nsresult HTMLContentSink::CloseHTML() {
+ if (mHeadContext) {
+ if (mCurrentContext == mHeadContext) {
+ // Pop off the second html context if it's not done earlier
+ mCurrentContext = mContextStack.PopLastElement();
+ }
+
+ mHeadContext->End();
+
+ delete mHeadContext;
+ mHeadContext = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsresult HTMLContentSink::OpenBody() {
+ CloseHeadContext(); // do this just in case if the HEAD was left open!
+
+ // if we already have a body we're done
+ if (mBody) {
+ return NS_OK;
+ }
+
+ nsresult rv = mCurrentContext->OpenBody();
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mBody = mCurrentContext->mStack[mCurrentContext->mStackPos - 1].mContent;
+
+ if (mCurrentContext->mStackPos > 1) {
+ int32_t parentIndex = mCurrentContext->mStackPos - 2;
+ nsGenericHTMLElement* parent =
+ mCurrentContext->mStack[parentIndex].mContent;
+ int32_t numFlushed = mCurrentContext->mStack[parentIndex].mNumFlushed;
+ int32_t childCount = parent->GetChildCount();
+ NS_ASSERTION(numFlushed < childCount, "Already notified on the body?");
+
+ int32_t insertionPoint =
+ mCurrentContext->mStack[parentIndex].mInsertionPoint;
+
+ // XXX: I have yet to see a case where numFlushed is non-zero and
+ // insertionPoint is not -1, but this code will try to handle
+ // those cases too.
+
+ uint32_t oldUpdates = mUpdatesInNotification;
+ mUpdatesInNotification = 0;
+ if (insertionPoint != -1) {
+ NotifyInsert(parent, mBody);
+ } else {
+ NotifyAppend(parent, numFlushed);
+ }
+ mCurrentContext->mStack[parentIndex].mNumFlushed = childCount;
+ if (mUpdatesInNotification > 1) {
+ UpdateChildCounts();
+ }
+ mUpdatesInNotification = oldUpdates;
+ }
+
+ StartLayout(false);
+
+ return NS_OK;
+}
+
+nsresult HTMLContentSink::CloseBody() {
+ // Flush out anything that's left
+ mCurrentContext->FlushTags();
+ mCurrentContext->CloseBody();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HTMLContentSink::OpenContainer(ElementType aElementType) {
+ nsresult rv = NS_OK;
+
+ switch (aElementType) {
+ case eBody:
+ rv = OpenBody();
+ break;
+ case eHTML:
+ if (mRoot) {
+ // If we've already hit this code once, then we're done
+ if (!mNotifiedRootInsertion) {
+ NotifyRootInsertion();
+ }
+ }
+ break;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLContentSink::CloseContainer(const ElementType aTag) {
+ nsresult rv = NS_OK;
+
+ switch (aTag) {
+ case eBody:
+ rv = CloseBody();
+ break;
+ case eHTML:
+ rv = CloseHTML();
+ break;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+HTMLContentSink::WillInterrupt() { return WillInterruptImpl(); }
+
+void HTMLContentSink::WillResume() { WillResumeImpl(); }
+
+void HTMLContentSink::CloseHeadContext() {
+ if (mCurrentContext) {
+ if (!mCurrentContext->IsCurrentContainer(eHTMLTag_head)) return;
+
+ mCurrentContext->FlushTags();
+ }
+
+ if (!mContextStack.IsEmpty()) {
+ mCurrentContext = mContextStack.PopLastElement();
+ }
+}
+
+void HTMLContentSink::NotifyInsert(nsIContent* aContent,
+ nsIContent* aChildContent) {
+ mInNotification++;
+
+ {
+ // Scope so we call EndUpdate before we decrease mInNotification
+ // Note that aContent->OwnerDoc() may be different to mDocument already.
+ MOZ_AUTO_DOC_UPDATE(aContent ? aContent->OwnerDoc() : mDocument.get(),
+ true);
+ MutationObservers::NotifyContentInserted(NODE_FROM(aContent, mDocument),
+ aChildContent);
+ mLastNotificationTime = PR_Now();
+ }
+
+ mInNotification--;
+}
+
+void HTMLContentSink::NotifyRootInsertion() {
+ MOZ_ASSERT(!mNotifiedRootInsertion, "Double-notifying on root?");
+ NS_ASSERTION(!mLayoutStarted,
+ "How did we start layout without notifying on root?");
+ // Now make sure to notify that we have now inserted our root. If
+ // there has been no initial reflow yet it'll be a no-op, but if
+ // there has been one we need this to get its frames constructed.
+ // Note that if mNotifiedRootInsertion is true we don't notify here,
+ // since that just means there are multiple <html> tags in the
+ // document; in those cases we just want to put all the attrs on one
+ // tag.
+ mNotifiedRootInsertion = true;
+ NotifyInsert(nullptr, mRoot);
+
+ // Now update the notification information in all our
+ // contexts, since we just inserted the root and notified on
+ // our whole tree
+ UpdateChildCounts();
+
+ nsContentUtils::AddScriptRunner(
+ new nsDocElementCreatedNotificationRunner(mDocument));
+}
+
+void HTMLContentSink::UpdateChildCounts() {
+ uint32_t numContexts = mContextStack.Length();
+ for (uint32_t i = 0; i < numContexts; i++) {
+ SinkContext* sc = mContextStack.ElementAt(i);
+
+ sc->UpdateChildCounts();
+ }
+
+ mCurrentContext->UpdateChildCounts();
+}
+
+void HTMLContentSink::FlushPendingNotifications(FlushType aType) {
+ // Only flush tags if we're not doing the notification ourselves
+ // (since we aren't reentrant)
+ if (!mInNotification) {
+ // Only flush if we're still a document observer (so that our child counts
+ // should be correct).
+ if (mIsDocumentObserver) {
+ if (aType >= FlushType::ContentAndNotify) {
+ FlushTags();
+ }
+ }
+ if (aType >= FlushType::EnsurePresShellInitAndFrames) {
+ // Make sure that layout has started so that the reflow flush
+ // will actually happen.
+ StartLayout(true);
+ }
+ }
+}
+
+nsresult HTMLContentSink::FlushTags() {
+ if (!mNotifiedRootInsertion) {
+ NotifyRootInsertion();
+ return NS_OK;
+ }
+
+ return mCurrentContext ? mCurrentContext->FlushTags() : NS_OK;
+}
+
+void HTMLContentSink::SetDocumentCharset(NotNull<const Encoding*> aEncoding) {
+ MOZ_ASSERT_UNREACHABLE("<meta charset> case doesn't occur with about:blank");
+}
+
+nsISupports* HTMLContentSink::GetTarget() { return ToSupports(mDocument); }
+
+bool HTMLContentSink::IsScriptExecuting() { return IsScriptExecutingImpl(); }
+
+void HTMLContentSink::ContinueInterruptedParsingIfEnabled() {
+ if (mParser && mParser->IsParserEnabled()) {
+ static_cast<nsIParser*>(mParser.get())->ContinueInterruptedParsing();
+ }
+}
+
+bool HTMLContentSink::WaitForPendingSheets() {
+ return nsContentSink::WaitForPendingSheets();
+}
+
+void HTMLContentSink::ContinueInterruptedParsingAsync() {
+ nsCOMPtr<nsIRunnable> ev = NewRunnableMethod(
+ "HTMLContentSink::ContinueInterruptedParsingIfEnabled", this,
+ &HTMLContentSink::ContinueInterruptedParsingIfEnabled);
+ mHTMLDocument->Dispatch(ev.forget());
+}
diff --git a/dom/html/nsHTMLDocument.cpp b/dom/html/nsHTMLDocument.cpp
new file mode 100644
index 0000000000..26165bb622
--- /dev/null
+++ b/dom/html/nsHTMLDocument.cpp
@@ -0,0 +1,747 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsHTMLDocument.h"
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_intl.h"
+#include "nsCommandManager.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsPrintfCString.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsIHTMLContentSink.h"
+#include "nsIProtocolHandler.h"
+#include "nsIXMLContentSink.h"
+#include "nsHTMLParts.h"
+#include "nsGkAtoms.h"
+#include "nsPresContext.h"
+#include "nsPIDOMWindow.h"
+#include "nsDOMString.h"
+#include "nsIStreamListener.h"
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsIDocumentViewer.h"
+#include "nsDocShell.h"
+#include "nsDocShellLoadTypes.h"
+#include "nsIScriptContext.h"
+#include "nsContentList.h"
+#include "nsError.h"
+#include "nsIPrincipal.h"
+#include "nsJSPrincipals.h"
+#include "nsAttrName.h"
+
+#include "nsNetCID.h"
+#include "mozilla/parser/PrototypeDocumentParser.h"
+#include "mozilla/dom/PrototypeDocumentContentSink.h"
+#include "nsNameSpaceManager.h"
+#include "nsGenericHTMLElement.h"
+#include "mozilla/css/Loader.h"
+#include "nsFrameSelection.h"
+
+#include "nsContentUtils.h"
+#include "nsJSUtils.h"
+#include "DocumentInlines.h"
+#include "nsICachingChannel.h"
+#include "nsIScriptElement.h"
+#include "nsArrayUtils.h"
+
+// AHMED 12-2
+#include "nsBidiUtils.h"
+
+#include "mozilla/Encoding.h"
+#include "mozilla/EventListenerManager.h"
+#include "mozilla/IdentifierMapEntry.h"
+#include "mozilla/LoadInfo.h"
+#include "nsNodeInfoManager.h"
+#include "nsRange.h"
+#include "mozAutoDocUpdate.h"
+#include "nsCCUncollectableMarker.h"
+#include "nsHtml5Module.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/Preferences.h"
+#include "nsMimeTypes.h"
+#include "nsIRequest.h"
+#include "nsHtml5TreeOpExecutor.h"
+#include "nsHtml5Parser.h"
+#include "nsParser.h"
+#include "nsSandboxFlags.h"
+#include "mozilla/dom/HTMLBodyElement.h"
+#include "mozilla/dom/HTMLDocumentBinding.h"
+#include "mozilla/dom/nsCSPContext.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/ShadowIncludingTreeIterator.h"
+#include "nsCharsetSource.h"
+#include "nsFocusManager.h"
+#include "nsIFrame.h"
+#include "nsIContent.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StyleSheet.h"
+#include "mozilla/StyleSheetInlines.h"
+#include "mozilla/Unused.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+#include "prtime.h"
+
+// #define DEBUG_charset
+
+// ==================================================================
+// =
+// ==================================================================
+
+static bool IsAsciiCompatible(const Encoding* aEncoding) {
+ return aEncoding->IsAsciiCompatible() || aEncoding == ISO_2022_JP_ENCODING;
+}
+
+nsresult NS_NewHTMLDocument(Document** aInstancePtrResult,
+ nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal,
+ bool aLoadedAsData) {
+ RefPtr<nsHTMLDocument> doc = new nsHTMLDocument();
+
+ nsresult rv = doc->Init(aPrincipal, aPartitionedPrincipal);
+
+ if (NS_FAILED(rv)) {
+ *aInstancePtrResult = nullptr;
+ return rv;
+ }
+
+ doc->SetLoadedAsData(aLoadedAsData, /* aConsiderForMemoryReporting */ true);
+ doc.forget(aInstancePtrResult);
+
+ return NS_OK;
+}
+
+nsHTMLDocument::nsHTMLDocument()
+ : Document("text/html"),
+ mContentListHolder(nullptr),
+ mNumForms(0),
+ mLoadFlags(0),
+ mWarnedWidthHeight(false),
+ mIsPlainText(false),
+ mViewSource(false) {
+ mType = eHTML;
+ mDefaultElementType = kNameSpaceID_XHTML;
+ mCompatMode = eCompatibility_NavQuirks;
+}
+
+nsHTMLDocument::~nsHTMLDocument() = default;
+
+JSObject* nsHTMLDocument::WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return HTMLDocument_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsresult nsHTMLDocument::Init(nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) {
+ nsresult rv = Document::Init(aPrincipal, aPartitionedPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now reset the compatibility mode of the CSSLoader
+ // to match our compat mode.
+ CSSLoader()->SetCompatibilityMode(mCompatMode);
+
+ return NS_OK;
+}
+
+void nsHTMLDocument::Reset(nsIChannel* aChannel, nsILoadGroup* aLoadGroup) {
+ Document::Reset(aChannel, aLoadGroup);
+
+ if (aChannel) {
+ aChannel->GetLoadFlags(&mLoadFlags);
+ }
+}
+
+void nsHTMLDocument::ResetToURI(nsIURI* aURI, nsILoadGroup* aLoadGroup,
+ nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) {
+ mLoadFlags = nsIRequest::LOAD_NORMAL;
+
+ Document::ResetToURI(aURI, aLoadGroup, aPrincipal, aPartitionedPrincipal);
+
+ mImages = nullptr;
+ mApplets = nullptr;
+ mEmbeds = nullptr;
+ mLinks = nullptr;
+ mAnchors = nullptr;
+ mScripts = nullptr;
+
+ mForms = nullptr;
+
+ // Make the content type default to "text/html", we are a HTML
+ // document, after all. Once we start getting data, this may be
+ // changed.
+ SetContentType(nsDependentCString("text/html"));
+}
+
+void nsHTMLDocument::TryReloadCharset(nsIDocumentViewer* aViewer,
+ int32_t& aCharsetSource,
+ NotNull<const Encoding*>& aEncoding) {
+ if (aViewer) {
+ int32_t reloadEncodingSource;
+ const auto reloadEncoding =
+ aViewer->GetReloadEncodingAndSource(&reloadEncodingSource);
+ if (kCharsetUninitialized != reloadEncodingSource) {
+ aViewer->ForgetReloadEncoding();
+
+ if (reloadEncodingSource <= aCharsetSource ||
+ !IsAsciiCompatible(aEncoding)) {
+ return;
+ }
+
+ if (reloadEncoding && IsAsciiCompatible(reloadEncoding)) {
+ aCharsetSource = reloadEncodingSource;
+ aEncoding = WrapNotNull(reloadEncoding);
+ }
+ }
+ }
+}
+
+void nsHTMLDocument::TryUserForcedCharset(nsIDocumentViewer* aViewer,
+ nsIDocShell* aDocShell,
+ int32_t& aCharsetSource,
+ NotNull<const Encoding*>& aEncoding,
+ bool& aForceAutoDetection) {
+ auto resetForce = MakeScopeExit([&] {
+ if (aDocShell) {
+ nsDocShell::Cast(aDocShell)->ResetForcedAutodetection();
+ }
+ });
+
+ if (aCharsetSource >= kCharsetFromOtherComponent) {
+ return;
+ }
+
+ // mCharacterSet not updated yet for channel, so check aEncoding, too.
+ if (WillIgnoreCharsetOverride() || !IsAsciiCompatible(aEncoding)) {
+ return;
+ }
+
+ if (aDocShell && nsDocShell::Cast(aDocShell)->GetForcedAutodetection()) {
+ // This is the Character Encoding menu code path in Firefox
+ aForceAutoDetection = true;
+ }
+}
+
+void nsHTMLDocument::TryParentCharset(nsIDocShell* aDocShell,
+ int32_t& aCharsetSource,
+ NotNull<const Encoding*>& aEncoding,
+ bool& aForceAutoDetection) {
+ if (!aDocShell) {
+ return;
+ }
+ if (aCharsetSource >= kCharsetFromOtherComponent) {
+ return;
+ }
+
+ int32_t parentSource;
+ const Encoding* parentCharset;
+ nsCOMPtr<nsIPrincipal> parentPrincipal;
+ aDocShell->GetParentCharset(parentCharset, &parentSource,
+ getter_AddRefs(parentPrincipal));
+ if (!parentCharset) {
+ return;
+ }
+ if (kCharsetFromInitialUserForcedAutoDetection == parentSource ||
+ kCharsetFromFinalUserForcedAutoDetection == parentSource) {
+ if (WillIgnoreCharsetOverride() ||
+ !IsAsciiCompatible(aEncoding) || // if channel said UTF-16
+ !IsAsciiCompatible(parentCharset)) {
+ return;
+ }
+ aEncoding = WrapNotNull(parentCharset);
+ aCharsetSource = kCharsetFromParentFrame;
+ aForceAutoDetection = true;
+ return;
+ }
+
+ if (aCharsetSource >= kCharsetFromParentFrame) {
+ return;
+ }
+
+ if (kCharsetFromInitialAutoDetectionASCII <= parentSource) {
+ // Make sure that's OK
+ if (!NodePrincipal()->Equals(parentPrincipal) ||
+ !IsAsciiCompatible(parentCharset)) {
+ return;
+ }
+
+ aEncoding = WrapNotNull(parentCharset);
+ aCharsetSource = kCharsetFromParentFrame;
+ }
+}
+
+// Using a prototype document is only allowed with chrome privilege.
+bool ShouldUsePrototypeDocument(nsIChannel* aChannel, Document* aDoc) {
+ if (!aChannel || !aDoc ||
+ !StaticPrefs::dom_prototype_document_cache_enabled()) {
+ return false;
+ }
+ return nsContentUtils::IsChromeDoc(aDoc);
+}
+
+nsresult nsHTMLDocument::StartDocumentLoad(
+ const char* aCommand, nsIChannel* aChannel, nsILoadGroup* aLoadGroup,
+ nsISupports* aContainer, nsIStreamListener** aDocListener, bool aReset) {
+ if (!aCommand) {
+ MOZ_ASSERT(false, "Command is mandatory");
+ return NS_ERROR_INVALID_POINTER;
+ }
+ if (mType != eHTML) {
+ MOZ_ASSERT(mType == eXHTML);
+ MOZ_ASSERT(false, "Must not set HTML doc to XHTML mode before load start.");
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ nsAutoCString contentType;
+ aChannel->GetContentType(contentType);
+
+ bool view =
+ !strcmp(aCommand, "view") || !strcmp(aCommand, "external-resource");
+ mViewSource = !strcmp(aCommand, "view-source");
+ bool asData = !strcmp(aCommand, kLoadAsData);
+ if (!(view || mViewSource || asData)) {
+ MOZ_ASSERT(false, "Bad parser command");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool html = contentType.EqualsLiteral(TEXT_HTML);
+ bool xhtml = !html && (contentType.EqualsLiteral(APPLICATION_XHTML_XML) ||
+ contentType.EqualsLiteral(APPLICATION_WAPXHTML_XML));
+ mIsPlainText =
+ !html && !xhtml && nsContentUtils::IsPlainTextType(contentType);
+ if (!(html || xhtml || mIsPlainText || mViewSource)) {
+ MOZ_ASSERT(false, "Channel with bad content type.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool forceUtf8 =
+ mIsPlainText && nsContentUtils::IsUtf8OnlyPlainTextType(contentType);
+
+ bool loadAsHtml5 = true;
+
+ if (!mViewSource && xhtml) {
+ // We're parsing XHTML as XML, remember that.
+ mType = eXHTML;
+ SetCompatibilityMode(eCompatibility_FullStandards);
+ loadAsHtml5 = false;
+ }
+
+ // TODO: Proper about:blank treatment is bug 543435
+ if (loadAsHtml5 && view) {
+ // mDocumentURI hasn't been set, yet, so get the URI from the channel
+ nsCOMPtr<nsIURI> uri;
+ aChannel->GetOriginalURI(getter_AddRefs(uri));
+ // Adapted from nsDocShell:
+ // GetSpec can be expensive for some URIs, so check the scheme first.
+ if (uri && uri->SchemeIs("about")) {
+ if (uri->GetSpecOrDefault().EqualsLiteral("about:blank")) {
+ loadAsHtml5 = false;
+ }
+ }
+ }
+
+ nsresult rv = Document::StartDocumentLoad(aCommand, aChannel, aLoadGroup,
+ aContainer, aDocListener, aReset);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aContainer));
+
+ bool loadWithPrototype = false;
+ RefPtr<nsHtml5Parser> html5Parser;
+ if (loadAsHtml5) {
+ html5Parser = nsHtml5Module::NewHtml5Parser();
+ mParser = html5Parser;
+ if (mIsPlainText) {
+ if (mViewSource) {
+ html5Parser->MarkAsNotScriptCreated("view-source-plain");
+ } else {
+ html5Parser->MarkAsNotScriptCreated("plain-text");
+ }
+ } else if (mViewSource && !html) {
+ html5Parser->MarkAsNotScriptCreated("view-source-xml");
+ } else {
+ html5Parser->MarkAsNotScriptCreated(aCommand);
+ }
+ } else if (xhtml && ShouldUsePrototypeDocument(aChannel, this)) {
+ loadWithPrototype = true;
+ nsCOMPtr<nsIURI> originalURI;
+ aChannel->GetOriginalURI(getter_AddRefs(originalURI));
+ mParser = new mozilla::parser::PrototypeDocumentParser(originalURI, this);
+ } else {
+ mParser = new nsParser();
+ }
+
+ // Look for the parent document. Note that at this point we don't have our
+ // content viewer set up yet, and therefore do not have a useful
+ // mParentDocument.
+
+ // in this block of code, if we get an error result, we return it
+ // but if we get a null pointer, that's perfectly legal for parent
+ // and parentViewer
+ nsCOMPtr<nsIDocShellTreeItem> parentAsItem;
+ if (docShell) {
+ docShell->GetInProcessSameTypeParent(getter_AddRefs(parentAsItem));
+ }
+
+ nsCOMPtr<nsIDocShell> parent(do_QueryInterface(parentAsItem));
+ nsCOMPtr<nsIDocumentViewer> parentViewer;
+ if (parent) {
+ rv = parent->GetDocViewer(getter_AddRefs(parentViewer));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIDocumentViewer> viewer;
+ if (docShell) {
+ docShell->GetDocViewer(getter_AddRefs(viewer));
+ }
+ if (!viewer) {
+ viewer = std::move(parentViewer);
+ }
+
+ nsAutoCString urlSpec;
+ uri->GetSpec(urlSpec);
+#ifdef DEBUG_charset
+ printf("Determining charset for %s\n", urlSpec.get());
+#endif
+
+ // These are the charset source and charset for our document
+ bool forceAutoDetection = false;
+ int32_t charsetSource = kCharsetUninitialized;
+ auto encoding = UTF_8_ENCODING;
+
+ // For error reporting and referrer policy setting
+ nsHtml5TreeOpExecutor* executor = nullptr;
+ if (loadAsHtml5) {
+ executor = static_cast<nsHtml5TreeOpExecutor*>(mParser->GetContentSink());
+ }
+
+ if (forceUtf8) {
+ charsetSource = kCharsetFromUtf8OnlyMime;
+ } else if (!IsHTMLDocument() || !docShell) { // no docshell for text/html XHR
+ charsetSource =
+ IsHTMLDocument() ? kCharsetFromFallback : kCharsetFromDocTypeDefault;
+ TryChannelCharset(aChannel, charsetSource, encoding, executor);
+ } else {
+ NS_ASSERTION(docShell, "Unexpected null value");
+
+ // The following will try to get the character encoding from various
+ // sources. Each Try* function will return early if the source is already
+ // at least as large as any of the sources it might look at. Some of
+ // these functions (like TryReloadCharset and TryParentCharset) can set
+ // charsetSource to various values depending on where the charset they
+ // end up finding originally comes from.
+
+ // Try the channel's charset (e.g., charset from HTTP
+ // "Content-Type" header) first. This way, we get to reject overrides in
+ // TryParentCharset and TryUserForcedCharset if the channel said UTF-16.
+ // This is to avoid socially engineered XSS by adding user-supplied
+ // content to a UTF-16 site such that the byte have a dangerous
+ // interpretation as ASCII and the user can be lured to using the
+ // charset menu.
+ TryChannelCharset(aChannel, charsetSource, encoding, executor);
+
+ TryUserForcedCharset(viewer, docShell, charsetSource, encoding,
+ forceAutoDetection);
+
+ TryReloadCharset(viewer, charsetSource, encoding); // For encoding reload
+ TryParentCharset(docShell, charsetSource, encoding, forceAutoDetection);
+ }
+
+ SetDocumentCharacterSetSource(charsetSource);
+ SetDocumentCharacterSet(encoding);
+
+ // Set the parser as the stream listener for the document loader...
+ rv = NS_OK;
+ nsCOMPtr<nsIStreamListener> listener = mParser->GetStreamListener();
+ listener.forget(aDocListener);
+
+#ifdef DEBUG_charset
+ printf(" charset = %s source %d\n", charset.get(), charsetSource);
+#endif
+ mParser->SetDocumentCharset(encoding, charsetSource, forceAutoDetection);
+ mParser->SetCommand(aCommand);
+
+ if (!IsHTMLDocument()) {
+ MOZ_ASSERT(!loadAsHtml5);
+ if (loadWithPrototype) {
+ nsCOMPtr<nsIContentSink> sink;
+ NS_NewPrototypeDocumentContentSink(getter_AddRefs(sink), this, uri,
+ docShell, aChannel);
+ mParser->SetContentSink(sink);
+ } else {
+ nsCOMPtr<nsIXMLContentSink> xmlsink;
+ NS_NewXMLContentSink(getter_AddRefs(xmlsink), this, uri, docShell,
+ aChannel);
+ mParser->SetContentSink(xmlsink);
+ }
+ } else {
+ if (loadAsHtml5) {
+ html5Parser->Initialize(this, uri, docShell, aChannel);
+ } else {
+ // about:blank *only*
+ nsCOMPtr<nsIHTMLContentSink> htmlsink;
+ NS_NewHTMLContentSink(getter_AddRefs(htmlsink), this, uri, docShell,
+ aChannel);
+ mParser->SetContentSink(htmlsink);
+ }
+ }
+
+ // parser the content of the URI
+ mParser->Parse(uri);
+
+ return rv;
+}
+
+bool nsHTMLDocument::UseWidthDeviceWidthFallbackViewport() const {
+ if (mIsPlainText) {
+ // Plain text documents are simple enough that font inflation doesn't offer
+ // any appreciable advantage over defaulting to "width=device-width" and
+ // subsequently turning on word-wrapping.
+ return true;
+ }
+ return Document::UseWidthDeviceWidthFallbackViewport();
+}
+
+Element* nsHTMLDocument::GetUnfocusedKeyEventTarget() {
+ if (nsGenericHTMLElement* body = GetBody()) {
+ return body;
+ }
+ return Document::GetUnfocusedKeyEventTarget();
+}
+
+bool nsHTMLDocument::IsRegistrableDomainSuffixOfOrEqualTo(
+ const nsAString& aHostSuffixString, const nsACString& aOrigHost) {
+ // https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to
+ if (aHostSuffixString.IsEmpty()) {
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> origURI = CreateInheritingURIForHost(aOrigHost);
+ if (!origURI) {
+ // Error: failed to parse input domain
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> newURI =
+ RegistrableDomainSuffixOfInternal(aHostSuffixString, origURI);
+ if (!newURI) {
+ // Error: illegal domain
+ return false;
+ }
+ return true;
+}
+
+void nsHTMLDocument::AddedForm() { ++mNumForms; }
+
+void nsHTMLDocument::RemovedForm() { --mNumForms; }
+
+int32_t nsHTMLDocument::GetNumFormsSynchronous() const { return mNumForms; }
+
+bool nsHTMLDocument::ResolveName(JSContext* aCx, const nsAString& aName,
+ JS::MutableHandle<JS::Value> aRetval,
+ ErrorResult& aError) {
+ IdentifierMapEntry* entry = mIdentifierMap.GetEntry(aName);
+ if (!entry) {
+ return false;
+ }
+
+ nsBaseContentList* list = entry->GetNameContentList();
+ uint32_t length = list ? list->Length() : 0;
+
+ nsIContent* node;
+ if (length > 0) {
+ if (length > 1) {
+ // The list contains more than one element, return the whole list.
+ if (!ToJSValue(aCx, list, aRetval)) {
+ aError.NoteJSContextException(aCx);
+ return false;
+ }
+ return true;
+ }
+
+ // Only one element in the list, return the element instead of returning
+ // the list.
+ node = list->Item(0);
+ } else {
+ // No named items were found, see if there's one registerd by id for aName.
+ Element* e = entry->GetIdElement();
+
+ if (!e || !nsGenericHTMLElement::ShouldExposeIdAsHTMLDocumentProperty(e)) {
+ return false;
+ }
+
+ node = e;
+ }
+
+ if (!ToJSValue(aCx, node, aRetval)) {
+ aError.NoteJSContextException(aCx);
+ return false;
+ }
+
+ return true;
+}
+
+void nsHTMLDocument::GetSupportedNames(nsTArray<nsString>& aNames) {
+ for (const auto& entry : mIdentifierMap) {
+ if (entry.HasNameElement() ||
+ entry.HasIdElementExposedAsHTMLDocumentProperty()) {
+ aNames.AppendElement(entry.GetKeyAsString());
+ }
+ }
+}
+
+//----------------------------
+
+// forms related stuff
+
+bool nsHTMLDocument::MatchFormControls(Element* aElement, int32_t aNamespaceID,
+ nsAtom* aAtom, void* aData) {
+ return aElement->IsHTMLFormControlElement();
+}
+
+nsresult nsHTMLDocument::Clone(dom::NodeInfo* aNodeInfo,
+ nsINode** aResult) const {
+ NS_ASSERTION(aNodeInfo->NodeInfoManager() == mNodeInfoManager,
+ "Can't import this document into another document!");
+
+ RefPtr<nsHTMLDocument> clone = new nsHTMLDocument();
+ nsresult rv = CloneDocHelper(clone.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // State from nsHTMLDocument
+ clone->mLoadFlags = mLoadFlags;
+
+ clone.forget(aResult);
+ return NS_OK;
+}
+
+/* virtual */
+void nsHTMLDocument::DocAddSizeOfExcludingThis(
+ nsWindowSizes& aWindowSizes) const {
+ Document::DocAddSizeOfExcludingThis(aWindowSizes);
+
+ // Measurement of the following members may be added later if DMD finds it is
+ // worthwhile:
+ // - mLinks
+ // - mAnchors
+}
+
+bool nsHTMLDocument::WillIgnoreCharsetOverride() {
+ if (mEncodingMenuDisabled) {
+ return true;
+ }
+ if (mType != eHTML) {
+ MOZ_ASSERT(mType == eXHTML);
+ return true;
+ }
+ if (mCharacterSetSource >= kCharsetFromByteOrderMark) {
+ return true;
+ }
+ if (!mCharacterSet->IsAsciiCompatible() &&
+ mCharacterSet != ISO_2022_JP_ENCODING) {
+ return true;
+ }
+ nsIURI* uri = GetOriginalURI();
+ if (uri) {
+ if (uri->SchemeIs("about")) {
+ return true;
+ }
+ bool isResource;
+ nsresult rv = NS_URIChainHasFlags(
+ uri, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isResource);
+ if (NS_FAILED(rv) || isResource) {
+ return true;
+ }
+ }
+
+ switch (mCharacterSetSource) {
+ case kCharsetUninitialized:
+ case kCharsetFromFallback:
+ case kCharsetFromDocTypeDefault:
+ case kCharsetFromInitialAutoDetectionWouldHaveBeenUTF8:
+ case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD:
+ case kCharsetFromFinalAutoDetectionWouldHaveBeenUTF8InitialWasASCII:
+ case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD:
+ case kCharsetFromParentFrame:
+ case kCharsetFromXmlDeclaration:
+ case kCharsetFromMetaTag:
+ case kCharsetFromChannel:
+ return false;
+ }
+
+ bool potentialEffect = false;
+ nsIPrincipal* parentPrincipal = NodePrincipal();
+
+ auto subDoc = [&potentialEffect, parentPrincipal](Document& aSubDoc) {
+ if (parentPrincipal->Equals(aSubDoc.NodePrincipal()) &&
+ !aSubDoc.WillIgnoreCharsetOverride()) {
+ potentialEffect = true;
+ return CallState::Stop;
+ }
+ return CallState::Continue;
+ };
+ EnumerateSubDocuments(subDoc);
+
+ return !potentialEffect;
+}
+
+void nsHTMLDocument::GetFormsAndFormControls(nsContentList** aFormList,
+ nsContentList** aFormControlList) {
+ RefPtr<ContentListHolder> holder = mContentListHolder;
+ if (!holder) {
+ // Flush our content model so it'll be up to date
+ // If this becomes unnecessary and the following line is removed,
+ // please also remove the corresponding flush operation from
+ // nsHtml5TreeBuilderCppSupplement.h. (Look for "See bug 497861." there.)
+ // XXXsmaug nsHtml5TreeBuilderCppSupplement doesn't seem to have such flush
+ // anymore.
+ FlushPendingNotifications(FlushType::Content);
+
+ RefPtr<nsContentList> htmlForms = GetExistingForms();
+ if (!htmlForms) {
+ // If the document doesn't have an existing forms content list, create a
+ // new one which will be released soon by ContentListHolder. The idea is
+ // that we don't have that list hanging around for a long time and slowing
+ // down future DOM mutations.
+ //
+ // Please keep this in sync with Document::Forms().
+ htmlForms = new nsContentList(this, kNameSpaceID_XHTML, nsGkAtoms::form,
+ nsGkAtoms::form,
+ /* aDeep = */ true,
+ /* aLiveList = */ true);
+ }
+
+ RefPtr<nsContentList> htmlFormControls = new nsContentList(
+ this, nsHTMLDocument::MatchFormControls, nullptr, nullptr,
+ /* aDeep = */ true,
+ /* aMatchAtom = */ nullptr,
+ /* aMatchNameSpaceId = */ kNameSpaceID_None,
+ /* aFuncMayDependOnAttr = */ true,
+ /* aLiveList = */ true);
+
+ holder = new ContentListHolder(this, htmlForms, htmlFormControls);
+ RefPtr<ContentListHolder> runnable = holder;
+ if (NS_SUCCEEDED(Dispatch(runnable.forget()))) {
+ mContentListHolder = holder;
+ }
+ }
+
+ NS_ADDREF(*aFormList = holder->mFormList);
+ NS_ADDREF(*aFormControlList = holder->mFormControlList);
+}
diff --git a/dom/html/nsHTMLDocument.h b/dom/html/nsHTMLDocument.h
new file mode 100644
index 0000000000..652ffd91db
--- /dev/null
+++ b/dom/html/nsHTMLDocument.h
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef nsHTMLDocument_h___
+#define nsHTMLDocument_h___
+
+#include "mozilla/Attributes.h"
+#include "nsContentList.h"
+#include "mozilla/dom/Document.h"
+#include "nsIHTMLCollection.h"
+#include "nsIScriptElement.h"
+#include "nsTArray.h"
+
+#include "PLDHashTable.h"
+#include "nsThreadUtils.h"
+#include "mozilla/dom/HTMLSharedElement.h"
+#include "mozilla/dom/BindingDeclarations.h"
+
+class nsCommandManager;
+class nsIURI;
+class nsIDocShell;
+class nsICachingChannel;
+class nsILoadGroup;
+
+namespace mozilla::dom {
+template <typename T>
+struct Nullable;
+class WindowProxyHolder;
+} // namespace mozilla::dom
+
+class nsHTMLDocument : public mozilla::dom::Document {
+ protected:
+ using ReferrerPolicy = mozilla::dom::ReferrerPolicy;
+ using Document = mozilla::dom::Document;
+ using Encoding = mozilla::Encoding;
+ template <typename T>
+ using NotNull = mozilla::NotNull<T>;
+
+ public:
+ using Document::SetDocumentURI;
+
+ nsHTMLDocument();
+ virtual nsresult Init(nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) override;
+
+ // Document
+ virtual void Reset(nsIChannel* aChannel, nsILoadGroup* aLoadGroup) override;
+ virtual void ResetToURI(nsIURI* aURI, nsILoadGroup* aLoadGroup,
+ nsIPrincipal* aPrincipal,
+ nsIPrincipal* aPartitionedPrincipal) override;
+
+ virtual nsresult StartDocumentLoad(const char* aCommand, nsIChannel* aChannel,
+ nsILoadGroup* aLoadGroup,
+ nsISupports* aContainer,
+ nsIStreamListener** aDocListener,
+ bool aReset = true) override;
+
+ protected:
+ virtual bool UseWidthDeviceWidthFallbackViewport() const override;
+
+ public:
+ mozilla::dom::Element* GetUnfocusedKeyEventTarget() override;
+
+ nsContentList* GetExistingForms() const { return mForms; }
+
+ bool IsPlainText() const { return mIsPlainText; }
+
+ bool IsViewSource() const { return mViewSource; }
+
+ // Returns whether an object was found for aName.
+ bool ResolveName(JSContext* aCx, const nsAString& aName,
+ JS::MutableHandle<JS::Value> aRetval,
+ mozilla::ErrorResult& aError);
+
+ /**
+ * Called when form->BindToTree() is called so that document knows
+ * immediately when a form is added
+ */
+ void AddedForm();
+ /**
+ * Called when form->SetDocument() is called so that document knows
+ * immediately when a form is removed
+ */
+ void RemovedForm();
+ /**
+ * Called to get a better count of forms than document.forms can provide
+ * without calling FlushPendingNotifications (bug 138892).
+ */
+ // XXXbz is this still needed now that we can flush just content,
+ // not the rest?
+ int32_t GetNumFormsSynchronous() const;
+ void SetIsXHTML(bool aXHTML) { mType = (aXHTML ? eXHTML : eHTML); }
+
+ virtual nsresult Clone(mozilla::dom::NodeInfo*,
+ nsINode** aResult) const override;
+
+ using mozilla::dom::DocumentOrShadowRoot::GetElementById;
+
+ virtual void DocAddSizeOfExcludingThis(
+ nsWindowSizes& aWindowSizes) const override;
+ // DocAddSizeOfIncludingThis is inherited from Document.
+
+ virtual bool WillIgnoreCharsetOverride() override;
+
+ // WebIDL API
+ virtual JSObject* WrapNode(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString& aHostSuffixString,
+ const nsACString& aOrigHost);
+ void NamedGetter(JSContext* cx, const nsAString& aName, bool& aFound,
+ JS::MutableHandle<JSObject*> aRetval,
+ mozilla::ErrorResult& rv) {
+ JS::Rooted<JS::Value> v(cx);
+ if ((aFound = ResolveName(cx, aName, &v, rv))) {
+ SetUseCounter(mozilla::eUseCounter_custom_HTMLDocumentNamedGetterHit);
+ aRetval.set(v.toObjectOrNull());
+ }
+ }
+ void GetSupportedNames(nsTArray<nsString>& aNames);
+ // We're picking up GetLocation from Document
+ already_AddRefed<mozilla::dom::Location> GetLocation() const {
+ return Document::GetLocation();
+ }
+
+ static bool MatchFormControls(mozilla::dom::Element* aElement,
+ int32_t aNamespaceID, nsAtom* aAtom,
+ void* aData);
+
+ void GetFormsAndFormControls(nsContentList** aFormList,
+ nsContentList** aFormControlList);
+
+ protected:
+ ~nsHTMLDocument();
+
+ nsresult GetBodySize(int32_t* aWidth, int32_t* aHeight);
+
+ nsIContent* MatchId(nsIContent* aContent, const nsAString& aId);
+
+ static void DocumentWriteTerminationFunc(nsISupports* aRef);
+
+ // A helper class to keep nsContentList objects alive for a short period of
+ // time. Note, when the final Release is called on an nsContentList object, it
+ // removes itself from MutationObserver list.
+ class ContentListHolder : public mozilla::Runnable {
+ public:
+ ContentListHolder(nsHTMLDocument* aDocument, nsContentList* aFormList,
+ nsContentList* aFormControlList)
+ : mozilla::Runnable("ContentListHolder"),
+ mDocument(aDocument),
+ mFormList(aFormList),
+ mFormControlList(aFormControlList) {}
+
+ ~ContentListHolder() {
+ MOZ_ASSERT(!mDocument->mContentListHolder ||
+ mDocument->mContentListHolder == this);
+ mDocument->mContentListHolder = nullptr;
+ }
+
+ RefPtr<nsHTMLDocument> mDocument;
+ RefPtr<nsContentList> mFormList;
+ RefPtr<nsContentList> mFormControlList;
+ };
+
+ friend class ContentListHolder;
+ ContentListHolder* mContentListHolder;
+
+ /** # of forms in the document, synchronously set */
+ int32_t mNumForms;
+
+ static void TryReloadCharset(nsIDocumentViewer* aViewer,
+ int32_t& aCharsetSource,
+ NotNull<const Encoding*>& aEncoding);
+ void TryUserForcedCharset(nsIDocumentViewer* aViewer, nsIDocShell* aDocShell,
+ int32_t& aCharsetSource,
+ NotNull<const Encoding*>& aEncoding,
+ bool& aForceAutoDetection);
+ void TryParentCharset(nsIDocShell* aDocShell, int32_t& charsetSource,
+ NotNull<const Encoding*>& aEncoding,
+ bool& aForceAutoDetection);
+
+ // Load flags of the document's channel
+ uint32_t mLoadFlags;
+
+ bool mWarnedWidthHeight;
+
+ /**
+ * Set to true once we know that we are loading plain text content.
+ */
+ bool mIsPlainText;
+
+ /**
+ * Set to true once we know that we are viewing source.
+ */
+ bool mViewSource;
+};
+
+namespace mozilla::dom {
+
+inline nsHTMLDocument* Document::AsHTMLDocument() {
+ MOZ_ASSERT(IsHTMLOrXHTML());
+ return static_cast<nsHTMLDocument*>(this);
+}
+
+inline const nsHTMLDocument* Document::AsHTMLDocument() const {
+ MOZ_ASSERT(IsHTMLOrXHTML());
+ return static_cast<const nsHTMLDocument*>(this);
+}
+
+} // namespace mozilla::dom
+
+#endif /* nsHTMLDocument_h___ */
diff --git a/dom/html/nsIConstraintValidation.cpp b/dom/html/nsIConstraintValidation.cpp
new file mode 100644
index 0000000000..6ccda2ea7e
--- /dev/null
+++ b/dom/html/nsIConstraintValidation.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIConstraintValidation.h"
+
+#include "nsGenericHTMLElement.h"
+#include "mozilla/dom/CustomEvent.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "mozilla/dom/HTMLFieldSetElement.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/ValidityState.h"
+#include "nsIFormControl.h"
+#include "nsISimpleEnumerator.h"
+#include "nsContentUtils.h"
+
+const uint16_t nsIConstraintValidation::sContentSpecifiedMaxLengthMessage = 256;
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsIConstraintValidation::nsIConstraintValidation()
+ : mValidityBitField(0)
+ // By default, all elements are subjects to constraint validation.
+ ,
+ mBarredFromConstraintValidation(false) {}
+
+nsIConstraintValidation::~nsIConstraintValidation() = default;
+
+mozilla::dom::ValidityState* nsIConstraintValidation::Validity() {
+ if (!mValidity) {
+ mValidity = new mozilla::dom::ValidityState(this);
+ }
+
+ return mValidity;
+}
+
+bool nsIConstraintValidation::CheckValidity(nsIContent& aEventTarget,
+ bool* aEventDefaultAction) const {
+ if (!IsCandidateForConstraintValidation() || IsValid()) {
+ return true;
+ }
+
+ nsContentUtils::DispatchTrustedEvent(
+ aEventTarget.OwnerDoc(), &aEventTarget, u"invalid"_ns, CanBubble::eNo,
+ Cancelable::eYes, Composed::eDefault, aEventDefaultAction);
+ return false;
+}
+
+bool nsIConstraintValidation::ReportValidity() {
+ nsCOMPtr<Element> element = do_QueryInterface(this);
+ MOZ_ASSERT(element, "This class should be inherited by HTML elements only!");
+
+ bool defaultAction = true;
+ if (CheckValidity(*element, &defaultAction)) {
+ return true;
+ }
+
+ if (!defaultAction) {
+ return false;
+ }
+
+ AutoTArray<RefPtr<Element>, 1> invalidElements;
+ invalidElements.AppendElement(element);
+
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(element->GetOwnerGlobal())) {
+ return false;
+ }
+ JS::Rooted<JS::Value> detail(jsapi.cx());
+ if (!ToJSValue(jsapi.cx(), invalidElements, &detail)) {
+ return false;
+ }
+
+ RefPtr<CustomEvent> event =
+ NS_NewDOMCustomEvent(element->OwnerDoc(), nullptr, nullptr);
+ event->InitCustomEvent(jsapi.cx(), u"MozInvalidForm"_ns,
+ /* CanBubble */ true,
+ /* Cancelable */ true, detail);
+ event->SetTrusted(true);
+ event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
+
+ element->DispatchEvent(*event);
+ return false;
+}
+
+void nsIConstraintValidation::SetValidityState(ValidityStateType aState,
+ bool aValue) {
+ bool previousValidity = IsValid();
+
+ if (aValue) {
+ mValidityBitField |= aState;
+ } else {
+ mValidityBitField &= ~aState;
+ }
+
+ // Inform the form and fieldset elements if our validity has changed.
+ if (previousValidity != IsValid() && IsCandidateForConstraintValidation()) {
+ nsCOMPtr<nsIFormControl> formCtrl = do_QueryInterface(this);
+ NS_ASSERTION(formCtrl, "This interface should be used by form elements!");
+
+ if (HTMLFormElement* form = formCtrl->GetForm()) {
+ form->UpdateValidity(IsValid());
+ }
+ if (HTMLFieldSetElement* fieldSet = formCtrl->GetFieldSet()) {
+ fieldSet->UpdateValidity(IsValid());
+ }
+ }
+}
+
+void nsIConstraintValidation::SetBarredFromConstraintValidation(bool aBarred) {
+ bool previousBarred = mBarredFromConstraintValidation;
+
+ mBarredFromConstraintValidation = aBarred;
+
+ // Inform the form and fieldset elements if our status regarding constraint
+ // validation is going to change.
+ if (!IsValid() && previousBarred != mBarredFromConstraintValidation) {
+ nsCOMPtr<nsIFormControl> formCtrl = do_QueryInterface(this);
+ NS_ASSERTION(formCtrl, "This interface should be used by form elements!");
+
+ // If the element is going to be barred from constraint validation, we can
+ // inform the form and fieldset that we are now valid. Otherwise, we are now
+ // invalid.
+ if (HTMLFormElement* form = formCtrl->GetForm()) {
+ form->UpdateValidity(aBarred);
+ }
+ HTMLFieldSetElement* fieldSet = formCtrl->GetFieldSet();
+ if (fieldSet) {
+ fieldSet->UpdateValidity(aBarred);
+ }
+ }
+}
diff --git a/dom/html/nsIConstraintValidation.h b/dom/html/nsIConstraintValidation.h
new file mode 100644
index 0000000000..14f7cee77f
--- /dev/null
+++ b/dom/html/nsIConstraintValidation.h
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIConstraintValidition_h___
+#define nsIConstraintValidition_h___
+
+#include "nsISupports.h"
+
+class nsIContent;
+
+namespace mozilla::dom {
+class ValidityState;
+} // namespace mozilla::dom
+
+#define NS_ICONSTRAINTVALIDATION_IID \
+ { \
+ 0x983829da, 0x1aaf, 0x449c, { \
+ 0xa3, 0x06, 0x85, 0xd4, 0xf0, 0x31, 0x1c, 0xf6 \
+ } \
+ }
+
+/**
+ * This interface is for form elements implementing the validity constraint API.
+ * See: http://dev.w3.org/html5/spec/forms.html#the-constraint-validation-api
+ *
+ * This interface has to be implemented by all elements implementing the API
+ * and only them.
+ */
+class nsIConstraintValidation : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_ICONSTRAINTVALIDATION_IID);
+
+ friend class mozilla::dom::ValidityState;
+
+ static const uint16_t sContentSpecifiedMaxLengthMessage;
+
+ virtual ~nsIConstraintValidation();
+
+ bool IsValid() const { return mValidityBitField == 0; }
+
+ bool IsCandidateForConstraintValidation() const {
+ return !mBarredFromConstraintValidation;
+ }
+
+ enum ValidityStateType {
+ VALIDITY_STATE_VALUE_MISSING = 0x1 << 0,
+ VALIDITY_STATE_TYPE_MISMATCH = 0x1 << 1,
+ VALIDITY_STATE_PATTERN_MISMATCH = 0x1 << 2,
+ VALIDITY_STATE_TOO_LONG = 0x1 << 3,
+ VALIDITY_STATE_TOO_SHORT = 0x1 << 4,
+ VALIDITY_STATE_RANGE_UNDERFLOW = 0x1 << 5,
+ VALIDITY_STATE_RANGE_OVERFLOW = 0x1 << 6,
+ VALIDITY_STATE_STEP_MISMATCH = 0x1 << 7,
+ VALIDITY_STATE_BAD_INPUT = 0x1 << 8,
+ VALIDITY_STATE_CUSTOM_ERROR = 0x1 << 9,
+ };
+
+ void SetValidityState(ValidityStateType aState, bool aValue);
+
+ /**
+ * Check the validity of this object. If it is not valid, file a "invalid"
+ * event on the aEventTarget.
+ *
+ * @param aEventTarget The target of the event.
+ * @param aDefaultAction Set to true if default action should be taken,
+ * see EventTarget::DispatchEvent.
+ * @return whether it's valid.
+ */
+ bool CheckValidity(nsIContent& aEventTarget,
+ bool* aEventDefaultAction = nullptr) const;
+
+ // Web IDL binding methods
+ bool WillValidate() const { return IsCandidateForConstraintValidation(); }
+ mozilla::dom::ValidityState* Validity();
+ bool ReportValidity();
+
+ protected:
+ // You can't instantiate an object from that class.
+ nsIConstraintValidation();
+
+ bool GetValidityState(ValidityStateType aState) const {
+ return mValidityBitField & aState;
+ }
+
+ void SetBarredFromConstraintValidation(bool aBarred);
+
+ /**
+ * A pointer to the ValidityState object.
+ */
+ RefPtr<mozilla::dom::ValidityState> mValidity;
+
+ private:
+ /**
+ * A bitfield representing the current validity state of the element.
+ * Each bit represent an error. All bits to zero means the element is valid.
+ */
+ int16_t mValidityBitField;
+
+ /**
+ * Keeps track whether the element is barred from constraint validation.
+ */
+ bool mBarredFromConstraintValidation;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIConstraintValidation,
+ NS_ICONSTRAINTVALIDATION_IID)
+
+#endif // nsIConstraintValidation_h___
diff --git a/dom/html/nsIFormControl.h b/dom/html/nsIFormControl.h
new file mode 100644
index 0000000000..051f83215f
--- /dev/null
+++ b/dom/html/nsIFormControl.h
@@ -0,0 +1,288 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef nsIFormControl_h___
+#define nsIFormControl_h___
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsISupports.h"
+
+namespace mozilla {
+class PresState;
+namespace dom {
+class Element;
+class FormData;
+class HTMLFieldSetElement;
+class HTMLFormElement;
+} // namespace dom
+} // namespace mozilla
+
+// Elements with different types, the value is used as a mask.
+// When changing the order, adding or removing elements, be sure to update
+// the static_assert checks accordingly.
+constexpr uint8_t kFormControlButtonElementMask = 0x40; // 0b01000000
+constexpr uint8_t kFormControlInputElementMask = 0x80; // 0b10000000
+
+enum class FormControlType : uint8_t {
+ Fieldset = 1,
+ Output,
+ Select,
+ Textarea,
+ Object,
+ FormAssociatedCustomElement,
+
+ LastWithoutSubtypes = FormAssociatedCustomElement,
+
+ ButtonButton = kFormControlButtonElementMask + 1,
+ ButtonReset,
+ ButtonSubmit,
+ LastButtonElement = ButtonSubmit,
+
+ InputButton = kFormControlInputElementMask + 1,
+ InputCheckbox,
+ InputColor,
+ InputDate,
+ InputEmail,
+ InputFile,
+ InputHidden,
+ InputReset,
+ InputImage,
+ InputMonth,
+ InputNumber,
+ InputPassword,
+ InputRadio,
+ InputSearch,
+ InputSubmit,
+ InputTel,
+ InputText,
+ InputTime,
+ InputUrl,
+ InputRange,
+ InputWeek,
+ InputDatetimeLocal,
+ LastInputElement = InputDatetimeLocal,
+};
+
+static_assert(uint8_t(FormControlType::LastWithoutSubtypes) <
+ kFormControlButtonElementMask,
+ "Too many FormControlsTypes without sub-types");
+static_assert(uint8_t(FormControlType::LastButtonElement) <
+ kFormControlInputElementMask,
+ "Too many ButtonElementTypes");
+static_assert(uint32_t(FormControlType::LastInputElement) < (1 << 8),
+ "Too many form control types");
+
+#define NS_IFORMCONTROL_IID \
+ { \
+ 0x4b89980c, 0x4dcd, 0x428f, { \
+ 0xb7, 0xad, 0x43, 0x5b, 0x93, 0x29, 0x79, 0xec \
+ } \
+ }
+
+/**
+ * Interface which all form controls (e.g. buttons, checkboxes, text,
+ * radio buttons, select, etc) implement in addition to their dom specific
+ * interface.
+ */
+class nsIFormControl : public nsISupports {
+ public:
+ nsIFormControl(FormControlType aType) : mType(aType) {}
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IFORMCONTROL_IID)
+
+ /**
+ * Get the fieldset for this form control.
+ * @return the fieldset
+ */
+ virtual mozilla::dom::HTMLFieldSetElement* GetFieldSet() = 0;
+
+ /**
+ * Get the form for this form control.
+ * @return the form
+ */
+ virtual mozilla::dom::HTMLFormElement* GetForm() const = 0;
+
+ /**
+ * Set the form for this form control.
+ * @param aForm the form. This must not be null.
+ *
+ * @note that when setting the form the control is not added to the
+ * form. It adds itself when it gets bound to the tree thereafter,
+ * so that it can be properly sorted with the other controls in the
+ * form.
+ */
+ virtual void SetForm(mozilla::dom::HTMLFormElement* aForm) = 0;
+
+ /**
+ * Tell the control to forget about its form.
+ *
+ * @param aRemoveFromForm set false if you do not want this element removed
+ * from the form. (Used by nsFormControlList::Clear())
+ * @param aUnbindOrDelete set true if the element is being deleted or unbound
+ * from tree.
+ */
+ virtual void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) = 0;
+
+ /**
+ * Get the type of this control as an int (see NS_FORM_* above)
+ * @return the type of this control
+ */
+ FormControlType ControlType() const { return mType; }
+
+ /**
+ * Reset this form control (as it should be when the user clicks the Reset
+ * button)
+ */
+ NS_IMETHOD Reset() = 0;
+
+ /**
+ * Tells the form control to submit its names and values to the form data
+ * object
+ *
+ * @param aFormData the form data to notify of names/values/files to submit
+ */
+ NS_IMETHOD
+ SubmitNamesValues(mozilla::dom::FormData* aFormData) = 0;
+
+ /**
+ * Returns whether this is a control which submits the form when activated by
+ * the user.
+ * @return whether this is a submit control.
+ */
+ inline bool IsSubmitControl() const;
+
+ /**
+ * Returns whether this is a text control.
+ * @param aExcludePassword to have NS_FORM_INPUT_PASSWORD returning false.
+ * @return whether this is a text control.
+ */
+ inline bool IsTextControl(bool aExcludePassword) const;
+
+ /**
+ * Returns whether this is a single line text control.
+ * @param aExcludePassword to have NS_FORM_INPUT_PASSWORD returning false.
+ * @return whether this is a single line text control.
+ */
+ inline bool IsSingleLineTextControl(bool aExcludePassword) const;
+
+ /**
+ * Returns whether this is a submittable form control.
+ * @return whether this is a submittable form control.
+ */
+ inline bool IsSubmittableControl() const;
+
+ /**
+ * https://html.spec.whatwg.org/multipage/forms.html#concept-button
+ */
+ inline bool IsConceptButton() const;
+
+ /**
+ * Returns whether this is an ordinal button or a concept button that has no
+ * form associated.
+ */
+ inline bool IsButtonControl() const;
+
+ /**
+ * Returns whether this form control can have draggable children.
+ * @return whether this form control can have draggable children.
+ */
+ inline bool AllowDraggableChildren() const;
+
+ // Returns a number for this form control that is unique within its
+ // owner document. This is used by nsContentUtils::GenerateStateKey
+ // to identify form controls that are inserted into the document by
+ // the parser. -1 is returned for form controls with no state or
+ // which were inserted into the document by some other means than
+ // the parser from the network.
+ virtual int32_t GetParserInsertedControlNumberForStateKey() const {
+ return -1;
+ };
+
+ protected:
+ /**
+ * Returns whether mType corresponds to a single line text control type.
+ * @param aExcludePassword to have NS_FORM_INPUT_PASSWORD ignored.
+ * @param aType the type to be tested.
+ * @return whether mType corresponds to a single line text control type.
+ */
+ inline static bool IsSingleLineTextControl(bool aExcludePassword,
+ FormControlType);
+
+ inline static bool IsButtonElement(FormControlType aType) {
+ return uint8_t(aType) & kFormControlButtonElementMask;
+ }
+
+ inline static bool IsInputElement(FormControlType aType) {
+ return uint8_t(aType) & kFormControlInputElementMask;
+ }
+
+ FormControlType mType;
+};
+
+bool nsIFormControl::IsSubmitControl() const {
+ FormControlType type = ControlType();
+ return type == FormControlType::InputSubmit ||
+ type == FormControlType::InputImage ||
+ type == FormControlType::ButtonSubmit;
+}
+
+bool nsIFormControl::IsTextControl(bool aExcludePassword) const {
+ FormControlType type = ControlType();
+ return type == FormControlType::Textarea ||
+ IsSingleLineTextControl(aExcludePassword, type);
+}
+
+bool nsIFormControl::IsSingleLineTextControl(bool aExcludePassword) const {
+ return IsSingleLineTextControl(aExcludePassword, ControlType());
+}
+
+/*static*/
+bool nsIFormControl::IsSingleLineTextControl(bool aExcludePassword,
+ FormControlType aType) {
+ switch (aType) {
+ case FormControlType::InputText:
+ case FormControlType::InputEmail:
+ case FormControlType::InputSearch:
+ case FormControlType::InputTel:
+ case FormControlType::InputUrl:
+ case FormControlType::InputNumber:
+ // TODO: those are temporary until bug 773205 is fixed.
+ case FormControlType::InputMonth:
+ case FormControlType::InputWeek:
+ return true;
+ case FormControlType::InputPassword:
+ return !aExcludePassword;
+ default:
+ return false;
+ }
+}
+
+bool nsIFormControl::IsSubmittableControl() const {
+ auto type = ControlType();
+ return type == FormControlType::Object || type == FormControlType::Textarea ||
+ type == FormControlType::Select || IsButtonElement(type) ||
+ IsInputElement(type);
+}
+
+bool nsIFormControl::IsConceptButton() const {
+ auto type = ControlType();
+ return IsSubmitControl() || type == FormControlType::InputReset ||
+ type == FormControlType::InputButton || IsButtonElement(type);
+}
+
+bool nsIFormControl::IsButtonControl() const {
+ return IsConceptButton() && (!GetForm() || !IsSubmitControl());
+}
+
+bool nsIFormControl::AllowDraggableChildren() const {
+ auto type = ControlType();
+ return type == FormControlType::Object || type == FormControlType::Fieldset ||
+ type == FormControlType::Output;
+}
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIFormControl, NS_IFORMCONTROL_IID)
+
+#endif /* nsIFormControl_h___ */
diff --git a/dom/html/nsIHTMLCollection.h b/dom/html/nsIHTMLCollection.h
new file mode 100644
index 0000000000..17d0c1fbcf
--- /dev/null
+++ b/dom/html/nsIHTMLCollection.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIHTMLCollection_h___
+#define nsIHTMLCollection_h___
+
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArrayForwardDeclare.h"
+#include "nsWrapperCache.h"
+#include "js/TypeDecls.h"
+
+class nsINode;
+
+namespace mozilla::dom {
+class Element;
+} // namespace mozilla::dom
+
+// IID for the nsIHTMLCollection interface
+#define NS_IHTMLCOLLECTION_IID \
+ { \
+ 0x4e169191, 0x5196, 0x4e17, { \
+ 0xa4, 0x79, 0xd5, 0x35, 0x0b, 0x5b, 0x0a, 0xcd \
+ } \
+ }
+
+/**
+ * An internal interface
+ */
+class nsIHTMLCollection : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IHTMLCOLLECTION_IID)
+
+ /**
+ * Get the root node for this HTML collection.
+ */
+ virtual nsINode* GetParentObject() = 0;
+
+ virtual uint32_t Length() = 0;
+ virtual mozilla::dom::Element* GetElementAt(uint32_t index) = 0;
+ mozilla::dom::Element* Item(uint32_t index) { return GetElementAt(index); }
+ mozilla::dom::Element* IndexedGetter(uint32_t index, bool& aFound) {
+ mozilla::dom::Element* item = Item(index);
+ aFound = !!item;
+ return item;
+ }
+ mozilla::dom::Element* NamedItem(const nsAString& aName) {
+ bool dummy;
+ return NamedGetter(aName, dummy);
+ }
+ mozilla::dom::Element* NamedGetter(const nsAString& aName, bool& aFound) {
+ return GetFirstNamedElement(aName, aFound);
+ }
+ virtual mozilla::dom::Element* GetFirstNamedElement(const nsAString& aName,
+ bool& aFound) = 0;
+
+ virtual void GetSupportedNames(nsTArray<nsString>& aNames) = 0;
+
+ JSObject* GetWrapperPreserveColor() {
+ return GetWrapperPreserveColorInternal();
+ }
+ JSObject* GetWrapper() {
+ JSObject* obj = GetWrapperPreserveColor();
+ if (obj) {
+ JS::ExposeObjectToActiveJS(obj);
+ }
+ return obj;
+ }
+ void PreserveWrapper(nsISupports* aScriptObjectHolder) {
+ PreserveWrapperInternal(aScriptObjectHolder);
+ }
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) = 0;
+
+ protected:
+ // Hook for calling nsWrapperCache::GetWrapperPreserveColor.
+ virtual JSObject* GetWrapperPreserveColorInternal() = 0;
+ // Hook for calling nsWrapperCache::PreserveWrapper.
+ virtual void PreserveWrapperInternal(nsISupports* aScriptObjectHolder) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIHTMLCollection, NS_IHTMLCOLLECTION_IID)
+
+#endif /* nsIHTMLCollection_h___ */
diff --git a/dom/html/nsIRadioVisitor.h b/dom/html/nsIRadioVisitor.h
new file mode 100644
index 0000000000..308d75170a
--- /dev/null
+++ b/dom/html/nsIRadioVisitor.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIRadioVisitor_h___
+#define nsIRadioVisitor_h___
+
+#include "nsISupports.h"
+
+namespace mozilla::dom {
+class HTMLInputElement;
+} // namespace mozilla::dom
+
+// IID for the nsIRadioControl interface
+#define NS_IRADIOVISITOR_IID \
+ { \
+ 0xc6bed232, 0x1181, 0x4ab2, { \
+ 0xa1, 0xda, 0x55, 0xc2, 0x13, 0x6d, 0xea, 0x3d \
+ } \
+ }
+
+/**
+ * This interface is used for the text control frame to store its value away
+ * into the content.
+ */
+class nsIRadioVisitor : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IRADIOVISITOR_IID)
+
+ /**
+ * Visit a node in the tree. This is meant to be called on all radios in a
+ * group, sequentially. (Each radio group implementor may define
+ * sequentially in their own way, it just has to be the same every time.)
+ * Currently all radio groups are ordered in the order they appear in the
+ * document. Radio group implementors should honor the return value of the
+ * method and stop iterating if the return value is false.
+ *
+ * @param aRadio the radio button in question (must be nullptr and QI'able to
+ * nsIRadioControlElement)
+ */
+ virtual bool Visit(mozilla::dom::HTMLInputElement* aRadio) = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIRadioVisitor, NS_IRADIOVISITOR_IID)
+
+#endif // nsIRadioVisitor_h___
diff --git a/dom/html/nsRadioVisitor.cpp b/dom/html/nsRadioVisitor.cpp
new file mode 100644
index 0000000000..b7670f4774
--- /dev/null
+++ b/dom/html/nsRadioVisitor.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsRadioVisitor.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "nsIConstraintValidation.h"
+
+using namespace mozilla::dom;
+
+NS_IMPL_ISUPPORTS(nsRadioVisitor, nsIRadioVisitor)
+
+bool nsRadioSetCheckedChangedVisitor::Visit(HTMLInputElement* aRadio) {
+ NS_ASSERTION(aRadio, "Visit() passed a null button!");
+ aRadio->SetCheckedChangedInternal(mCheckedChanged);
+ return true;
+}
+
+bool nsRadioGetCheckedChangedVisitor::Visit(HTMLInputElement* aRadio) {
+ if (aRadio == mExcludeElement) {
+ return true;
+ }
+
+ NS_ASSERTION(aRadio, "Visit() passed a null button!");
+ *mCheckedChanged = aRadio->GetCheckedChanged();
+ return false;
+}
+
+bool nsRadioSetValueMissingState::Visit(HTMLInputElement* aRadio) {
+ if (aRadio == mExcludeElement) {
+ return true;
+ }
+
+ aRadio->SetValidityState(
+ nsIConstraintValidation::VALIDITY_STATE_VALUE_MISSING, mValidity);
+ aRadio->UpdateValidityElementStates(true);
+ return true;
+}
+
+bool nsRadioUpdateStateVisitor::Visit(HTMLInputElement* aRadio) {
+ if (aRadio == mExcludeElement) {
+ return true;
+ }
+ aRadio->UpdateIndeterminateState(true);
+ aRadio->UpdateValidityElementStates(true);
+ return true;
+}
diff --git a/dom/html/nsRadioVisitor.h b/dom/html/nsRadioVisitor.h
new file mode 100644
index 0000000000..0ec20a25a0
--- /dev/null
+++ b/dom/html/nsRadioVisitor.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsRadioVisitor_h__
+#define _nsRadioVisitor_h__
+
+#include "mozilla/Attributes.h"
+#include "nsIRadioVisitor.h"
+
+using mozilla::dom::HTMLInputElement;
+
+/**
+ * nsRadioVisitor is the base class implementing nsIRadioVisitor and inherited
+ * by all radio visitors.
+ */
+class nsRadioVisitor : public nsIRadioVisitor {
+ protected:
+ virtual ~nsRadioVisitor() = default;
+
+ public:
+ nsRadioVisitor() = default;
+
+ NS_DECL_ISUPPORTS
+
+ virtual bool Visit(HTMLInputElement* aRadio) override = 0;
+};
+
+/**
+ * The following declarations are radio visitors inheriting from nsRadioVisitor.
+ */
+
+/**
+ * nsRadioSetCheckedChangedVisitor is calling SetCheckedChanged with the given
+ * parameter to all radio elements in the group.
+ */
+class nsRadioSetCheckedChangedVisitor : public nsRadioVisitor {
+ public:
+ explicit nsRadioSetCheckedChangedVisitor(bool aCheckedChanged)
+ : mCheckedChanged(aCheckedChanged) {}
+
+ virtual bool Visit(HTMLInputElement* aRadio) override;
+
+ protected:
+ bool mCheckedChanged;
+};
+
+/**
+ * nsRadioGetCheckedChangedVisitor is getting the current checked changed value.
+ * Getting it from one radio element is the group is enough given that all
+ * elements should have the same value.
+ */
+class nsRadioGetCheckedChangedVisitor : public nsRadioVisitor {
+ public:
+ nsRadioGetCheckedChangedVisitor(bool* aCheckedChanged,
+ HTMLInputElement* aExcludeElement)
+ : mCheckedChanged(aCheckedChanged), mExcludeElement(aExcludeElement) {}
+
+ virtual bool Visit(HTMLInputElement* aRadio) override;
+
+ protected:
+ bool* mCheckedChanged;
+ HTMLInputElement* mExcludeElement;
+};
+
+/**
+ * nsRadioSetValueMissingState is calling SetValueMissingState with the given
+ * parameter to all radio elements in the group.
+ * It is also calling ContentStatesChanged if needed.
+ */
+class nsRadioSetValueMissingState : public nsRadioVisitor {
+ public:
+ nsRadioSetValueMissingState(HTMLInputElement* aExcludeElement, bool aValidity)
+ : mExcludeElement(aExcludeElement), mValidity(aValidity) {}
+
+ virtual bool Visit(HTMLInputElement* aRadio) override;
+
+ protected:
+ HTMLInputElement* mExcludeElement;
+ bool mValidity;
+};
+
+class nsRadioUpdateStateVisitor : public nsRadioVisitor {
+ public:
+ explicit nsRadioUpdateStateVisitor(HTMLInputElement* aExcludeElement)
+ : mExcludeElement(aExcludeElement) {}
+
+ virtual bool Visit(HTMLInputElement* aRadio) override;
+
+ protected:
+ HTMLInputElement* mExcludeElement;
+};
+
+#endif // _nsRadioVisitor_h__
diff --git a/dom/html/reftests/41464-1-ref.html b/dom/html/reftests/41464-1-ref.html
new file mode 100644
index 0000000000..3b68fca6d7
--- /dev/null
+++ b/dom/html/reftests/41464-1-ref.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<title>Dynamic manipulation of textarea.wrap</title>
+<link rel=help href=http://www.whatwg.org/html5/#dom-textarea-wrap>
+<link rel=author title=Ms2ger href=mailto:ms2ger@gmail.com>
+<textarea wrap=soft cols=20>01234567890 01234567890 01234567890</textarea>
diff --git a/dom/html/reftests/41464-1a.html b/dom/html/reftests/41464-1a.html
new file mode 100644
index 0000000000..f0569347fe
--- /dev/null
+++ b/dom/html/reftests/41464-1a.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<title>Dynamic manipulation of textarea.wrap</title>
+<link rel=help href=http://www.whatwg.org/html5/#dom-textarea-wrap>
+<link rel=author title=Ms2ger href=mailto:ms2ger@gmail.com>
+<textarea wrap=off cols=20>01234567890 01234567890 01234567890</textarea>
+<script>
+document.getElementsByTagName("textarea")[0].wrap = "soft";
+</script>
diff --git a/dom/html/reftests/41464-1b.html b/dom/html/reftests/41464-1b.html
new file mode 100644
index 0000000000..13c42518cc
--- /dev/null
+++ b/dom/html/reftests/41464-1b.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<title>Dynamic manipulation of textarea.wrap</title>
+<link rel=help href=http://www.whatwg.org/html5/#dom-textarea-wrap>
+<link rel=author title=Ms2ger href=mailto:ms2ger@gmail.com>
+<textarea wrap=off cols=20>01234567890 01234567890 01234567890</textarea>
+<script>
+document.getElementsByTagName("textarea")[0].setAttribute("wrap", "soft");
+</script>
diff --git a/dom/html/reftests/468263-1a.html b/dom/html/reftests/468263-1a.html
new file mode 100644
index 0000000000..93ad7df34e
--- /dev/null
+++ b/dom/html/reftests/468263-1a.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <img id="image2" src="pass.png">
+</body>
+</html>
diff --git a/dom/html/reftests/468263-1b.html b/dom/html/reftests/468263-1b.html
new file mode 100644
index 0000000000..e637e30945
--- /dev/null
+++ b/dom/html/reftests/468263-1b.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <input id="image3" type="image" src="pass.png">
+</body>
+</html>
diff --git a/dom/html/reftests/468263-1c.html b/dom/html/reftests/468263-1c.html
new file mode 100644
index 0000000000..14c2b2b378
--- /dev/null
+++ b/dom/html/reftests/468263-1c.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <object id="image4" type="image/png" data="pass.png"></object>
+</body>
+</html>
diff --git a/dom/html/reftests/468263-1d.html b/dom/html/reftests/468263-1d.html
new file mode 100644
index 0000000000..53740e596f
--- /dev/null
+++ b/dom/html/reftests/468263-1d.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <object id="image5" type="text/html" data="data:text/html,<b>Testing</b>"></object>
+</body>
+</html>
diff --git a/dom/html/reftests/468263-2-alternate-ref.html b/dom/html/reftests/468263-2-alternate-ref.html
new file mode 100644
index 0000000000..c3bdb9da93
--- /dev/null
+++ b/dom/html/reftests/468263-2-alternate-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <img id="image1" src="">
+ <img id="image2">
+ <input id="image3" type="image">
+</body>
+</html>
diff --git a/dom/html/reftests/468263-2-ref.html b/dom/html/reftests/468263-2-ref.html
new file mode 100644
index 0000000000..6e7f6cb360
--- /dev/null
+++ b/dom/html/reftests/468263-2-ref.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <img id="image1" src="">
+ <img id="image2">
+ <input id="image3" type="image">
+ <object id="image4" type="image/png">
+ <object id="image5" type="text/html"></object>
+</body>
+</html>
diff --git a/dom/html/reftests/468263-2.html b/dom/html/reftests/468263-2.html
new file mode 100644
index 0000000000..d3d447d07e
--- /dev/null
+++ b/dom/html/reftests/468263-2.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<body onload="document.getElementById('image1').setAttribute('src', ''); document.getElementById('image2').removeAttribute('src'); document.getElementById('image3').removeAttribute('src'); document.getElementById('image4').removeAttribute('data'); document.getElementById('image5').removeAttribute('data');">
+ <img id="image1" src="pass.png">
+ <img id="image2" src="pass.png">
+ <input id="image3" type="image" src="pass.png">
+ <object id="image4" type="image/png" data="pass.png"></object>
+ <object id="image5" type="text/html" data="data:text/html,<b>Testing</b>"></object>
+</body>
+</html>
diff --git a/dom/html/reftests/484200-1-ref.html b/dom/html/reftests/484200-1-ref.html
new file mode 100644
index 0000000000..39ef12261d
--- /dev/null
+++ b/dom/html/reftests/484200-1-ref.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Test for Bug 484200</title>
+<style>
+p { background:lime }
+</style>
+</head>
+<body>
+<p>xxxxx</p>
+</body>
+</html>
diff --git a/dom/html/reftests/484200-1.html b/dom/html/reftests/484200-1.html
new file mode 100644
index 0000000000..c2663d1aea
--- /dev/null
+++ b/dom/html/reftests/484200-1.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Test for Bug 484200</title>
+<style src=style>
+p { background:lime }
+</style>
+</head>
+<body>
+<p>xxxxx</p>
+</body>
+</html>
diff --git a/dom/html/reftests/485377-ref.html b/dom/html/reftests/485377-ref.html
new file mode 100644
index 0000000000..e98cbbb71f
--- /dev/null
+++ b/dom/html/reftests/485377-ref.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>The mark element</title>
+<p>Foo <span style="background: yellow; color: black;">bar</span> baz.
diff --git a/dom/html/reftests/485377.html b/dom/html/reftests/485377.html
new file mode 100644
index 0000000000..9f91f99805
--- /dev/null
+++ b/dom/html/reftests/485377.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<title>The mark element</title>
+<p>Foo <mark>bar</mark> baz.
diff --git a/dom/html/reftests/52019-1-ref.html b/dom/html/reftests/52019-1-ref.html
new file mode 100644
index 0000000000..73bf05f681
--- /dev/null
+++ b/dom/html/reftests/52019-1-ref.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Test for Bug 52019</title>
+</head>
+<body>
+<font>font</font>
+<font>font</font>
+<font size="+2">font</font>
+<font size="-2">font</font>
+</body>
+</html>
diff --git a/dom/html/reftests/52019-1.html b/dom/html/reftests/52019-1.html
new file mode 100644
index 0000000000..09cff148fa
--- /dev/null
+++ b/dom/html/reftests/52019-1.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Test for Bug 52019</title>
+</head>
+<body>
+<font size="+.5">font</font>
+<font size="-.5">font</font>
+<font size="+2.5">font</font>
+<font size="-2.5">font</font>
+</body>
+</html>
diff --git a/dom/html/reftests/557840-ref.html b/dom/html/reftests/557840-ref.html
new file mode 100644
index 0000000000..ea72d50f5f
--- /dev/null
+++ b/dom/html/reftests/557840-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Canvas and hspace, vspace</title>
+<canvas style="background: black;"></canvas>
diff --git a/dom/html/reftests/557840.html b/dom/html/reftests/557840.html
new file mode 100644
index 0000000000..4aed5092a8
--- /dev/null
+++ b/dom/html/reftests/557840.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Canvas and hspace, vspace</title>
+<canvas hspace="42" vspace="42" style="background: black;"></canvas>
diff --git a/dom/html/reftests/560059-video-dimensions-ref.html b/dom/html/reftests/560059-video-dimensions-ref.html
new file mode 100644
index 0000000000..f3424de689
--- /dev/null
+++ b/dom/html/reftests/560059-video-dimensions-ref.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Video dimensions</title>
+<video style="border: thin solid black;" width="300" height="150"></video>
diff --git a/dom/html/reftests/560059-video-dimensions.html b/dom/html/reftests/560059-video-dimensions.html
new file mode 100644
index 0000000000..99373c999a
--- /dev/null
+++ b/dom/html/reftests/560059-video-dimensions.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<title>Video dimensions</title>
+<video style="border: thin solid black;"></video>
diff --git a/dom/html/reftests/573322-no-quirks-ref.html b/dom/html/reftests/573322-no-quirks-ref.html
new file mode 100644
index 0000000000..e3f993f2f7
--- /dev/null
+++ b/dom/html/reftests/573322-no-quirks-ref.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<style>
+div { background: green; border: solid blue; width: 100px; height: 100px; }
+</style>
+<table border=1 width=50%>
+<tr>
+<td><div></div>left
+</table>
+<table border=1 width=50%>
+<tr>
+<td><div></div>justify
+</table>
+<table border=1 width=50%>
+<tr>
+<td style="text-align: -moz-right;"><div></div>right
+</table>
+<table border=1 width=50%>
+<tr>
+<td style="text-align: -moz-center;"><div></div>center
+</table>
+<table border=1 width=50%>
+<tr>
+<td style="text-align: -moz-center;"><div></div>middle
+</table>
+<table border=1 width=50%>
+<tr>
+<td style="text-align: center;"><div></div>absmiddle
+</table>
diff --git a/dom/html/reftests/573322-no-quirks.html b/dom/html/reftests/573322-no-quirks.html
new file mode 100644
index 0000000000..ac27609e36
--- /dev/null
+++ b/dom/html/reftests/573322-no-quirks.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<style>
+div { background: green; border: solid blue; width: 100px; height: 100px; }
+</style>
+<table border=1 width=50%>
+<tr>
+<td align=left><div></div>left
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=justify><div></div>justify
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=right><div></div>right
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=center><div></div>center
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=middle><div></div>middle
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=absmiddle><div></div>absmiddle
+</table>
diff --git a/dom/html/reftests/573322-quirks-ref.html b/dom/html/reftests/573322-quirks-ref.html
new file mode 100644
index 0000000000..6cc22a58e5
--- /dev/null
+++ b/dom/html/reftests/573322-quirks-ref.html
@@ -0,0 +1,27 @@
+<style>
+div { background: green; border: solid blue; width: 100px; height: 100px; }
+</style>
+<table border=1 width=50%>
+<tr>
+<td><div></div>left
+</table>
+<table border=1 width=50%>
+<tr>
+<td><div></div>justify
+</table>
+<table border=1 width=50%>
+<tr>
+<td style="text-align: -moz-right;"><div></div>right
+</table>
+<table border=1 width=50%>
+<tr>
+<td style="text-align: -moz-center;"><div></div>center
+</table>
+<table border=1 width=50%>
+<tr>
+<td style="text-align: -moz-center;"><div></div>middle
+</table>
+<table border=1 width=50%>
+<tr>
+<td style="text-align: center;"><div></div>absmiddle
+</table>
diff --git a/dom/html/reftests/573322-quirks.html b/dom/html/reftests/573322-quirks.html
new file mode 100644
index 0000000000..35757b1859
--- /dev/null
+++ b/dom/html/reftests/573322-quirks.html
@@ -0,0 +1,27 @@
+<style>
+div { background: green; border: solid blue; width: 100px; height: 100px; }
+</style>
+<table border=1 width=50%>
+<tr>
+<td align=left><div></div>left
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=justify><div></div>justify
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=right><div></div>right
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=center><div></div>center
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=middle><div></div>middle
+</table>
+<table border=1 width=50%>
+<tr>
+<td align=absmiddle><div></div>absmiddle
+</table>
diff --git a/dom/html/reftests/596455-1a.html b/dom/html/reftests/596455-1a.html
new file mode 100644
index 0000000000..60d494072e
--- /dev/null
+++ b/dom/html/reftests/596455-1a.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class='reftest-wait'>
+ <script>
+ function onLoadHandler()
+ {
+ document.getElementById('l').value = document.getElementById('i').value;
+ document.documentElement.className='';
+ }
+ </script>
+ <body onload="onLoadHandler();">
+ <input type='hidden' value='foo&#13;bar' id='i'>
+ <textarea id='l'></textarea>
+ </body>
+</html>
diff --git a/dom/html/reftests/596455-1b.html b/dom/html/reftests/596455-1b.html
new file mode 100644
index 0000000000..8478786c12
--- /dev/null
+++ b/dom/html/reftests/596455-1b.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class='reftest-wait'>
+ <script>
+ function onLoadHandler()
+ {
+ document.getElementById('l').value = document.getElementById('i').value;
+ document.documentElement.className='';
+ }
+ </script>
+ <body onload="onLoadHandler();">
+ <input value='foo&#13;bar' type='hidden' id='i'>
+ <textarea id='l'></textarea>
+ </body>
+</html>
diff --git a/dom/html/reftests/596455-2a.html b/dom/html/reftests/596455-2a.html
new file mode 100644
index 0000000000..f78a36c613
--- /dev/null
+++ b/dom/html/reftests/596455-2a.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class='reftest-wait'>
+ <script>
+ function onLoadHandler()
+ {
+ document.getElementById('l').value = document.getElementById('i').value;
+ document.documentElement.className='';
+ }
+ </script>
+ <body onload="onLoadHandler();">
+ <input style="display:none;" type='text' value='foo&#13;bar' id='i'>
+ <textarea id='l'></textarea>
+ </body>
+</html>
diff --git a/dom/html/reftests/596455-2b.html b/dom/html/reftests/596455-2b.html
new file mode 100644
index 0000000000..1867327903
--- /dev/null
+++ b/dom/html/reftests/596455-2b.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class='reftest-wait'>
+ <script>
+ function onLoadHandler()
+ {
+ document.getElementById('l').value = document.getElementById('i').value;
+ document.documentElement.className='';
+ }
+ </script>
+ <body onload="onLoadHandler();">
+ <input style="display:none;" value='foo&#13;bar' type='text' id='i'>
+ <textarea id='l'></textarea>
+ </body>
+</html>
diff --git a/dom/html/reftests/596455-ref-1.html b/dom/html/reftests/596455-ref-1.html
new file mode 100644
index 0000000000..10371df270
--- /dev/null
+++ b/dom/html/reftests/596455-ref-1.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea>foo&#13;bar</textarea>
+ </body>
+</html>
diff --git a/dom/html/reftests/596455-ref-2.html b/dom/html/reftests/596455-ref-2.html
new file mode 100644
index 0000000000..0dbc4dbb31
--- /dev/null
+++ b/dom/html/reftests/596455-ref-2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <textarea>foobar</textarea>
+ </body>
+</html>
diff --git a/dom/html/reftests/610935-ref.html b/dom/html/reftests/610935-ref.html
new file mode 100644
index 0000000000..7a2a41a52a
--- /dev/null
+++ b/dom/html/reftests/610935-ref.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<title>Test for bug 610935</title>
+<style>
+div { width:300px; background:yellow; height:50px; }
+table { width:150%; }
+td { background:blue; }
+</style>
+<div>
+ <table cellspacing="0" cellpadding="0" border="0">
+ <tr><td>parent div float=left</td></tr>
+ </table>
+</div>
diff --git a/dom/html/reftests/610935.html b/dom/html/reftests/610935.html
new file mode 100644
index 0000000000..5495ae3d5a
--- /dev/null
+++ b/dom/html/reftests/610935.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<title>Test for bug 610935</title>
+<style>
+div { width:300px; background:yellow; height:50px; }
+td { background:blue; }
+</style>
+<div>
+ <table width="150%" cellspacing="0" cellpadding="0" border="0">
+ <tr><td>parent div float=left</td></tr>
+ </table>
+</div>
diff --git a/dom/html/reftests/649134-1.html b/dom/html/reftests/649134-1.html
new file mode 100644
index 0000000000..b38e988304
--- /dev/null
+++ b/dom/html/reftests/649134-1.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html><head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Testcase for bug </title>
+<link rel="stylesheet" type="text/css" href="" />
+<!--
+ #foo {
+ /* This doesn't get evaluated */
+ color: red;
+ }
+ #ie {
+ border: 5px solid red;
+ }
+ #ie {
+ display: block;
+ }
+ #moz {
+ color: blue;
+ /* display: none; */
+ }
+-->
+
+</head>
+<body>
+
+<p id="foo">foo</p>
+<p id="ie">ie</p>
+<p id="moz">moz</p>
+
+</body>
+</html>
diff --git a/dom/html/reftests/649134-2-ref.html b/dom/html/reftests/649134-2-ref.html
new file mode 100644
index 0000000000..d15fae5282
--- /dev/null
+++ b/dom/html/reftests/649134-2-ref.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html><head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Testcase for bug </title>
+<style>
+ #ie {
+ border: 5px solid red;
+ }
+ #ie {
+ display: block;
+ }
+ #moz {
+ color: blue;
+ /* display: none; */
+ }
+</style>
+</head>
+<body>
+
+<p id="foo">foo</p>
+<p id="ie">ie</p>
+<p id="moz">moz</p>
+
+</body>
+</html>
diff --git a/dom/html/reftests/649134-2.html b/dom/html/reftests/649134-2.html
new file mode 100644
index 0000000000..4d2a5ae50d
--- /dev/null
+++ b/dom/html/reftests/649134-2.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html><head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Testcase for bug </title>
+<link rel="stylesheet" type="text/css" href=" " />
+<!--
+ #foo {
+ /* This doesn't get evaluated */
+ color: red;
+ }
+ #ie {
+ border: 5px solid red;
+ }
+ #ie {
+ display: block;
+ }
+ #moz {
+ color: blue;
+ /* display: none; */
+ }
+-->
+
+</head>
+<body>
+
+<p id="foo">foo</p>
+<p id="ie">ie</p>
+<p id="moz">moz</p>
+
+</body>
+</html>
diff --git a/dom/html/reftests/649134-ref.html b/dom/html/reftests/649134-ref.html
new file mode 100644
index 0000000000..2968464bed
--- /dev/null
+++ b/dom/html/reftests/649134-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html><head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Testcase for bug </title>
+</head>
+<body>
+
+<p id="foo">foo</p>
+<p id="ie">ie</p>
+<p id="moz">moz</p>
+
+</body>
+</html>
diff --git a/dom/html/reftests/741776-1-ref.html b/dom/html/reftests/741776-1-ref.html
new file mode 100644
index 0000000000..bc05ae4487
--- /dev/null
+++ b/dom/html/reftests/741776-1-ref.html
@@ -0,0 +1 @@
+<!DOCTYPE html><meta charset=utf-8><pre>ää
diff --git a/dom/html/reftests/741776-1.vtt b/dom/html/reftests/741776-1.vtt
new file mode 100644
index 0000000000..b66b11694c
--- /dev/null
+++ b/dom/html/reftests/741776-1.vtt
@@ -0,0 +1 @@
+ää
diff --git a/dom/html/reftests/82711-1-ref.html b/dom/html/reftests/82711-1-ref.html
new file mode 100644
index 0000000000..e0b25fc9b3
--- /dev/null
+++ b/dom/html/reftests/82711-1-ref.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+<body>
+<textarea rows="10" cols="25" wrap="off">
+ 0 1 2 3
+ 4 5
+
+ 6 7 8
+ 9
+
+
+ This is a long line that could wrap.
+</textarea>
+</body>
+</html>
diff --git a/dom/html/reftests/82711-1.html b/dom/html/reftests/82711-1.html
new file mode 100644
index 0000000000..70a8c1b23f
--- /dev/null
+++ b/dom/html/reftests/82711-1.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+<body>
+<textarea rows="10" cols="25" style="white-space: pre">
+ 0 1 2 3
+ 4 5
+
+ 6 7 8
+ 9
+
+
+ This is a long line that could wrap.
+</textarea>
+</body>
+</html>
diff --git a/dom/html/reftests/82711-2-ref.html b/dom/html/reftests/82711-2-ref.html
new file mode 100644
index 0000000000..963b9c7141
--- /dev/null
+++ b/dom/html/reftests/82711-2-ref.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html>
+<body>
+<textarea rows="10" cols="25" wrap="off" style="white-space: normal">
+ 0 1 2 3
+ 4 5
+
+ 6 7 8
+ 9
+
+
+ This is a long line that could wrap.
+</textarea>
+</body>
+</html>
diff --git a/dom/html/reftests/82711-2.html b/dom/html/reftests/82711-2.html
new file mode 100644
index 0000000000..aacd6d481e
--- /dev/null
+++ b/dom/html/reftests/82711-2.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<html>
+<body>
+<textarea rows="10" cols="25" style="white-space: normal">
+0 1 2 3 4 5 6 7 8 9 This is a long line that could wrap.
+</textarea>
+</body>
+</html>
diff --git a/dom/html/reftests/autofocus/autofocus-after-body-focus-ref.html b/dom/html/reftests/autofocus/autofocus-after-body-focus-ref.html
new file mode 100644
index 0000000000..3801ed7543
--- /dev/null
+++ b/dom/html/reftests/autofocus/autofocus-after-body-focus-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body onload="document.getElementsByTagName('input')[0].focus();">
+ <input onfocus="document.documentElement.removeAttribute('class');">
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/autofocus-after-body-focus.html b/dom/html/reftests/autofocus/autofocus-after-body-focus.html
new file mode 100644
index 0000000000..6d43b865a8
--- /dev/null
+++ b/dom/html/reftests/autofocus/autofocus-after-body-focus.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body>
+ <script>
+ document.body.focus();
+ </script>
+ <input autofocus onfocus="document.documentElement.removeAttribute('class');">
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/autofocus-after-load-ref.html b/dom/html/reftests/autofocus/autofocus-after-load-ref.html
new file mode 100644
index 0000000000..f28f06f0f3
--- /dev/null
+++ b/dom/html/reftests/autofocus/autofocus-after-load-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body>
+ <input autofocus><textarea></textarea><select></select><button></button>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/autofocus-after-load.html b/dom/html/reftests/autofocus/autofocus-after-load.html
new file mode 100644
index 0000000000..753ef183d1
--- /dev/null
+++ b/dom/html/reftests/autofocus/autofocus-after-load.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <script>
+ function loadHandler()
+ {
+ var body = document.body;
+
+ var elements = ["input", "textarea", "select", "button"];
+ for (var e of elements) {
+ var el = document.createElement(e);
+ el.autofocus = true;
+ body.appendChild(el);
+ }
+
+ setTimeout(document.documentElement.removeAttribute('class'), 0);
+ }
+ </script>
+ <body onload="loadHandler();">
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/autofocus-leaves-iframe-ref.html b/dom/html/reftests/autofocus/autofocus-leaves-iframe-ref.html
new file mode 100644
index 0000000000..e74e2753dc
--- /dev/null
+++ b/dom/html/reftests/autofocus/autofocus-leaves-iframe-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <script>
+ function loadHandler()
+ {
+ frames[0].document.getElementsByTagName('input')[0].onfocus = function() {
+ document.documentElement.removeAttribute('class');
+ }
+ frames[0].document.getElementsByTagName('input')[0].focus();
+ }
+ </script>
+ <body onload="loadHandler();">
+ <iframe srcdoc="<input>"></iframe>
+ <input></input>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/autofocus-leaves-iframe.html b/dom/html/reftests/autofocus/autofocus-leaves-iframe.html
new file mode 100644
index 0000000000..9069156878
--- /dev/null
+++ b/dom/html/reftests/autofocus/autofocus-leaves-iframe.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <script>
+ function frameLoadHandler()
+ {
+ var i = document.createElement('input');
+ i.autofocus = true;
+ document.body.appendChild(i);
+ setTimeout(document.documentElement.removeAttribute('class'), 0);
+ }
+ </script>
+ <body>
+ <iframe onload="frameLoadHandler();" srcdoc="<input autofocus>"></iframe>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/button-create.html b/dom/html/reftests/autofocus/button-create.html
new file mode 100644
index 0000000000..ae49d162ad
--- /dev/null
+++ b/dom/html/reftests/autofocus/button-create.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body>
+ <script>
+ var body = document.body;
+
+ var i = document.createElement('button');
+ i.autofocus = false;
+ body.appendChild(i);
+
+ i = document.createElement('button');
+ i.autofocus = true;
+ i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); };
+ body.appendChild(i);
+
+ i = document.createElement('button');
+ i.autofocus = true;
+ i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); };
+ body.appendChild(i);
+ </script>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/button-load.html b/dom/html/reftests/autofocus/button-load.html
new file mode 100644
index 0000000000..a9e28e2bb6
--- /dev/null
+++ b/dom/html/reftests/autofocus/button-load.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <script>
+ function focusHandler()
+ {
+ setTimeout(document.documentElement.removeAttribute('class'), 0);
+ }
+ </script>
+ <body>
+ <button></button><button autofocus onfocus="focusHandler();"></button><button autofocus onfocus="focusHandler();"></button>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/button-ref.html b/dom/html/reftests/autofocus/button-ref.html
new file mode 100644
index 0000000000..878c8e2681
--- /dev/null
+++ b/dom/html/reftests/autofocus/button-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body onload="document.getElementsByTagName('button')[1].focus();">
+ <button></button><button onfocus="document.documentElement.removeAttribute('class');"></button><button></button>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/input-create.html b/dom/html/reftests/autofocus/input-create.html
new file mode 100644
index 0000000000..c6d0c28089
--- /dev/null
+++ b/dom/html/reftests/autofocus/input-create.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body>
+ <script>
+ var body = document.body;
+
+ var i = document.createElement('input');
+ i.autofocus = false;
+ body.appendChild(i);
+
+ i = document.createElement('input');
+ i.autofocus = true;
+ i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); };
+ body.appendChild(i);
+
+ i = document.createElement('input');
+ i.autofocus = true;
+ i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); };
+ body.appendChild(i);
+ </script>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/input-load.html b/dom/html/reftests/autofocus/input-load.html
new file mode 100644
index 0000000000..d40b49177a
--- /dev/null
+++ b/dom/html/reftests/autofocus/input-load.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <script>
+ function focusHandler()
+ {
+ document.documentElement.removeAttribute('class');
+ }
+ </script>
+ <body>
+ <input><input autofocus onfocus="focusHandler();"><input autofocus onfocus="focusHandler();">
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/input-number-ref.html b/dom/html/reftests/autofocus/input-number-ref.html
new file mode 100644
index 0000000000..384915edb8
--- /dev/null
+++ b/dom/html/reftests/autofocus/input-number-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <!-- In this case we're using reftest-wait to make sure the test doesn't
+ get snapshotted before it's been focused. We're not testing
+ invalidation so we don't need to listen for MozReftestInvalidate.
+ -->
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body onload="document.getElementsByTagName('input')[0].focus();">
+ <input onfocus="document.documentElement.removeAttribute('class');"
+ style="-moz-appearance: none;">
+ <!-- div to cover spin box area for type=number to type=text comparison -->
+ <div style="display:block; position:absolute; background-color:black; width:200px; height:100px; top:0px; left:100px;">
+ </body>
+</html>
+
diff --git a/dom/html/reftests/autofocus/input-number.html b/dom/html/reftests/autofocus/input-number.html
new file mode 100644
index 0000000000..7816ee9bd9
--- /dev/null
+++ b/dom/html/reftests/autofocus/input-number.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <!-- In this case we're using reftest-wait to make sure the test doesn't
+ get snapshotted before it's been focused. We're not testing
+ invalidation so we don't need to listen for MozReftestInvalidate.
+ -->
+ <head>
+ <meta charset="utf-8">
+ <script>
+
+function focusHandler() {
+ setTimeout(function() {
+ document.documentElement.removeAttribute('class');
+ }, 0);
+}
+
+ </script>
+ </head>
+ <body>
+ <input type="number" autofocus onfocus="focusHandler();"
+ style="-moz-appearance: none;">
+ <!-- div to cover spin box area for type=number to type=text comparison -->
+ <div style="display:block; position:absolute; background-color:black; width:200px; height:100px; top:0px; left:100px;">
+ </body>
+</html>
+
diff --git a/dom/html/reftests/autofocus/input-ref.html b/dom/html/reftests/autofocus/input-ref.html
new file mode 100644
index 0000000000..6e2e546d24
--- /dev/null
+++ b/dom/html/reftests/autofocus/input-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body onload="document.getElementsByTagName('input')[1].focus();">
+ <input><input onfocus="document.documentElement.removeAttribute('class');"><input>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/input-time-ref.html b/dom/html/reftests/autofocus/input-time-ref.html
new file mode 100644
index 0000000000..abaa6feeac
--- /dev/null
+++ b/dom/html/reftests/autofocus/input-time-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <!-- In this case we're using reftest-wait to make sure the test doesn't
+ get snapshotted before it's been focused. We're not testing
+ invalidation so we don't need to listen for MozReftestInvalidate.
+ -->
+ <head>
+ <script>
+ function focusHandler() {
+ setTimeout(function() {
+ document.documentElement.removeAttribute("class");
+ }, 0);
+ }
+ </script>
+ </head>
+ <body onload="document.getElementById('t').focus();">
+ <input type="time" id="t" onfocus="focusHandler();"
+ style="-moz-appearance: none;">
+ </body>
+</html>
+
+
diff --git a/dom/html/reftests/autofocus/input-time.html b/dom/html/reftests/autofocus/input-time.html
new file mode 100644
index 0000000000..a86a91bbfc
--- /dev/null
+++ b/dom/html/reftests/autofocus/input-time.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <!-- In this case we're using reftest-wait to make sure the test doesn't
+ get snapshotted before it's been focused. We're not testing
+ invalidation so we don't need to listen for MozReftestInvalidate.
+ -->
+ <head>
+ <script>
+ function focusHandler() {
+ setTimeout(function() {
+ document.documentElement.removeAttribute("class");
+ }, 0);
+ }
+ </script>
+ </head>
+ <body>
+ <input type="time" autofocus onfocus="focusHandler();"
+ style="-moz-appearance: none;">
+ </body>
+</html>
+
+
diff --git a/dom/html/reftests/autofocus/reftest.list b/dom/html/reftests/autofocus/reftest.list
new file mode 100644
index 0000000000..03ddce4149
--- /dev/null
+++ b/dom/html/reftests/autofocus/reftest.list
@@ -0,0 +1,13 @@
+needs-focus == input-load.html input-ref.html
+needs-focus == input-create.html input-ref.html
+needs-focus == input-number.html input-number-ref.html
+fuzzy(0-5,0-1) needs-focus == input-time.html input-time-ref.html # One anti-aliased outline corner.
+needs-focus == button-load.html button-ref.html
+needs-focus == button-create.html button-ref.html
+fuzzy-if(gtkWidget,0-18,0-1) needs-focus == textarea-load.html textarea-ref.html # One anti-aliased corner.
+needs-focus == textarea-create.html textarea-ref.html
+fuzzy-if(Android,0-10,0-5) needs-focus == select-load.html select-ref.html
+fuzzy(0-10,0-5) needs-focus == select-create.html select-ref.html
+fuzzy(0-1,0-1) needs-focus == autofocus-after-load.html autofocus-after-load-ref.html
+needs-focus == autofocus-leaves-iframe.html autofocus-leaves-iframe-ref.html
+fuzzy(0-5,0-1) needs-focus == autofocus-after-body-focus.html autofocus-after-body-focus-ref.html
diff --git a/dom/html/reftests/autofocus/select-create.html b/dom/html/reftests/autofocus/select-create.html
new file mode 100644
index 0000000000..fd9c29c954
--- /dev/null
+++ b/dom/html/reftests/autofocus/select-create.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body>
+ <script>
+ var body = document.body;
+
+ var i = document.createElement('select');
+ i.autofocus = false;
+ body.appendChild(i);
+
+ i = document.createElement('select');
+ i.autofocus = true;
+ i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); };
+ body.appendChild(i);
+
+ i = document.createElement('select');
+ i.autofocus = true;
+ i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); };
+ body.appendChild(i);
+ </script>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/select-load.html b/dom/html/reftests/autofocus/select-load.html
new file mode 100644
index 0000000000..976005bec2
--- /dev/null
+++ b/dom/html/reftests/autofocus/select-load.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <script>
+ function focusHandler()
+ {
+ setTimeout(document.documentElement.removeAttribute('class'), 0);
+ }
+ </script>
+ <body>
+ <select></select><select autofocus onfocus="focusHandler();"></select><select autofocus onfocus="focusHandler();"></select>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/select-ref.html b/dom/html/reftests/autofocus/select-ref.html
new file mode 100644
index 0000000000..7fa9cd6559
--- /dev/null
+++ b/dom/html/reftests/autofocus/select-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body onload="document.getElementsByTagName('select')[1].focus();">
+ <select></select><select onfocus="document.documentElement.removeAttribute('class');"></select><select></select>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/style.css b/dom/html/reftests/autofocus/style.css
new file mode 100644
index 0000000000..f0dd5d1498
--- /dev/null
+++ b/dom/html/reftests/autofocus/style.css
@@ -0,0 +1,10 @@
+:focus { background-color: green; }
+
+/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1645773 */
+textarea { -moz-appearance: none; }
+
+/**
+ * autofocus is considered like a keyboard focus and .focus() isn't.
+ * We might change that with bug 620056 but for these tests, we don't really care.
+ */
+::-moz-focus-inner { border: none; }
diff --git a/dom/html/reftests/autofocus/textarea-create.html b/dom/html/reftests/autofocus/textarea-create.html
new file mode 100644
index 0000000000..e506bb2b72
--- /dev/null
+++ b/dom/html/reftests/autofocus/textarea-create.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body>
+ <script>
+ var body = document.body;
+
+ var i = document.createElement('textarea');
+ i.autofocus = false;
+ body.appendChild(i);
+
+ i = document.createElement('textarea');
+ i.autofocus = true;
+ i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); };
+ body.appendChild(i);
+
+ i = document.createElement('textarea');
+ i.autofocus = true;
+ i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); };
+ body.appendChild(i);
+ </script>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/textarea-load.html b/dom/html/reftests/autofocus/textarea-load.html
new file mode 100644
index 0000000000..13ab2cb2cc
--- /dev/null
+++ b/dom/html/reftests/autofocus/textarea-load.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <script>
+ function focusHandler()
+ {
+ setTimeout(document.documentElement.removeAttribute('class'), 0);
+ }
+ </script>
+ <body>
+ <textarea></textarea><textarea autofocus onfocus="focusHandler();"></textarea><textarea autofocus onfocus="focusHandler();"></textarea>
+ </body>
+</html>
diff --git a/dom/html/reftests/autofocus/textarea-ref.html b/dom/html/reftests/autofocus/textarea-ref.html
new file mode 100644
index 0000000000..b79bd7abe8
--- /dev/null
+++ b/dom/html/reftests/autofocus/textarea-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <link rel='stylesheet' type='text/css' href='style.css'>
+ <body onload="document.getElementsByTagName('textarea')[1].focus();">
+ <textarea></textarea><textarea onfocus="document.documentElement.removeAttribute('class');"></textarea><textarea></textarea>
+ </body>
+</html>
diff --git a/dom/html/reftests/body-frame-margin-remove-other-pres-hint-ref.html b/dom/html/reftests/body-frame-margin-remove-other-pres-hint-ref.html
new file mode 100644
index 0000000000..1e651db66d
--- /dev/null
+++ b/dom/html/reftests/body-frame-margin-remove-other-pres-hint-ref.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title></title>
+</head>
+<body>
+<script type="text/javascript">
+ function loadFrame() {
+ document.documentElement.className = "";
+ }
+</script>
+<iframe id=frame onload="loadFrame()" srcdoc="<body><span lang='en'>text</span></body>" marginwidth="100px" marginheight="100px" width=300px height=300px></iframe>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/html/reftests/body-frame-margin-remove-other-pres-hint.html b/dom/html/reftests/body-frame-margin-remove-other-pres-hint.html
new file mode 100644
index 0000000000..16428813af
--- /dev/null
+++ b/dom/html/reftests/body-frame-margin-remove-other-pres-hint.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title></title>
+</head>
+<body>
+<script type="text/javascript">
+ function loadFrame() {
+ let frame = document.getElementById('frame');
+ frame.contentDocument.body.removeAttribute('lang');
+ document.documentElement.className = "";
+ }
+</script>
+<iframe id=frame onload="loadFrame()" srcdoc="<body lang='en'>text</body>" marginwidth="100px" marginheight="100px" width=300px height=300px></iframe>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/html/reftests/body-topmargin-dynamic.html b/dom/html/reftests/body-topmargin-dynamic.html
new file mode 100644
index 0000000000..e6c8c505e7
--- /dev/null
+++ b/dom/html/reftests/body-topmargin-dynamic.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+this text should have a margin of 100px on the top and left
+<p style="direction: rtl">this text should have a margin of 100px on the right</p>
+<script type="text/javascript">
+ document.body.setAttribute("topmargin", "100px");
+ document.body.setAttribute("leftmargin", "100px");
+ document.body.setAttribute("rightmargin", "100px");
+</script>
+</body>
+</html>
diff --git a/dom/html/reftests/body-topmargin-ref.html b/dom/html/reftests/body-topmargin-ref.html
new file mode 100644
index 0000000000..6530a0ae4b
--- /dev/null
+++ b/dom/html/reftests/body-topmargin-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<body topmargin="100px" leftmargin="100px" rightmargin="100px">
+this text should have a margin of 100px on the top and left
+<p style="direction: rtl">this text should have a margin of 100px on the right</p>
+</body>
+</html>
diff --git a/dom/html/reftests/bug1106522-1.html b/dom/html/reftests/bug1106522-1.html
new file mode 100644
index 0000000000..db07c1010e
--- /dev/null
+++ b/dom/html/reftests/bug1106522-1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<body>
+ <picture>
+ <source srcset="lime100x100.svg" type="image/svg+xml">
+ <img src="red.png" width="100" height="100">
+ </picture>
+</body>
+</html>
diff --git a/dom/html/reftests/bug1106522-2.html b/dom/html/reftests/bug1106522-2.html
new file mode 100644
index 0000000000..15520982fc
--- /dev/null
+++ b/dom/html/reftests/bug1106522-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<body>
+ <picture>
+ <source srcset="lime100x100.svg">
+ <img src="red.png" width="100" height="100">
+ </picture>
+</body>
+</html>
diff --git a/dom/html/reftests/bug1106522-ref.html b/dom/html/reftests/bug1106522-ref.html
new file mode 100644
index 0000000000..476c47c12d
--- /dev/null
+++ b/dom/html/reftests/bug1106522-ref.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<body>
+ <img src="lime100x100.svg">
+</body>
+</html>
diff --git a/dom/html/reftests/bug1196784-no-srcset.html b/dom/html/reftests/bug1196784-no-srcset.html
new file mode 100644
index 0000000000..df55d48631
--- /dev/null
+++ b/dom/html/reftests/bug1196784-no-srcset.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <img height="100" width="100" src="bug1196784.png">
+</body>
+</html>
diff --git a/dom/html/reftests/bug1196784-with-srcset.html b/dom/html/reftests/bug1196784-with-srcset.html
new file mode 100644
index 0000000000..1cd77bad9b
--- /dev/null
+++ b/dom/html/reftests/bug1196784-with-srcset.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <img height="100" width="100" src="bug1196784.png" srcset="bug1196784.png">
+</body>
+</html>
diff --git a/dom/html/reftests/bug1196784.png b/dom/html/reftests/bug1196784.png
new file mode 100644
index 0000000000..8d0ed56825
--- /dev/null
+++ b/dom/html/reftests/bug1196784.png
Binary files differ
diff --git a/dom/html/reftests/bug1228601-video-rotated-ref.html b/dom/html/reftests/bug1228601-video-rotated-ref.html
new file mode 100644
index 0000000000..e489c9a75b
--- /dev/null
+++ b/dom/html/reftests/bug1228601-video-rotated-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+function done() {
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+<body onload="setTimeout(done, 3);">
+ <video src="video_rotated.mp4" onended="done()" autoplay="true">
+</body>
+</html>
diff --git a/dom/html/reftests/bug1228601-video-rotation-90.html b/dom/html/reftests/bug1228601-video-rotation-90.html
new file mode 100644
index 0000000000..94c57d7504
--- /dev/null
+++ b/dom/html/reftests/bug1228601-video-rotation-90.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+function done() {
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+<body onload="setTimeout(done, 3);">
+ <video src="video_rotation_90.mp4" onended="done()" autoplay="true">
+</body>
+</html>
diff --git a/dom/html/reftests/bug1423850-canvas-video-rotated-ref.html b/dom/html/reftests/bug1423850-canvas-video-rotated-ref.html
new file mode 100644
index 0000000000..8927b6e9f7
--- /dev/null
+++ b/dom/html/reftests/bug1423850-canvas-video-rotated-ref.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head><script>
+function done() {
+ let video = document.querySelector("video");
+ let canvas = document.querySelector("canvas");
+ let context = canvas.getContext("2d");
+ context.drawImage(video, 30, 50, video.videoWidth, video.videoHeight);
+ document.documentElement.removeAttribute("class");
+}
+</script></head>
+<body bgcolor="gray">
+ <video src="video_rotated.mp4" onended="done()" autoplay="true"></video>
+ <canvas width="60" height="100"></canvas>
+</body>
+</html>
diff --git a/dom/html/reftests/bug1423850-canvas-video-rotation-90.html b/dom/html/reftests/bug1423850-canvas-video-rotation-90.html
new file mode 100644
index 0000000000..5039e64afa
--- /dev/null
+++ b/dom/html/reftests/bug1423850-canvas-video-rotation-90.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head><script>
+function done() {
+ let video = document.querySelector("video");
+ let canvas = document.querySelector("canvas");
+ let context = canvas.getContext("2d");
+ context.drawImage(video, 30, 50, video.videoWidth, video.videoHeight);
+ document.documentElement.removeAttribute("class");
+}
+</script></head>
+<body bgcolor="gray">
+ <video src="video_rotation_90.mp4" onended="done()" autoplay="true"></video>
+ <canvas width="60" height="100"></canvas>
+</body>
+</html>
diff --git a/dom/html/reftests/bug1512297-ref.html b/dom/html/reftests/bug1512297-ref.html
new file mode 100644
index 0000000000..45026e86cc
--- /dev/null
+++ b/dom/html/reftests/bug1512297-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<head></head>
+<body>
+<div><img src="" alt="ALT"></div>
+</body>
+</html>
diff --git a/dom/html/reftests/bug1512297.html b/dom/html/reftests/bug1512297.html
new file mode 100644
index 0000000000..55d8d4564f
--- /dev/null
+++ b/dom/html/reftests/bug1512297.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head class="reftest-wait"></head>
+<body>
+<div><img src="" alt="ALT"></div>
+<script>
+var img = document.querySelector('img');
+img.remove();
+
+var div = document.querySelector('div');
+div.appendChild(img);
+</script>
+</body>
+</html>
diff --git a/dom/html/reftests/bug448564-1_ideal.html b/dom/html/reftests/bug448564-1_ideal.html
new file mode 100644
index 0000000000..e93c1771f6
--- /dev/null
+++ b/dom/html/reftests/bug448564-1_ideal.html
@@ -0,0 +1,13 @@
+<html>
+<head>
+ <link rel="stylesheet" type="text/css"
+ href="bug448564_forms.css">
+ </link>
+</head>
+<body>
+ <i><b>
+ <form>a</form>
+ <form>b</form>
+ </b></i>
+</body>
+</html>
diff --git a/dom/html/reftests/bug448564-1_malformed.html b/dom/html/reftests/bug448564-1_malformed.html
new file mode 100644
index 0000000000..404517c70e
--- /dev/null
+++ b/dom/html/reftests/bug448564-1_malformed.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+ <link rel="stylesheet" type="text/css"
+ href="bug448564_forms.css">
+ </link>
+</head>
+<body>
+ <i><b>
+ <form>a</form> <!-- These forms should not end up nested! -->
+ <form>b</form>
+ <!-- Why does it matter whether we explicitly close this tag? -->
+ <!-- It matters because nsHTMLTokenizer::ScanDocStructure checks
+ whether there are any malformed tags before parsing begins,
+ and, if there are any, residual style tags (<i>, <b>, &c.)
+ must be pushed inside block elements (e.g., <form>). -->
+ <div><!-- </div> -->
+ </b></i>
+</body>
+</html>
diff --git a/dom/html/reftests/bug448564-1_well-formed.html b/dom/html/reftests/bug448564-1_well-formed.html
new file mode 100644
index 0000000000..46dbb8bdd0
--- /dev/null
+++ b/dom/html/reftests/bug448564-1_well-formed.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+ <link rel="stylesheet" type="text/css"
+ href="bug448564_forms.css">
+ </link>
+</head>
+<body>
+ <form><i><b>a</b></i></form>
+ <form><i><b>b</b></i></form>
+</body>
+</html>
diff --git a/dom/html/reftests/bug448564-4a.html b/dom/html/reftests/bug448564-4a.html
new file mode 100644
index 0000000000..6fbaf85c2f
--- /dev/null
+++ b/dom/html/reftests/bug448564-4a.html
@@ -0,0 +1,10 @@
+<html>
+<body>
+ <b><i>
+ <!-- Closing a form causes any open residual style tags to be closed
+ as well. This test ensures that these tags get reopened. -->
+ <form>form contents</form>
+ bold text
+ </i></b>
+</body>
+</html>
diff --git a/dom/html/reftests/bug448564-4b.html b/dom/html/reftests/bug448564-4b.html
new file mode 100644
index 0000000000..f04d5fe48f
--- /dev/null
+++ b/dom/html/reftests/bug448564-4b.html
@@ -0,0 +1,6 @@
+<html>
+<body>
+ <form><b><i>form contents</i></b></form>
+ <b><i>bold text</i></b>
+</body>
+</html>
diff --git a/dom/html/reftests/bug448564_forms.css b/dom/html/reftests/bug448564_forms.css
new file mode 100644
index 0000000000..b98788862c
--- /dev/null
+++ b/dom/html/reftests/bug448564_forms.css
@@ -0,0 +1,2 @@
+/* make nesting obvious */
+form { border: 1px solid black; }
diff --git a/dom/html/reftests/bug502168-1_malformed.html b/dom/html/reftests/bug502168-1_malformed.html
new file mode 100644
index 0000000000..efe23ac47f
--- /dev/null
+++ b/dom/html/reftests/bug502168-1_malformed.html
@@ -0,0 +1,10 @@
+<html><head>
+<title> Bug 502168 - Particular images are displayed multiple times in a formated way - only FF 3.5</title>
+</head><body>
+
+<table><tbody><tr><td >You should see this text only once</td>
+<embed type="*" style="display: none;"/>
+</td></tr></tbody></table>
+
+</body>
+</html>
diff --git a/dom/html/reftests/bug502168-1_well-formed.html b/dom/html/reftests/bug502168-1_well-formed.html
new file mode 100644
index 0000000000..5eb25c6b35
--- /dev/null
+++ b/dom/html/reftests/bug502168-1_well-formed.html
@@ -0,0 +1,9 @@
+<html><head>
+<title> Bug 502168 - Particular images are displayed multiple times in a formated way - only FF 3.5</title>
+</head><body>
+
+<embed type="*" style="display: none;">
+<table><tbody><tr><td>You should see this text only once</td>
+</tr></tbody></table>
+
+</body></html>
diff --git a/dom/html/reftests/bug917595-1-ref.html b/dom/html/reftests/bug917595-1-ref.html
new file mode 100644
index 0000000000..b777751ff8
--- /dev/null
+++ b/dom/html/reftests/bug917595-1-ref.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<style>
+ iframe {
+ width: 100%;
+ height: 100%;
+ border: 0px;
+ }
+</style>
+<script>
+ document.addEventListener('MozReftestInvalidate',
+ () => document.documentElement.removeAttribute('class'));
+</script>
+<body>
+ <iframe src="bug917595-pixel-rotated.jpg" scrolling="no" marginwidth="0" marginheight="0"></iframe>
+</body>
+</html>
diff --git a/dom/html/reftests/bug917595-exif-rotated.jpg b/dom/html/reftests/bug917595-exif-rotated.jpg
new file mode 100644
index 0000000000..e7b0c22f35
--- /dev/null
+++ b/dom/html/reftests/bug917595-exif-rotated.jpg
Binary files differ
diff --git a/dom/html/reftests/bug917595-iframe-1.html b/dom/html/reftests/bug917595-iframe-1.html
new file mode 100644
index 0000000000..f7fca8232c
--- /dev/null
+++ b/dom/html/reftests/bug917595-iframe-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<style>
+ iframe {
+ width: 100%;
+ height: 100%;
+ border: 0px;
+ }
+</style>
+<script>
+ document.addEventListener('MozReftestInvalidate',
+ () => document.documentElement.removeAttribute('class'));
+</script>
+<body>
+ <iframe src="bug917595-exif-rotated.jpg" scrolling="no" marginwidth="0" marginheight="0"></iframe>
+</body>
+</html>
diff --git a/dom/html/reftests/bug917595-pixel-rotated.jpg b/dom/html/reftests/bug917595-pixel-rotated.jpg
new file mode 100644
index 0000000000..ac39faadad
--- /dev/null
+++ b/dom/html/reftests/bug917595-pixel-rotated.jpg
Binary files differ
diff --git a/dom/html/reftests/bug917595-unrotated.jpg b/dom/html/reftests/bug917595-unrotated.jpg
new file mode 100644
index 0000000000..a787797c5e
--- /dev/null
+++ b/dom/html/reftests/bug917595-unrotated.jpg
Binary files differ
diff --git a/dom/html/reftests/figure-ref.html b/dom/html/reftests/figure-ref.html
new file mode 100644
index 0000000000..23ca9f6037
--- /dev/null
+++ b/dom/html/reftests/figure-ref.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<title>The figure element</title>
+<link rel=author title=Ms2ger href=ms2ger@gmail.com>
+<link rel=help href=http://www.whatwg.org/html5/#the-figure-element>
+<style>
+body > div { margin: 1em 40px; }
+</style>
+<div>
+<div>Caption</div>
+Figure
+</div>
diff --git a/dom/html/reftests/figure.html b/dom/html/reftests/figure.html
new file mode 100644
index 0000000000..ad83670b80
--- /dev/null
+++ b/dom/html/reftests/figure.html
@@ -0,0 +1,8 @@
+<!doctype html>
+<title>The figure element</title>
+<link rel=author title=Ms2ger href=ms2ger@gmail.com>
+<link rel=help href=http://www.whatwg.org/html5/#the-figure-element>
+<figure>
+<figcaption>Caption</figcaption>
+Figure
+</figure>
diff --git a/dom/html/reftests/href-attr-change-restyles-ref.html b/dom/html/reftests/href-attr-change-restyles-ref.html
new file mode 100644
index 0000000000..4ebaec9249
--- /dev/null
+++ b/dom/html/reftests/href-attr-change-restyles-ref.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for bug 549797 - Removing href attribute doesn't remove link styling</title>
+ <style type="text/css">
+ :link, :visited {
+ color:blue;
+ }
+ link {
+ display:block;
+ }
+ #link2::before {
+ content:"Test link 1";
+ }
+ #link4::before {
+ content:"Test link 2";
+ }
+ #link6::before {
+ content:"Test link 3";
+ }
+ </style>
+</head>
+<body>
+<p>
+ <a>Test anchor 1</a>
+ <link id="link2"/>
+ <a href="http://example.com/1">Test anchor 2</a>
+ <link id="link4" href="http://example.com/1"/>
+ <a href="">Test anchor 3</a>
+ <link id="link6" href=""/>
+</p>
+</body>
+</html>
diff --git a/dom/html/reftests/href-attr-change-restyles.html b/dom/html/reftests/href-attr-change-restyles.html
new file mode 100644
index 0000000000..1fa54bfd6f
--- /dev/null
+++ b/dom/html/reftests/href-attr-change-restyles.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for bug 549797 - Removing href attribute doesn't remove link styling</title>
+ <style type="text/css">
+ :link, :visited {
+ color:blue;
+ }
+ link {
+ display:block;
+ }
+ #link2::before {
+ content:"Test link 1";
+ }
+ #link4::before {
+ content:"Test link 2";
+ }
+ #link6::before {
+ content:"Test link 3";
+ }
+ </style>
+</head>
+<body onload="run_test();">
+<script type="text/javascript">
+function run_test()
+{
+ // Remove the href attributes of the links so they should be restyled as
+ // non-links.
+ document.getElementById("link1").removeAttribute("href");
+ document.getElementById("link2").removeAttribute("href");
+
+ // Add the href attribute to the links so they should be restyled as links.
+ document.getElementById("link3").href = "http://example.com/1";
+ document.getElementById("link4").href = "http://example.com/1";
+ document.getElementById("link5").setAttribute("href", "");
+ document.getElementById("link6").setAttribute("href", "");
+}
+</script>
+<p>
+ <a id="link1" href="http://example.com/1">Test anchor 1</a>
+ <link id="link2" href="http://example.com/1"/>
+ <a id="link3">Test anchor 2</a>
+ <link id="link4"/>
+ <a id="link5">Test anchor 3</a>
+ <link id="link6"/>
+</p>
+</body>
+</html>
diff --git a/dom/html/reftests/iframe-with-image-src.html b/dom/html/reftests/iframe-with-image-src.html
new file mode 100644
index 0000000000..554abc60db
--- /dev/null
+++ b/dom/html/reftests/iframe-with-image-src.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <iframe height="100" width="100" style="border: none;" src="bug1196784.png"></iframe>
+</body>
+</html>
diff --git a/dom/html/reftests/image-load-shortcircuit-1.html b/dom/html/reftests/image-load-shortcircuit-1.html
new file mode 100644
index 0000000000..28e16b7464
--- /dev/null
+++ b/dom/html/reftests/image-load-shortcircuit-1.html
@@ -0,0 +1,8 @@
+<html>
+<div></div>
+<script>
+ var d = (new DOMParser()).parseFromString("<img src=pass.png>", "text/html");
+ var n = d.adoptNode(d.querySelector('img'));
+ document.querySelector('div').appendChild(n);
+</script>
+</html>
diff --git a/dom/html/reftests/image-load-shortcircuit-2.html b/dom/html/reftests/image-load-shortcircuit-2.html
new file mode 100644
index 0000000000..3c4baa43be
--- /dev/null
+++ b/dom/html/reftests/image-load-shortcircuit-2.html
@@ -0,0 +1,10 @@
+<html>
+<body>
+<template id="template">
+<img src="pass.png" alt="Alt Text" />
+</template>
+<script>
+ document.body.appendChild(document.getElementById('template').content.children[0].cloneNode(1));
+</script>
+</body>
+</html>
diff --git a/dom/html/reftests/image-load-shortcircuit-ref.html b/dom/html/reftests/image-load-shortcircuit-ref.html
new file mode 100644
index 0000000000..7dd28922d4
--- /dev/null
+++ b/dom/html/reftests/image-load-shortcircuit-ref.html
@@ -0,0 +1 @@
+<div><img src=pass.png></div>
diff --git a/dom/html/reftests/lime100x100.svg b/dom/html/reftests/lime100x100.svg
new file mode 100644
index 0000000000..8bdec62c1f
--- /dev/null
+++ b/dom/html/reftests/lime100x100.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
+ width="100" height="100">
+ <rect width="100%" height="100%" fill="lime"/>
+</svg>
diff --git a/dom/html/reftests/pass.png b/dom/html/reftests/pass.png
new file mode 100644
index 0000000000..3b30b1de7c
--- /dev/null
+++ b/dom/html/reftests/pass.png
Binary files differ
diff --git a/dom/html/reftests/pre-1-ref.html b/dom/html/reftests/pre-1-ref.html
new file mode 100644
index 0000000000..a79b4f46a4
--- /dev/null
+++ b/dom/html/reftests/pre-1-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<div style="width: 15em">
+<pre>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre wrap>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre wrap>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre wrap>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+</div>
+12
diff --git a/dom/html/reftests/pre-1.html b/dom/html/reftests/pre-1.html
new file mode 100644
index 0000000000..1b21bcd746
--- /dev/null
+++ b/dom/html/reftests/pre-1.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<div style="width: 15em">
+<pre>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre width=12>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre cols=12>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre wrap>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre wrap width=12>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+<pre wrap cols=12>
+MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM
+</pre>
+</div>
+<script>document.write(document.querySelectorAll('pre')[1].width);</script>
diff --git a/dom/html/reftests/red.png b/dom/html/reftests/red.png
new file mode 100644
index 0000000000..aa9ce25263
--- /dev/null
+++ b/dom/html/reftests/red.png
Binary files differ
diff --git a/dom/html/reftests/reftest.list b/dom/html/reftests/reftest.list
new file mode 100644
index 0000000000..77dd250af9
--- /dev/null
+++ b/dom/html/reftests/reftest.list
@@ -0,0 +1,78 @@
+# autofocus attribute (we can't test with mochitests)
+include autofocus/reftest.list
+include toblob-todataurl/reftest.list
+
+== 41464-1a.html 41464-1-ref.html
+== 41464-1b.html 41464-1-ref.html
+== 52019-1.html 52019-1-ref.html
+== 82711-1.html 82711-1-ref.html
+== 82711-2.html 82711-2-ref.html
+!= 82711-1-ref.html 82711-2-ref.html
+!= 468263-1a.html about:blank
+!= 468263-1b.html about:blank
+!= 468263-1c.html about:blank
+!= 468263-1d.html about:blank
+== 468263-2.html 468263-2-ref.html
+== 468263-2.html 468263-2-alternate-ref.html
+== 484200-1.html 484200-1-ref.html
+== 485377.html 485377-ref.html
+== 557840.html 557840-ref.html
+== 560059-video-dimensions.html 560059-video-dimensions-ref.html
+== 573322-quirks.html 573322-quirks-ref.html
+== 573322-no-quirks.html 573322-no-quirks-ref.html
+== 596455-1a.html 596455-ref-1.html
+== 596455-1b.html 596455-ref-1.html
+== 596455-2a.html 596455-ref-2.html
+== 596455-2b.html 596455-ref-2.html
+== 610935.html 610935-ref.html
+== 649134-1.html 649134-ref.html
+skip-if(Android) == 649134-2.html 649134-2-ref.html
+== 741776-1.vtt 741776-1-ref.html
+
+== bug448564-1_malformed.html bug448564-1_well-formed.html
+== bug448564-1_malformed.html bug448564-1_ideal.html
+
+== bug448564-4a.html bug448564-4b.html
+== bug502168-1_malformed.html bug502168-1_well-formed.html
+
+== responsive-image-load-shortcircuit.html responsive-image-load-shortcircuit-ref.html
+== image-load-shortcircuit-1.html image-load-shortcircuit-ref.html
+== image-load-shortcircuit-2.html image-load-shortcircuit-ref.html
+
+# Test that image documents taken into account CSS properties like
+# image-orientation when determining the size of the image.
+# (Fuzzy necessary due to pixel-wise comparison of different JPEGs.
+# The vast majority of the fuzziness comes from Linux and WinXP.)
+skip-if(isCoverageBuild) fuzzy(0-2,0-830) random-if(useDrawSnapshot) == bug917595-iframe-1.html bug917595-1-ref.html
+fuzzy(0-3,0-7544) fuzzy-if(!geckoview,2-3,50-7544) == bug917595-exif-rotated.jpg bug917595-pixel-rotated.jpg # bug 1060869
+
+# Test support for SVG-as-image in <picture> elements.
+== bug1106522-1.html bug1106522-ref.html
+== bug1106522-2.html bug1106522-ref.html
+
+== href-attr-change-restyles.html href-attr-change-restyles-ref.html
+== figure.html figure-ref.html
+== pre-1.html pre-1-ref.html
+== table-border-1.html table-border-1-ref.html
+== table-border-2.html table-border-2-ref.html
+!= table-border-2.html table-border-2-notref.html
+
+# Test imageset is using permissions.default.image
+pref(permissions.default.image,1) HTTP == bug1196784-with-srcset.html bug1196784-no-srcset.html
+pref(permissions.default.image,2) HTTP == bug1196784-with-srcset.html bug1196784-no-srcset.html
+# Test <iframe src=image>
+pref(permissions.default.image,1) HTTP == iframe-with-image-src.html bug1196784-no-srcset.html
+pref(permissions.default.image,2) HTTP == iframe-with-image-src.html about:blank
+
+# Test video with rotation information can be rotated.
+fails-if(geckoview&&!swgl) == bug1228601-video-rotation-90.html bug1228601-video-rotated-ref.html # bug 1558285 for geckoview
+fuzzy(0-1,0-30) fails-if(geckoview&&!swgl) random-if(geckoview&&swgl) == bug1423850-canvas-video-rotation-90.html bug1423850-canvas-video-rotated-ref.html # bug 1558285 for geckoview
+
+== bug1512297.html bug1512297-ref.html
+
+# Test that dynamically setting body margin attributes updates style appropriately
+== body-topmargin-dynamic.html body-topmargin-ref.html
+
+# Test that dynamically removing a nonmargin mapped attribute does not
+# destroy margins inherited from the frame.
+== body-frame-margin-remove-other-pres-hint.html body-frame-margin-remove-other-pres-hint-ref.html
diff --git a/dom/html/reftests/responsive-image-load-shortcircuit-ref.html b/dom/html/reftests/responsive-image-load-shortcircuit-ref.html
new file mode 100644
index 0000000000..59d8925ba5
--- /dev/null
+++ b/dom/html/reftests/responsive-image-load-shortcircuit-ref.html
@@ -0,0 +1 @@
+<iframe srcdoc="<img src=pass.png>" width="300px"></iframe>
diff --git a/dom/html/reftests/responsive-image-load-shortcircuit.html b/dom/html/reftests/responsive-image-load-shortcircuit.html
new file mode 100644
index 0000000000..1cfb92cb20
--- /dev/null
+++ b/dom/html/reftests/responsive-image-load-shortcircuit.html
@@ -0,0 +1,15 @@
+<html class="reftest-wait">
+<iframe srcdoc="<img srcset=red.png>" width="150px"></iframe>
+<script>
+ var iframe = document.querySelector('iframe');
+ iframe.onload = function() {
+ var doc = iframe.contentDocument;
+ var img = doc.querySelector('img');
+ img.srcset = "pass.png";
+ iframe.width = "300px";
+ img.onload = function() {
+ document.documentElement.classList.remove('reftest-wait');
+ };
+ }
+</script>
+</html>
diff --git a/dom/html/reftests/table-border-1-ref.html b/dom/html/reftests/table-border-1-ref.html
new file mode 100644
index 0000000000..ceac88e9a3
--- /dev/null
+++ b/dom/html/reftests/table-border-1-ref.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Table borders</title>
+<style>
+table {
+ border-width: 1px;
+ border-style: outset;
+}
+td {
+ border-width: 1px;
+ border-style: inset;
+}
+</style>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
diff --git a/dom/html/reftests/table-border-1.html b/dom/html/reftests/table-border-1.html
new file mode 100644
index 0000000000..12bfb2af46
--- /dev/null
+++ b/dom/html/reftests/table-border-1.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Table borders</title>
+<table border>
+<tr><td>Test
+</table>
+<table border="">
+<tr><td>Test
+</table>
+<table border=null>
+<tr><td>Test
+</table>
+<table border=undefined>
+<tr><td>Test
+</table>
+<table border=foo>
+<tr><td>Test
+</table>
+<table border=1>
+<tr><td>Test
+</table>
+<table border=1foo>
+<tr><td>Test
+</table>
+<table border=1%>
+<tr><td>Test
+</table>
+<table border=-1>
+<tr><td>Test
+</table>
+<table border=-1foo>
+<tr><td>Test
+</table>
+<table border=-1%>
+<tr><td>Test
+</table>
diff --git a/dom/html/reftests/table-border-2-notref.html b/dom/html/reftests/table-border-2-notref.html
new file mode 100644
index 0000000000..7558e5271a
--- /dev/null
+++ b/dom/html/reftests/table-border-2-notref.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Table borders</title>
+<style>
+table {
+ border-width: 1px;
+ border-style: outset;
+}
+td {
+ border-width: 1px;
+ border-style: inset;
+}
+</style>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
diff --git a/dom/html/reftests/table-border-2-ref.html b/dom/html/reftests/table-border-2-ref.html
new file mode 100644
index 0000000000..36d1e45106
--- /dev/null
+++ b/dom/html/reftests/table-border-2-ref.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Table borders</title>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
+<table>
+<tr><td>Test
+</table>
diff --git a/dom/html/reftests/table-border-2.html b/dom/html/reftests/table-border-2.html
new file mode 100644
index 0000000000..4f209545c2
--- /dev/null
+++ b/dom/html/reftests/table-border-2.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Table borders</title>
+<table border=0>
+<tr><td>Test
+</table>
+<table border=0foo>
+<tr><td>Test
+</table>
+<table border=0%>
+<tr><td>Test
+</table>
+<table border=+0>
+<tr><td>Test
+</table>
+<table border=+0foo>
+<tr><td>Test
+</table>
+<table border=+0%>
+<tr><td>Test
+</table>
+<table border=-0>
+<tr><td>Test
+</table>
+<table border=-0foo>
+<tr><td>Test
+</table>
+<table border=-0%>
+<tr><td>Test
+</table>
diff --git a/dom/html/reftests/toblob-todataurl/blob.js b/dom/html/reftests/toblob-todataurl/blob.js
new file mode 100644
index 0000000000..4ed9fdb372
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/blob.js
@@ -0,0 +1,68 @@
+function init() {
+ function end() {
+ document.documentElement.className = '';
+ }
+
+ function next() {
+ compressAndDisplay(original, end);
+ }
+
+ var original = getImageFromDataUrl(sample);
+ setImgLoadListener(original, next);
+}
+
+function compressAndDisplay(image, next) {
+ var canvas = document.createElement('canvas');
+ canvas.width = image.naturalWidth;
+ canvas.height = image.naturalHeight;
+ var ctx = canvas.getContext('2d');
+ ctx.drawImage(image, 0, 0);
+
+ function gotBlob(blob) {
+ var img = getImageFromBlob(blob);
+ setImgLoadListener(img, next);
+ document.body.appendChild(img);
+ }
+
+ // I want to test passing 'undefined' as quality as well
+ if ('quality' in window) {
+ canvas.toBlob(gotBlob, 'image/jpeg', quality);
+ } else {
+ canvas.toBlob(gotBlob, 'image/jpeg');
+ }
+}
+
+function setImgLoadListener(img, func) {
+ if (img.complete) {
+ func.call(img, { target: img});
+ } else {
+ img.addEventListener('load', func);
+ }
+}
+
+function naturalDimensionsHandler(e) {
+ var img = e.target;
+ img.width = img.naturalWidth;
+ img.height = img.naturalHeight;
+}
+
+function getImageFromBlob(blob) {
+ var img = document.createElement('img');
+ img.src = window.URL.createObjectURL(blob);
+ setImgLoadListener(img, naturalDimensionsHandler);
+ setImgLoadListener(img, function(e) {
+ window.URL.revokeObjectURL(e.target.src);
+ });
+
+ return img;
+}
+
+function getImageFromDataUrl(url) {
+ var img = document.createElement('img');
+ img.src = url;
+ setImgLoadListener(img, naturalDimensionsHandler);
+
+ return img;
+}
+
+init();
diff --git a/dom/html/reftests/toblob-todataurl/dataurl.js b/dom/html/reftests/toblob-todataurl/dataurl.js
new file mode 100644
index 0000000000..8ffba1fa8e
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/dataurl.js
@@ -0,0 +1,56 @@
+function init() {
+ function end() {
+ document.documentElement.className = '';
+ }
+
+ function next() {
+ compressAndDisplay(original, end);
+ }
+
+ var original = getImageFromDataUrl(sample);
+ setImgLoadListener(original, next);
+}
+
+function compressAndDisplay(image, next) {
+ var canvas = document.createElement('canvas');
+ canvas.width = image.naturalWidth;
+ canvas.height = image.naturalHeight;
+ var ctx = canvas.getContext('2d');
+ ctx.drawImage(image, 0, 0);
+
+ var dataUrl;
+ // I want to test passing undefined as well
+ if ('quality' in window) {
+ dataUrl = canvas.toDataURL('image/jpeg', quality);
+ } else {
+ dataUrl = canvas.toDataURL('image/jpeg');
+ }
+
+ var img = getImageFromDataUrl(dataUrl);
+ setImgLoadListener(img, next);
+ document.body.appendChild(img);
+}
+
+function setImgLoadListener(img, func) {
+ if (img.complete) {
+ func.call(img, { target: img});
+ } else {
+ img.addEventListener('load', func);
+ }
+}
+
+function naturalDimensionsHandler(e) {
+ var img = e.target;
+ img.width = img.naturalWidth;
+ img.height = img.naturalHeight;
+}
+
+function getImageFromDataUrl(url) {
+ var img = document.createElement('img');
+ img.src = url;
+ setImgLoadListener(img, naturalDimensionsHandler);
+
+ return img;
+}
+
+init();
diff --git a/dom/html/reftests/toblob-todataurl/images/original.png b/dom/html/reftests/toblob-todataurl/images/original.png
new file mode 100644
index 0000000000..c2da5b3597
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/images/original.png
Binary files differ
diff --git a/dom/html/reftests/toblob-todataurl/images/q0.jpg b/dom/html/reftests/toblob-todataurl/images/q0.jpg
new file mode 100644
index 0000000000..eb41ad3e93
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/images/q0.jpg
Binary files differ
diff --git a/dom/html/reftests/toblob-todataurl/images/q100.jpg b/dom/html/reftests/toblob-todataurl/images/q100.jpg
new file mode 100644
index 0000000000..aaa79f2d31
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/images/q100.jpg
Binary files differ
diff --git a/dom/html/reftests/toblob-todataurl/images/q25.jpg b/dom/html/reftests/toblob-todataurl/images/q25.jpg
new file mode 100644
index 0000000000..d8b1c9bfb2
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/images/q25.jpg
Binary files differ
diff --git a/dom/html/reftests/toblob-todataurl/images/q50.jpg b/dom/html/reftests/toblob-todataurl/images/q50.jpg
new file mode 100644
index 0000000000..f93356ef22
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/images/q50.jpg
Binary files differ
diff --git a/dom/html/reftests/toblob-todataurl/images/q75.jpg b/dom/html/reftests/toblob-todataurl/images/q75.jpg
new file mode 100644
index 0000000000..6c25c55a1a
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/images/q75.jpg
Binary files differ
diff --git a/dom/html/reftests/toblob-todataurl/images/q92.jpg b/dom/html/reftests/toblob-todataurl/images/q92.jpg
new file mode 100644
index 0000000000..1de242a171
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/images/q92.jpg
Binary files differ
diff --git a/dom/html/reftests/toblob-todataurl/quality-0-ref.html b/dom/html/reftests/toblob-todataurl/quality-0-ref.html
new file mode 100644
index 0000000000..3d6923fd39
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/quality-0-ref.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<html><body><img src='images/q0.jpg'/></table></body></html>
diff --git a/dom/html/reftests/toblob-todataurl/quality-100-ref.html b/dom/html/reftests/toblob-todataurl/quality-100-ref.html
new file mode 100644
index 0000000000..8b157d0ab3
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/quality-100-ref.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<html><body><img src='images/q100.jpg'/></table></body></html>
diff --git a/dom/html/reftests/toblob-todataurl/quality-25-ref.html b/dom/html/reftests/toblob-todataurl/quality-25-ref.html
new file mode 100644
index 0000000000..385f2ab356
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/quality-25-ref.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<html><body><img src='images/q25.jpg'/></table></body></html>
diff --git a/dom/html/reftests/toblob-todataurl/quality-50-ref.html b/dom/html/reftests/toblob-todataurl/quality-50-ref.html
new file mode 100644
index 0000000000..68b91f43f6
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/quality-50-ref.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<html><body><img src='images/q50.jpg'/></table></body></html>
diff --git a/dom/html/reftests/toblob-todataurl/quality-75-ref.html b/dom/html/reftests/toblob-todataurl/quality-75-ref.html
new file mode 100644
index 0000000000..7e610d231b
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/quality-75-ref.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<html><body><img src='images/q75.jpg'/></table></body></html>
diff --git a/dom/html/reftests/toblob-todataurl/quality-92-ref.html b/dom/html/reftests/toblob-todataurl/quality-92-ref.html
new file mode 100644
index 0000000000..15a930c942
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/quality-92-ref.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<html><body><img src='images/q92.jpg'/></table></body></html>
diff --git a/dom/html/reftests/toblob-todataurl/reftest.list b/dom/html/reftests/toblob-todataurl/reftest.list
new file mode 100644
index 0000000000..efe5a3e7f6
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/reftest.list
@@ -0,0 +1,16 @@
+fuzzy-if(Android,0-105,0-482) == toblob-quality-0.html quality-0-ref.html
+fuzzy-if(Android,0-38,0-2024) == toblob-quality-25.html quality-25-ref.html
+fuzzy-if(Android,0-29,0-2336) == toblob-quality-50.html quality-50-ref.html
+fuzzy-if(Android,0-23,0-3533) == toblob-quality-75.html quality-75-ref.html
+fuzzy-if(Android,0-16,0-4199) == toblob-quality-92.html quality-92-ref.html
+fuzzy-if(Android,0-8,0-2461) == toblob-quality-100.html quality-100-ref.html
+fuzzy-if(Android,0-16,0-4199) == toblob-quality-undefined.html quality-92-ref.html
+fuzzy-if(Android,0-16,0-4199) == toblob-quality-default.html quality-92-ref.html
+fuzzy-if(Android,0-105,0-482) == todataurl-quality-0.html quality-0-ref.html
+fuzzy-if(Android,0-38,0-2024) == todataurl-quality-25.html quality-25-ref.html
+fuzzy-if(Android,0-29,0-2336) == todataurl-quality-50.html quality-50-ref.html
+fuzzy-if(Android,0-23,0-3533) == todataurl-quality-75.html quality-75-ref.html
+fuzzy-if(Android,0-16,0-4199) == todataurl-quality-92.html quality-92-ref.html
+fuzzy-if(Android,0-8,0-2461) == todataurl-quality-100.html quality-100-ref.html
+fuzzy-if(Android,0-16,0-4199) == todataurl-quality-undefined.html quality-92-ref.html
+fuzzy-if(Android,0-16,0-4199) == todataurl-quality-default.html quality-92-ref.html \ No newline at end of file
diff --git a/dom/html/reftests/toblob-todataurl/sample.js b/dom/html/reftests/toblob-todataurl/sample.js
new file mode 100644
index 0000000000..8948312c93
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/sample.js
@@ -0,0 +1,2 @@
+var sample =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADwCAYAAABxLb1rAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QcMEAgbxBgmbQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAACAASURBVHja5L151OVnWSW69zlfKklVSCVkBBOmhJiQECAJCVOYJ4OIQouzLSC0LY2ubvXq6nuvii677eVV1+peiAOiKDYgIkJkRkgIIQlVFTJPkIHMU1UmQpIavn3/OOf3vs/0O98XvH/keKsbU/V9Z/z93vd5n2fv/eyH+E+PF+wfAtCCf+NR/A71a+07XcGm6Qo2TqfYuLKCTdMpNk5WsHH+s00rs7/vO51i03QFGyYTTAhw/v8mAEiCECYkiPnv5o8ZfjaZ/2wy/zATzh8j9MeY589+r/m7DI8nJhrer7/PZP7+xOyzTQhQxISzL9o/L/rf2/P6689eu7/+xHwPDq9DmN/Z7w//WYH2u/4Y+xrm5+YagLPHTOavRfP3CYk9q8RDuyZ4eM8ED++c4OHdUzy8e/6z3bN/t7+3n02wczexe3WCXavE7j3ErlVi1x5i9+rsf7v29N/tXiV27pn/XbPfDc/pjwV275nM/j1/rQmB6VRYmQrTKTCdrmI6BVamwmQirEyB6WT2+8kKsDJZnT9OmE6AlekqJpP5PZoIJDAlwKlm62vC2e8hcKLZdZoApOb/xfz+C5wAk4lm13iC/vj570jNXnui2T2aqP9s/rqT+WflZPa5p8P3msw+72Sq9vfhO8z+LUxXZp9pOpl999l1mX//FXONwr9XpqvtfVbmz1mZCpwC23dtxo7dB2D7rs3YvnszduzejO3h3/dsuBT37rMN9+yzDffusxX37nshhN1Yhj+TFqgwHrDSY9YbGLXgtecPoMyvaB4g9bcVoOK5ACEB7ZGKH1Xz/z88Yv5+HD6meTQF2Q9Izn8s2BcfghFaMBy+gPnF/H37pZj9juEaSOkDh8uv8nsJ5npQw0cFOPsYcN9k/vf+f2bPnT/JfmwNbyTzeu3e9LWg9p2YPrPsc8xCcI+JX5rmPVgcosP3BGEXDMPjRPXvKbj7NntqXwkU+r0za4/z7zB7GttaFLVwi8T1lu7p8Hk5rJzZJ6HYvrvMhWe/RW0Nyq5huxY4e3HNF4bihWW47vSfTilxUXuavcN2eZcJTntPYVn+TNLdrHajwuIk8xfngqDJ/AZqW1w+aAwbqHi+pHz1bVRhCJ5EGWBY3b0hEqi/ht+s7NuZ/pO3JTN8BTIvLvUPSBsewnUkzXvRLXvY9csUPeympb8fIaTONr7mQSVeh357ZS5Ivh0Kv1Pf1O4x9FHWbUb129c/EoZzYbg2UrEg4/ebZ7U2sNvvHK+zaD+tuaocO9H7/yi5OMt4QtGHFZr76g/A4S3pDmS1C2GulebvI3OSmp/JvJHbZeY6tHWnuPr7daDKbKUfxrDvbZ7fKhITuZciAI5maCOZH8JqYhEsU1oQw14/aXosZcgMCMWNbTMOmZs2klH0j9lvH4edRRtg2I/+IfOjCVY+UWxbl7BBzcdOmSOcbfn57KWf/DSLfbYBNJY9Dl9qyFgULriK09olgHSpnRagFpQP/P4S5xNP8RXIllXS3Uu13T9kwTSBiT4Jb/81eXyxXMNhVpzFLVgpZ2s2GWeVwLQ0TvPPyBakxPn3cCHBBDuy2B5M+bH7fBrfmAorzn7uliKw2sD5cIh5udxJLFdZKGSJQEigh8NbyxgAuSADxEhJW9UDWF8QHTaX5EuuvlPlg5XLnOYnpC0HXAXKlLCSVfpOn/mYiCApLX7JVZIurvcQYAIWenkqKr2tDaEaLSlCAhleRAzZ1XBIqLgNNEdBW6wYL9dG76+5AJxFe3osw2TirAPn8LnZyzSyfnO17FDm3qplspyn5QqHpsn//JcYIgVVrB2g/CHzHelZrHp+xVDZqN48bIecP0rd5XPHq/9YdLm3bAHlAnrfX0qFEmvAxWeqlLlMcrhzLv0mWLY/kzUDXFoMC8oErYMECSee3dS0x/BwmrWMTDVoVJR4wx2Teb+2L0xp0VPR2Wayi49kKoFnJWNIneTLDLWgPDs2JX/q+8U2ZBByZYs9vBmzalvuoJ0DKe2lLcAVIAVF/Mfnc64urT4HadJY+ghOX84rRFhRvRwzUIVC8LS5TT9EzL01a0Ct5FTP2IfMOpzsqmoGoYBSNL+1xbqb486UT/1t2U6TtkuhRpCHO2wAw4Ls1sYxOTyxwJjnWSdzoheyUZbvmOENu6qUsFktYfmL9YdspTJ2cSoYQTyMg/3ivKyU2b9MS3Y44X3167OyBnGLvjiwC1k0IHdAahhL+PkNd6Xy/HPMg5bb9w6MVy5RHCFRvKctnTRS4rEobRl3lTx2yXHkziRr4YswERssIBC2Q0CJZFJRdtFlTwjXw38VKWdFPWzLlZZp2Smf6ywgAYbr4lCG+ecUe9aJmEm791Vet+aeargXVF6zKeFkmYMw7Un5XJQDudIzdIb7zRDc8qGgUJ+wvZZQYLisYJZ/UwFwcaxbX0AceR4j7tA3iN9w/aa1JG4Ut1bfmFUErnBKKCyYGCroYhCFkfN6joHY09khgaMfoIdIVhxdxXQz4RSymKbJAyml7ysyo0D2sCDrZJ6m+h2yjfmp5DLshlHKlWYDkRCvaScAaM4GFfwHR89kKlIX8hlUzNgCJOPvxXyTS55dLZQRrCoSBRwUaNeJBsuWuwNqJErHgAciJ5SpNtOWD8CiTRxMkNb4Hs5YfGCnhhXd1sgAYc1FW1qu7C8EwHjEcLQaXh9TwnU8Rwbjm20KGmypn0OzrK2DvErHvWjxpcj20mUaJDKz6/CbUGIpBy0RBvj2i9hLJtTSisQdFaqEWenNhJlxyERpJUMR28qgdmQnHaVh0mcHLzoMT8V1s5tdIfNhDZ2411AqlVweInMYkTU0TWbCwjKk7h7TrqTqyxQno8FvQ0FDh6/4ENvXCgu5lZAuIS1hQg+5GFKErdzMFJAKWQrFOQRhscA5iTP/bHJ7QI7Nd5+WIUOkOSLoSUsGhHA5AqBLqxYBv9XJsaAkrogHhlTCaZXmeE4gQxoQy754GUhpyqAToQ5qGRt9ySn6zUvRYGhZakEUq3e+Welw4yDTYeKi++eMxIrCd05McdatQxEPDHWobH6olD/2pymGAIf+9EsyJz1EE6CUyMGGBKTsmwXGyC4fMcROfWioPERItQyrMbwmmDYEt3+whAVarZ5kg7IR99CXAgm1HBawKVHbP6lU+guKkr1+D0zkHSoLaSyhsEoJmSydLqBTkcBTBTrl66IIxYQdQY95L08AFMblK/+aYK6Rw9VIYKw+YiifegDpILZKIFcjyWbEJvoddEWEY5+JSl1rQ4B80uQij8rzopcFLD6nnBYvbkabJc+zY0buGP3ASBmkeqbUJDPMsLc5TBSSSIt5yZEVgx5NJv+bl62B9IiJlpzQlsjyuKi+zhksmbdrl390fIQOL/Z3NSJglvjy8iqadWMPCBUKySK9j4BF4lRUHJD2rLdElswXYta/WMBkfiLLys1kqimXoY8w9W0v5WqO9iAYHqHeffRvBwNcK5xnoq2W1DCmLDRloVxCUKru5U/DknlG0FvJnHzqolzXrWC01R0D87VVDLYM+kGaBeBil+kuaRmVBcHpQ2w8iXuQmuF1FqGXlPI4cYzNoxNSYwRDpGGSWbDDCmyCSqKM/uAJZBOVs8+IwfryXmZD96xM0uzvVTbEnvF5CFmOcs4kjXqAN3fOqhF6UT1ngd1FGQg2BSyXRjtKdy04JwHljudOZri8QfRrUZkwoZWD0neN5CqvwlGTGrTfD5cjDK+tRNgtVwb4rwnWXJA5an2l86jWmpkizPtXHUOBzSotrtQBek8pDFiey+86syjD3EUaQ3Dlb0pYiu9KevzIw60dN4wsMS0xYREDExAppkOAiHrVcFMcTEVfwpsPIlOHNj1lIh2UunCsxE8252TNjDF0IpD0oL6r5dV6qxE6dWLAY8jK0j5HF5S36xbIH1q8lLYjCX29mPKSpt97uB6tHJ+TKl4InxleBq6WKdmQZ0ASxyeziWZ7gGIInIYsJFzALw+2yPjGqoOsGezHdABcb/dH9Quts1Qmi/LVhzYFKp0unQpYmmW4rL5CcH3DLLjUyPjRYGws+VQfMn0ywQIj8YLsmIcF8tM8J7fP2U0vedG4Am7qvimLM6h4zAI5YygdvWTF8TSUL6ppsUSmslDyxx9HyDLJ0wYUi+/DdLVc0FM+dOK9GukZNDs96OkUssbYsz5kp+g910plUaScqtbMKFwiEJNji+pqrMBnI7MUgmgLlGYdS0gdTr31LvSxOM6Ja8SPZcgA656nOq3hOkplBppqpDc3EwemZKMvWxCAYatNauJPg+XIbViWujJfUpuTlVWwLhqx6Ht1RZUi1KYDC6ylzeBiNtvxNYWFWesZrAbMZRR2gyRCKmQAytIfxFsRA7sDiIrvI4QG/5GDtK0FpXK6PZ8MB4dG6Vo1rFLZcMNlzb3P1hFCBIjCwGE4bDn0B8d4L9/SN5T2ET6wMhVbsrPoXgknrJokxTPyg3ax47vhoquX1J08k5OlptKYEcII4m3bh65lzQC1ILBxwWHpsJexoBhbcfqFkgWYjbzB91eabM+cQPZkj+mDaMBfdZyMpv1KRmaghqOZr0Kj82U+6XrWYWQu83JDId1tj5FPvRSYaTpsMNgSJEMKjp5RVenXsTya8sy2xDE39VD+WgRIIBHPkcGvUJKi82KIcBYDplWt2N1J+cOnHYAqYeJGLIspUPX1ZO4PWZInDDh3J1pMpsYgjTLBWhopa1MeQt/54fIA20LJorNDTfepXpfPg20XSjfYhXYNhnBvqxwGRDxwKC2ocpncYGLqOpYBilEmVUfKRb3BRI2ZJTDcat4KBxDzQmTxYQblvljLQiL2xCy47TKCfvR5+QCdy0cSEjA1XnrB99AmNsdwFKSRoQDMSjsFongMdvDwnC8YycI+q2+ihg2Jzqgp9hHQSoIQelVVvbrP+F2WIvWspkwSfV+N4vMzTNyocjndGgDH79pN7mUsWJAjtGxRhXjY3Rub1ZqWPac1DtmIdXcZ0X+y2HtKZGRvEaWVmsHf1ybAFn2lozrXiR1NXJwhPVZLYI1kDxyjica/oBZkgLlpIWnS4mtQxQ0O5a+HPzqo24oe+tILHPFeiFKAgQShgkVXEN2pCnrxzehhTGkx2161uyWsK3yvhDvmbo5UmBq2t9RoMJTMVvQsjWJ4skGEOXD7e8dkAJBvR5dHWR6U8o1sLmu3pZ69iuG2MWY7ipBGXtC+7FuDEIzPY/hEtnR1WDm9MYiQyksvxl+g7Zact2RfrkOwo7t+Q7+285x0pEvE4g1p+G+mF3hhGqtHaZLKbOM3agRq82mvsbG6waEFxwttbb9j4A+V3UrkRAkelBd7s3/LSFS1rnVWspUmwUTV25PW2TbN+zMAs865TgGBjIJeKkkmWpLBIs8zLjgx0GFoARs0iAYjI4myXdB0FvisJ2KNoYdGHnaIhhUVRNHxYNaBiUh9yaRnv5u/ifl8dKJuucjOsgRiFvsX2RKNr1QXKSs/icZdp9SUWca5PliAQNDY8n8gZ9iJHF+qsEiFPPZcfjqajHspMUCy5rkxnp0szABjVHSdDTKdAsPiTBajbrF50SpMhufbzPrJb8x7yPxBpZAJmA1tsk+7SCJTzJTamK6N2EsbA3/0I3TBWohWz3IliVAmr6IzabGPpdJD0x52oclYVil0u7igbjtqjJtOlztXUoooMfd7uIfPonxu+souexpwR9G6KQaRzIjVng3jUnDGs3IgKhtJlOQc64IqyBNE3yFuA5cqWb1K3bLfKa3cpwtyUiEDU1+ncnihT3J6+Zw4EZPtI2PSS8UCe/OwBQK9NV5RIxu8SCC91EKhhShrObwHGr3Lhi0tybQwUoBOEg8uZv2Mz15y0h36QZkPCVtitqCggIOZvky/wjsTKaLUtBF28ZrM1coeRBdqGJ2zXS+qQUDtgi70ak2jKNMnoWhZX7CvaYl5TY6IZk9G21ObnGu6NCjnzApcuYK43bujjFutzjV09NcieSnSVB6qrVpFi6BytGxyJhU2aXBjG4LvI3OrYLu/YkJw+83UaNUWDeGEXPp63eZ6AsRjNQMcw+sWGiQsiPVEnstgzyxZVxXWp2YMVIpGSqEnlGO9kjalyFqOeLYp86Zu1/qlq6JXWE7X2HGnILWwnJ+McQJVOHL4rM1Ku61Am7GcDIxpSApD94jdMPRGCNn1vfftwjvkIOBwM7t6VioZH05p8FdbSqsH8uY8Yja4AkYsVA6AEd1Ryro6hOFNEOmYbZSefK7v18EYSssw07shOXDGGQqlvrnzjLpoFtKrMKLATCmond1ZUk7+S5j3MZpcNvZ7skQBsIpjo10dlZIfiwkRInuT01j8BEcRD+mpYX22qE29jLFzKnqqO989Jl83MpsHiEFyUwj7ZqxibbPFEWSByZ0UrT2Phqnr4t15BhI2sljoFZNfm8cqrc9dDWUwtXD1RDx4GbqeeGfp7HkidksslWYb3dNRds2o9y87NJOGezZldkWAZ0rULxHJEmt94ysYdqjrRjJiJI0d3XUDeBH4KytGMqPNtENSxO6qboK1V02wBXtvdZUhicbpscLw1C2wrFNTYKzUlBFasgCokZuzLjeYkZsd2KH83Hwy51KV41b9I10oWsDC1cR2yPWopDt1eJ5QDk+q7Y/i0e/FbLbdzxm2wqXH83LQyzWsbESWIFlgXDs4uCh20DQZQ8dGC19mVG5YMtCF7LyS5pfAyLUm2/oBX2qfz5IljEIZBXzQl6UdpugstMrrQcOxyb9nyKaKxriepTlSh+1AsGauPVt1AIO7wgpMtUKrJiLEwqLzxSGKAUF1fKK1hSkYaeaBYH7WSG4s9fj30pshaHGVW+4EjuCHKl49XHhUAme4XluGBSCyLrlt4FBBqjiJhr3f/lQTsZjpTiMhffOcSj2lFXt3tw66RSPXe6oRHVZ29QytFsoGoD3YqJ3SlomMhqdOz1yQ9CkDDIGoY3GqYFeX1dsslqoPSQXwgQvECDNijNUoj7B5PcvroF0h8Og9e9YCjetA6MkaQZDOqYVh9glH/AabkS0ZYFQmwfSgoYxaVZs0MPJrUJ01a2SQqYFzWNAiXAsae8wGQK2RaY0SHYVgVIuDJsVicXt8EMVbuHaucvqZupmIC5jsJUMsCaKlj03azMwJ+7NuEMBoHI56olAXEjJ8MedjWCSsNBq9mjH0OBQrtp5KbjVyeGcIdIG8KWwby/vmYCHWi8fBRuH+UczMostiZEB4zrsgxg/r5hMQCa9gltEigrWTLydnaDTc+uusRPEgmIUq4rOIZm1jwXVYR0JSgHFk3rI5oIc2zS40YNJuj1V4zER+GI9ELFsz8GTN8pYLSIm6sqj/obAD6E9XFjhNnxwWs0dALGJ0w8PoQXmpKKUz12u1YjQ+hTQSAQ6taolpplM51N2CMt2aGCF+aAKAHw/mrZC4+NZlJ32o6sONga6lB0ojJLPCiW78Ix1ASz8D2G68MBAOqueGDKlWJBgViBnv4I10fSL2SCGGIoenKcA6ZVtYOn+KCWusTZG07u1lqS4/YbA4AfuxHh3ImTPLhu8qlMnyWCFZjmB2ZF+HX8oj+jEeANcK2lrA7FZ3brEvlJuqZRm3OGjaT18rSmvl7KO7MxduJOG45BjPZZ5L52un7kko23nQ3TbI+FZpQLGRv8j77jEzKQoezmZKaGLHGUTGjdJQRvFUTQKzs5eFbEaBwq6rkk0NzOm8PYzsjYIM2jLZaGI6a5JlWYnKxHnNvb6LshuicsYx/pDFYS75TFUazxEGSEeyUiqNuqRQZnjUgJGWVVQ47Kx5QVGxmFDpXdODiaofTBXQPMLjwfHUiXNh1Q+4ZZPAzAKgsEDAXJVTqNOc9djpm/vR+1kNmhAGYFfT03waruS+W87tdRhTWN1wpFmxuenzfxPYrJ2QA8+Ve5NzzPJSC6Xjms3r0E6z6EFQ8K1pMEaeQjmEKfmEMY9KSy4tXDjusKF4kRSiRQDp5yzHJSRLIQzuKvQ6Ndu9YSzr+6RAbw6hwTzVzZZBHlLu2ryC76E5VBEwTy+gD+M7NSIgcfNCwmAn1ycf+upc/Kun4flApwKcgB9QH3B3RokUc3dVdwynKbhsps7crroUJTCxeNbHmHXRQgusRWyxo12DAUDOzGQJARusTPZTzQNGsBjvNH4x6n5gLovRMG7kQiE18FWXFpAgoZs0aGXkNrpS/wsTkWzHhNIHq0qlGxlMt9UsQu5Z6tEJp+w4agzurkc1srQuv2MY1SgzItV8p0Qde5NZhm6NlpGrYuQtwWDeUSGjsykRipkzcZRCMb9EkYlF0OANpNRgpKqaSHDYogxR5MhH5b40ulQtLHkhCqoZU3RbvcgfpEruTksHARoZDNYZsNZ6XLXv0mPo7cwb+KwUG0vsgYWkofD65mjN0hF/hsBZzuVQZCKJYqK3wcKK8YhCmvgV5TzpvWX7lb00QUTpICOTgSotSJV8F71DZidslEvDaDjlJogZp+FYrmaiuHayVsKZOsAfm+JUduYatbUQKIBw2rONNjFZa7XwRsTUircwjDaXNWwwxJ27FkQ5fMEs+oqm88MWWJBKVvHMskhzYnOG0pve/4cZTPRmCAQKJ90lI0HGohoXMMWjGSQWOJx0H7KeeUX9fiH/EBOC5zIx9ckLVayBAXblJofBMXk2KCc3ZDcvw1MaXZoXJrpFMNzMmbWcQYWIxySmn8J+I7PpF8OidF5PZiMVGanYe0gNNWsyZ3gcKZqSmtqRMdsL5gkYYYr9ZGglVjzkHOGA9EFcUaRuDl1Z92PmzmMNchx5j7tWApf7gMGkIVYzLGG0eMNZVV/MrZapr1tFm2DCuOSaD0Q/Q1uMJJKqYtozvgMZ1oZWLSsLzBzli+tXy2XGrMDGSmayG0Q2I8wiSworXcjm551dtMOawwZgkQExF91yQGV38fW2iL4Xqm15jZ8dvq+yg9TRkJRV+ayqVVDe0det29B+Q3+Ouz5fVcCDHEMrWogAwWYqlqdKRq5wji9RL8YiXjtHwcLFRn6aIHK2r6KRnBYEy40rbgvQeOiJGnFIqftiK9zIZuOk938cgRvNPZn7DYbh8H42TCZ5xmY1M3rj2wogtMCS1ZQ4Y4ARu6hQ45+PbRYYMbfPFyJJDLiOGQCj/oi0QiSU9SKyAFYO8M84kMWD+kmqXDUHaYfXxFkdPdNcDxfOyBGhcs8N+8ziRZmz6R22wcSx4lFeSD9LObHjxqxewajAGKGWJBK8G5mfoWLKcfrDLJfJoeE/mxd66cZw3zigcl3wnKoILcBlDJ6WznYVmabytasafaLriYUqktV9sT9ksveEmQ73eYBKZPUQdDheNnmothCLzE3p/9Yytjg5L/bBy0yfs8THJBAty8IChwu3sDc4DUgpr3H+2YiNfg1whzYe+Wwhl3AoW+dkx0EmxxUgmhEx6A1ju4/vJvIGnDS9skykRTHdbFikzOXdUKKnCq5Tb04XaB2U6RQP8p+VocWhbfqggJaLBqikLnGKmXeyRaPUZZjGcbBZ3iNFiCIMtw6SdFi+fQzMAKjL/U12G3HIHqDky2qZAGYF2OaSynUHqbDeX6BNHLqbQktieXqymF+tMNeYKhAV+iqkUkDbCXGKbuV5fh3zibJkGGACoOSBqUdtf7U2kRKtiuJKV9CvW9t7D5TTZ5Ty7GgnwZj2sRXn2qZ8OwLSO+mH01PjM+QtwePk1k6PFXGkPBHMO5AULGgoPHtfsFLGaufdsszIfRZk2UAlu+6cSnmvQoZOgfHJa2wSioJZHEpRVtZVvae5bBdnUdio6uND0bkjYI1pdQyle5xU6YdPW9NVX35bwopw0zdRtRf2rEupjdN3zHDkkImXgRibA9PukCIabgTSWRG+ZCywCoCuam9bJG/hGiaqKjJEqlSXsoK7KxFsLBdJ1yAe/ycVH1PVaTw09rP36ZpTNTX20w4XwqhZpT3pXb+sCtA8Rt9it8bEoJm/yhtzotjjUeTL9Blp3JdlmF0ra2ERygJwrjjPN/PRqjlcN5nNlaixl5tdBlQFQUXstlhHFYkBpfmjroxWH+8RrOO99X11yKkooBjKhmx+KyTTCvpGPSo+J2DUvqUoidPt4CuHWQ5ONCHhyL6YY2zesrHA62F0XdKoep+OiKudEN9McKuyFpFFC1dkg2lss+zAF3pZhRCm25eMA6zw2gXZ0D7XpDscawFn9FYKiv/c54pme2TeizEK2CxrRKQnn0rKQQRMyZ8Ye42J6C7CgME6PTkVyA5PHsFDS2FkJI2jc5G5Uannu/bcoSex4j0xSncnVk6BOWedJTEnuEmGZXLpDn9D/TDYjaqrB3I/d84+WI0CpDw2HI4ajCGwlU12dMClX/PWtJXee33JMsDv5c9Y83QcDKM1hg4P7CYzg2aZvdxOpGANFU90hhmz5kRzTi6mZcgyWHL+qoVJQEwO5OYCR9dUjg1Ddklexyvt5lNS0WJ0il2bgxszidh1kPvp5q7OISCYHurOCHuop+B/vDBWOR73hJqtnCNG+khZ8Mv0GCX9FfQJu1sT8tckloRuLKQ3h/WhRo61j2YdjLb64XCPGvz4XwW21xFRo6lj4NwL9YUKq7PRQd0WSy2dr4V6ajaWrBd4PX/4KAJjouOKOtb0mTqm16bmUllE+gxQhc2UJybgSTqnlWOpW5GZthXiqZPh+BJkTC/liZqefqYT02U2hihptu/Zhsl1ldjxncHJuXeeKLG9ibQxeg66utOmfQuY6bgdCW8a63zv4TtXikNDg/knh9LcGCmUnTddVO/cDA1M4e3G7CEqJ+zWKH5jK5ZKklyTrG4ovAo9oBnkHjNmOaMKCw8lpNckgv1gZms3rWzoUn7pkKXAmCUopmsvuIQl8Hpc7hfhfusJmqOEsZ+RYUeCdAyZrSRUeSOqGOvNJP3MZwUb+GCOO9KK1MphZWxNBfJO1GYNlkSPrh4eUWCeAAkvVgAAIABJREFULqZoPxSAco0k5BFnZJxah2LWl2IqFUpn5tZhKpSRVVZAP9/CDhQfWvSMbUwrFRUnC9ILl1i02AUe2jHrI7CFTJ9uqiYcLMBGxA36b9rMnCqKpiyRyJWsF9VyhLSKSzFaafU1JEj1K6fPQgZnmxEvTxN4Rb8SCZR938uTAWq92Z/WkR4SNXyiVA419+Aw39eFsHoCTB44Zt2S0+yPkQJAi3kcpvnCnr2r2ML8PU2nBTHaKUPmCXh9hETO4siivkT2kiCjIWBYuAb7oiF4usGAyeZat4tCnKym3fce47HrHhwPiqpBwbrdTkKDKaN9WyMRgr5i73MkQYRkk+ZsqFQpidxMERUMHov16rxsaeVdSkiExx2rAVNehuQCHWPNEKfx9WzbkU6hnZBpbHQm6Wp0dhkCIB9FnFtkixUn77CGGVTNiLBGpSooZBaGdrKP90ULQ6dlLy29j10e1iOfRapnBjJatTx3dUyfGw1gu5QgZY+hEcfqIhg+Y+4gsL2kvS1QKrpI7AdWxraaGDdsYhfCCvZVrhBi2egWTVfrdnPfKuHa3NjNbTvZwjbQG8YtxzVvRTgkdTYb/RszOZWxX4/RqiKiyBosDS4rfdxGoblxD6xyDBmZU+RNVF9TjKmB6Bcz/Q1jsMiKqkBbZi9PAFykUa0y89EsceSFCssthhF93kOzp+TRXtCOQbQCXjpjXj/JXiVe1zEcDrZcRfSKkiqqmLMQFxFZ2IMVKZ8GFtmz0i4HYdhg4gLrMYadObRLjaW3dJsobjym1+w4xdgURDoIQ2laHJNDSu1LhzZwapAlIU/Toy913ZjRyAIrxHOarLFSM8h78Sno4ZwjeTlEzGBkUln2JkY8dNw5V+7sDOmJEvth5EN78RGQWj/LIwxhWhUKO++wtjRZMhKkaiviOoJgWTZrbfyQoedzwJJEo+ZnoD9qW6WIiWTfKJUKnZidWb1foXEPuKL/HCqdNrwu0BI82W6PwQyCgW1jWO9xYHWQFsVOzGoWLqpMlckm1dEBpKcGVCwL0s3GZVVEGIeU1o2Rgm7I6lUxlaEHNdnjx0knxWhRWhQxjDKojAds2W3JCIZZpWNjZcWcf6XuOlW8XJaTk5l4shZBynN04gw9jUD1zlyXecRDVFINE/qWyQdwBAPU4gyP64D+1nKXdnNb5QZoFyvO9TSqWSgZhq8I4LKntmEdxppWKB/g0nEexhHa2a4sO9irUg+hwyP7AdqFTBV7PeIuUY5USAJzH2+uhmRxxASUj/icOfPQMNeSWrgWFFoOhTBWEzk18phWUAgM2Ry9zr5i43zHTvFTLZhRXUiuSu9HVyKHoKwsrYIhD1h0ZlhCUNa0iKHrmoXjtplFI/k2wDGUywV3sHtpjuQ72aF6KYXQHA9oa/b0Lsj40uspMHXKVlFuE+SNp1A2tw3CmLtFBIqjG1Iq+kZF5N7hwHQWIHMU9CpimM6E0jCgo+A83DSuDtiHfEIKzOeYCQNHSzIV8048FMACXKeRy6mIdCzRPcAOflIyrHDdLlSbL2xbCmla9yQG+MrABizMKSkP28tKSegy1oCcBszWTyHM5xpzBlgltXRRuXWZ0Mp05IkXJ9VTBW94Kp5SwjZ75dyvqZehuU7tXPM0cHWpM0AtJjsKC77vKdin7gofnuhErgYncxs22DqJSdricDZHoPSbrdSsbssNOkso2gHULAgTBjA4YC3Dam6YnJSoX3YKGJWmpc0TkcUK4Q0FK3urEWPUNN5NfkwmjXi4g7Ryxg/VFD+LW2UTBBXEiHeuaR6KZkqbNRtwmKV1KLGHA9doTw0D0aM42mk/6UeQWjsuKrTkwXoZDkFZBXPqzPTr/IHBOiu21rGTO35f2OWuNvfYExRE7nf2xrtEnolHFfqzIOhf7rnAWsffx+qZdX5vMreUDVlT6pNk5nFdALJS/IB1kQr8i/xUMifjiN4mxliARYN6wl7sXIowktP8gGaFskqpLdgfAowMBsgkppZj6vwcDLTPd8UNO3Dg6/4MW6+6A4DwpQtvwqYz3ovbd3w3BaU+LFxuFKjNdl74jt/DK9/5O2kb03asWAKI9GctffeNM21VL8kYhy4lLadn6J2KQMVUOwKf+JO34x//1zu8DXw1E9i1nI/0WZtpan/wa7+M3/+VX+6fU0gsrR2UXgMoWajdWXo5k4/ocmS1eFIf5iFVfI/RFBZ4NmHZ/ziICt4hZvlmIo10gqwZyHL6/+iyQgU7czlgOqbodjyhvYFMFz2Whda9WE7Zrmi3pQzoc2QgOc0AIpY1nYBqQRHZ4kvBr5BwLVDthJcPILk/wQ9b98xuz1a2Xn0HNqxMcOJRBwMAzr/idhx56H44/PEbO6Gi4HDbNhFc9pLyuJGhZr4PcYSN4sgwrCGbtrNwHc6L2pZn/lLnfeQt+OqH3oJq3FAifEtm1rpcq09dC59bQMLKLAbICApzgRWfvJGtEztbd6IAFEuZOmxO4XHwVeW+g9pNKBkhuEudJ9WRy5oBjtiyj9LBWgMHHHmtXvAwya2GUkRK6qLs4pKwliBitaLhwVShsZC+RchJb1Ljak6Bk2UeqhI4D+elwzJHQGvUPngOi1MneNIMjkiOqBsnbL3qDjz76EOwYa8pAOKCK2/HqccePjrmeRB0uzGJVOIs0pDzEucqJDxWUuPIE6vF7NkMNVJ+MbBMpUkt8xS7BJcx4bCVmpHhEOuMa3bJSQ5ilpgwF8y7+ISRoGF6qHs924yskWY0hf50K2divKR5iiGDWzhHybJlHIxeGTKvNd1tPfluBGHS4Gqlf7oOkDB4hsrizDhaQmB1931GZrgNhSk8Up8mVs0WZhX0pJHASA8k02N2zhw+db4p60eGYOA84AZPvD7JjSaL9W2Fwtar78RzjzsMFLC6Ovv3qcceauadxAzCkhxmBKI0agJUboKg8axbJSu/PnXml+HaKfI5ZkRmYerLERTST4NTptGaU40XKbcR0S5zDgclhWgVlaZOVtpOJ+gOGDb9RCy54OYR0Z4VxqlwtZzJwhu+tY6luw8RW+KWqxd4pQxYUVqhtejh9UTCOt9vHRtR5zaAz/YAZTgB68PNjV4d0ncKfsi37ZVkQs18sS4AnKT36thgZ3XZWGlmptOURQShyYjIZO4IkzIN1LNMEgliHvTIrj148pve777TVTfeg/d8/JL271//86/h1//8a7PHf+adba899Mhu/P5HtuKjX/kWbrzrO1iZTPDsow/Hz772FLzwxGPcp35o1078yUc+g/MuuQKTyQQvOOlUvPqlZ7iT5rpvX40Ltp2Dm2+9Dnv27MHBBx+Bk07+QRz6xGNdRXHmh94BAHjZG9+Lb170Udx58xbs2vkdnPy6v27f+f7bzsW9t3wZOx/4NrS6E3vtcxAed/gpOOzpb8B0n30BAVv+4d+3z3f2B9/S/v4DP/8+99l373oEF539Idz0zQsxmUxx9IkvxEkv/RGjWZ1dkG9dfAGuvuhcbL/9ZuzevQv77X8gjj7+WTj1Za/Bvhv3NRP88vrfs3s3zj/rLFy27UJsv/MuAMDBhx2C55x2Ek5/1Uuw12SCBx/4Dn7nV34Hq3tW8Xvv+b9wwIGPcw4+jzy8E7/0lt/DXnut4E//7jcw3Xu2Js/6wkX4/Ke24rpv3Yadj+zGIYftj5e8/Bn49z9/OjZv3htWUNgPd2to6DFuue6fwUtTrbXUdh6JRWa/RL3AK6Nkx782A8zzmcc8zjseZ5Sbkkq0YchEGjZDmZJyYuOaZ7DouzoUhhm5U1kdHKAhbazomIuAU7Fwbcm2XvZ9KaOpA6NpSg+qYWSBJ827mWm3pHp0i7G1Y1H4xf91Fv7mi1e5359z6Y0459IbccXf/LpDUP74b/8J5158Rfv3F849G/vtdwCed9KL2jr40Mf+zL3WHXdcj89+9j143Q/9GjYf+JSEKV9+wftw5y3bkoLglkvei/tvP8+91q6H7sSO6z+NB++6GEe/+Dcxne6ziIVz77Pl83+Fm791Yfv3FV//PPbb/0Acd8pL2/o8++Pvx7WXb3Uvc/89d+PCr/4Lbrjmcvz4f/zP2GffjfmtBOzevQf/+73vxY3XXut+d/vNt+IzN9+Kqy+7Em//L/8Bm/bfhBNPfia+ccFF2HLuRXjVD57uzrbzzrkYD333Ebz8jadiZWUKra7ij/773+MrX7rEve5tt9yDD//tubjg3Gvw53/7NmzevLdf20UZUzpfyRzqJhGlmWVs05i+widLFADXGmnJkSA2NgdYC+KjGMZgegZLsqmeYafUfz9x9eIsENJhYoERs76OpqRiSnzpA45slqqUG8bXDCHOUAU0AdUaTc5edZLWpXHkYP2eucpkk1gQHjjfZ8MK7jzz7SCI9378EvzxR7+Bqz/4M9iwsoLf+LOv4uyLb8WWP/lxTIxEQ/Ng+4nzrgMA/OE7XoSfePnxmK7sja3XbMf//PjX0wlw+/Z78Me/+k5s2u9AfPCfP4evbN2CLZdsw2knvah9jWOffiJOfe6rcMABT8S999+Hc879BK6//mJcdskX8cKXvG1e5fbNc9+Oa/HMF/wy9nv892PPZCP27AF23Hw27r/9PKzs/Xg8/uifxoYDnoHpXntjz3dvwN1X/w0evu963HnNJ3HkM9+MU//dX+Pr//BzAIAX//T7sdcUmEyUoI3v3Hc3XvMzv4n9Nh+Ii7/yMXzrkq/i2svOawHwmovOxbWXb8Wmxx2A57/mTTjiqcdgw94bsP2Om3DOp/8Bd95yI75+1hfwkh94Qwl8f/3sL+PGa6/F3vvui9e+6Q049sTjMSFw9SWX4VMf/Sdcd/W1+OoXvoKXve5leN5Ln4dvXHARtp57IV79+tNdh9OXP3s+AODlrzkFAPDFz27DV750CQ46eH+8/V1n4DknPwWbNq3gumtvx5/80adx1RW34gPvOwe/9F9e2YwOcv9v8HVWMfSqLTHfedOwRPJ7rAwfqyzwIixTI1jhumBBpV0cp3XZ29RdnJVOVTsgxxjfOwzDBaqAJ6qmNQqfB6VZEq5c0FrpssyEMo4w6Tbb08hkLqtYU5xdPR8270ejdGv62Zt89Kxv4kdOPwp7TVewumcV/3jOtXjzS5/emMKIXB6yeV8AwJcuuhnv+eRF2HL1bXjW0Yfhvb/yI+HAAX7xR1+HIw8/DBv32Qc/9PJXAADuvPsOB8+96fVvwRMOPQLT6Qo2bz4YL3nxj80ed8e1ZQfGsc/+SRx0+ImYTPdtn2nHTWcBAA5/5jux6dDTMN1rf0ynG7DvAU/HE5/1rlngvG1rYqqdjjOs25Ne/pM44ODvw4YNG3HCC35w9hrbb2/r5ZqLzgUAvOKNb8VRzzgJ+2zaDysbNuAJT3oafuDH3jorjy+7OK2BwWn7sm2zLPa1b/xhPOf5p2G//fbDpv32wykvOg2ve/MbAADfuOBCQMLTjzsahxx2MG664RbcevOdLbjc8K1bcP23bsHTj30SnnjkoQCAf/nMFgDAr/3mm3H6S4/H5gM3Ye99NuD4Zx6J3/pv/25WHv/LlfO5OqEvOgpEh6qFdO0tlHfY6RVWF47SDERQipyP9QxwDN+T6gyv8gJab9BnBLzldFZuGqwpD1XYv9OWwkOgmYNqIg32p9YyRBoXGA6BScDEc8ETLGKv6XUfHGkAkiBOeovTJJfPtMAhfSZIjuCl9JlmI4dopr6hTwk7/If+wn2VS6/bjvd/upeq7/7ABXj3By4AADzwz7+AvVYmLQ1+z7teip/+/c/jU1+/AZ/6+g0ALsBTDj8A7/nPb8BTn3i4u+dHHXE49sw/6+bH7Q8A2LlrV4MjVves4rytZ+GyKy/EjnvuxO7du9pzH37oAeTZl8CBh3x/EzVjdfZtH37gZgDAzVt/d3SZ7fzu3VjQB+NsuQDggIOPaJnvvhs3z3HBna0a2HHnLQCAT37gj0bf8/57thdTXGb/GjC/Y044fuaCPVHTqB73rBMA/D3uvP3Oht08/6Wn4ZMf+RS2nPsNHHHkq+c43+weveRVp7T99+3rZwfMb/zS+0Y/12233ltuz2J5hZZQhgAnPxIBdYW3fIPRuSDNY4EHaj2GBxwPfowiAa+sdNPs5RUJtOBr6hgLrh7MLrWRtWxN7crGC6W+MU0GN67FsG67phHLEUtx3h3SpCaaPlCbveYppOydEjZjLEwI1g8AejD7lc85Ejf87c/hU7/7evynNzwbhx+4CTfcfi9++/1f9LIVAHutrJRlxXDPvnD2mfjSV87EnXfd4oIfAKyu7ik/78remzxcpfWqD/aMzJu2H7v/e2Vlr35oTCJ5tb4/q6urBUOlBWMjOFohnXb6czGdTrHlqxdCEB5+6BGc/5WLsPc+G/C80080a2bta7Fn92rJz7cRB0Y/2JnmPCXQDat36y3lNEulh15JqVkYHlRnd+WEabfR1wySioWr78eN8z+IOKUtHF8x8KmLQAX07M+QIBP1HctgI+VqhjBogqNKD4PrVQ9il1OwBPbgZogP2S842Pb4bn+a5vbJCJ1/+5lvx0TABz5zJX7nry/AdR95C1amE7z7r87Hp86/ARf+2U+AICYTlb3S++69gjNOfQpeftLT8Is//Fw84y1/gUvnmccochL1gQQuunxWrr3u1T+JJz3lBEynG/HgQw/jr/76N+rD0FyMLoOZYJ/9jsBD91+PI5/7bqzsdzRWRUxXhMlEc4xvFSvTvN5WV4XplC7LzrcgsLjzUvGAQ56A7bfdiB9+2/+Bw454MkhhMiEmc1Z1whm2yAKuAYSDDjkEd952G755+RV4zvOe68wbrrrk8hnccPih7ftu2rwJJ55yPL5xwSW49upv47abbscjD+/E6a88Bfvsu3f7jEc+5TBce80t+MM//Q849hlHYDoFJhSmU2A6EaZTYTrprtuy7e2K0JMxhVAcvqTUbig3Tc5IukiH4y4HBkjVGF9ZAi84hdcT+oWgo2bRU2CIEevoYv/noArPGPteTY+npY6eELRJT63aaQ9sBI4XNtsDeUwH54w51efEqhqjyTAN0fWd05knFsilbzKcB8ktV92B5xxzCKaTWdm/5ao7cNqxh6WoZV2Jz/g/P4kvbLsRDz68C995aBc+ff6MFFnVAiH8GuL4DRv2wV4re+OBB7bj7LM/tPDJHqCffceDnjTDF2+9+I9w/61fxq6H7sKePTuh1V145MHbsOPGs/DNc36nn/AbNgEA7r39ckirycrJUelRITh/3HEnvRgA8LkP/ymuvPBcPHDPduzetRN7du/CPXffgcu2nIsPv/cP51rWOPycOOHkGWnx2Y99HBddsAUPfuc7ePA7D2Lr17bgU3//CQDAc047qUUlgnjRy58HADj/Kxfi7C/MyI+XvPK5jq197etPAwD87n/9O3zuzK24/dZ78Mgju7Bz5y7c9O278cl/3IZ3/OxfmtZQ+N55qejaUBoT3tAesx/IAsDW91p6POYwwBFCRwvY3lR3jQypAMqZDe5qdwQ5kR82/XaKdHuacajts/NZwxVp271pyllve09MTN+mba1jwBvRhMe0w+WDcNdiXc02n2zvlQxSGV7PhkoRE9Pnm9GHfkG2XnUH3nD6URCA3avCN755F37sZcckw4tm/kDhc9tuxOe23ZiWxWtPPSYBScR4bysAHP3UY3HZVd/Ax/+5axKf8pQTxzhuU361LwiBOOiIF+PB+76Ne276ArZf/b41D9v9Dz0OO27eiku/1PG71/78XwYDWLbWQRZf4ZhnvwA77rwZV2w5C18583+vgSIoZcOnvuwl+OYVl+Om667Dx/82P/9pxxyF0191elNJCMIxxx+NQw47COedtRW7d+/B4d93CI457knOTv/lrz0Z1197Kz79T+fjf/7BJ8a38ZD6WbcgwQ3p8kFsNSGFRDen8EPJwqS9JRsLt7JmtlbFMxUlc+U4MhJAOdHIOEt4W6hgROANG+mzRNkMq8eLFgxpSxyvpysbRxwDzfxjWy4wlODWqqot6tB01x5qzBLoP5sATIYZrLFTJUpuhvcWA4tCbL//IVx32/045fsPBQVcfsMOfPeR3TjVZID+9s3+9S//44fxnk9eirMuuQXffWQXjjh4f/zI6c/AW8547shMXtZVP4AzXvlGCMS1118FSXja007EC1/4o3jfX/6qQ9yqOb4zfacaDHL4cT+LjQefjHtu/hIeue+b2LPrfpAT7LXxUDzukGfioCe/qL3kU57zUyCE++68Crt3PpiwS0uKsDi0h4PrBa95M57y/c/Eldu+ijtvvg4PPfgAOJli8+MPwpOffhyOP/nUPEVt/vfpygp+5p2/iAvOPguXbd02I0VIHHTowXjOaSfj9Fe/GNOVqT/oJ8QLX34q/ulDnwEAvPgVp6TMQwLe8a4fxPNfdBw+e+bXcdXlN+Leex7EdGWCJ37fgTjtBUfjjB96VkK2iskP1n5z3umziiy98CaSzjecXGBY/Nj9Q7zroOxntIg2Emqs8NFEXRIbpyvYtLKCTdMpNk5XsO90BZumK9i4MsXGyeznw882ray07THga5N5CTmZu9FO5jduwlldP7BRxOxnBDAZdHKczB5jHFUm7fGYv+5k/hyjrxveb/45+mv29xowoSHcTeavDWr2d/aG8fb68+8w/HxivuvEGCJMhtecB0TKfieY7zA83n7/+euaDpPhddx3H67nHM0cnvPIbuKhXRM8vHuCh+f/fWj3FA/vYvvv7HfT/pg9E+xaJXbvIXavErvMv91/3c8m2OmeQ+xanbTHxJ/t0QzvWplghnmtCCsGC5xOhZU5FjYdHjOd4WQr09UZXjbV/D53PG8ymUtHJsMaWMV0/m8SmExmuOxkMhcGTzS7npMZDjeZ/3f4t3vt+Q2I7zU8b8DwJg3Hw/zvwMpkeMzq7OfD9+H8e0wG/A8NA3TXYSqsrAgrE3MNVjB/7dX2s5UpMF0RHlrdB9v3bMaOnQfg7t2bsWP3Adi++wBs37U/tu8+ADt2bcYOfgf37rsV9+69Dffssw337bsN3125aVkwwDWCX2XxPYoNhh+MuH6QDDZ0zK7JNusxrKoS6DqchnJzfhWH2VjzaCcKDaMArb+cn7dYNNHH0ZByMh6aEZC09v9kgXQZzFJ+bAArikAJLs0HdjQcY5RohIHpw0+trTJRYmNikIjTm8c2w04lE5S+rJwdYWfjs2Gq7ByeoHGEnytcvEYaM1pYkUVXbOdk0/qD6U0JaOzJEPuBlVvETI/5bM5x8k7z7G7owGgPHeao0BoRmolwrNdILPkD7tGfm0xUY7upUldTs3sL5sKP/QCotdhalFPmS1ywehHbtR5wurGkVMWbN3++xqR6mQHDVC47tzUF5fBchUUo25liopFsuVsQ4VZDWvuaBm2B24ZjIz6VDLntyM3BnstLfNYmqbziRcGIUUVw9Bsotg9GI4A4BlL0qLpQDOemEUOPEhUpsoWhPcPXqaQiCrOhzfcJIn05UogtEMv4IzZLtjRh0GJkIzIeMTaCFtHOXMNiqJGdfmcPUBXjMd1oFYaZT3ak6ZAcuJEUdHZekjV9RZBiESWQ+phmgUcSOKynnCfGNaexQbqt1zwdLUVY40hB1YO7MhdDxCE6eV6DF1NLIxHdtKv5wdjGRaY4uJsHmz1VRRdknZV/SlRNq6AdlWcGUXMwcAjDkcQx/iibvmZr9IqUSr1Q4dZz0SmYCBm57Lhna24uDKO7T8BbjJmtzyJlZhUP93Zgzf2smOS/TCUvwi6FkwkKjPVHGDoUFAbN0bwg+QqL3ZgBxsZKN12vuRMyaUft+/TvbNea0kQ368vp5iBZcssaidNvBD/vmkseANfb1ZFYXy74ff2wvgDlb2CzeTL8Ytv00XxpOMGVjAhStR6HlxeGhI5QSd/H2ItH40jWPIrozdxkgWT67+yWXph13A9hFrMOg8SnmkskZcH3kM25gTreraEtcTt2EkWGSy8UVxEqFzJu4giG3q9mnAAq5UOgZ+RqnTLOph7DocTE5dVoEL3dYHhAF8KrkCbJxW8Pm6utb8RCS2YQ+3CAhKEpcUAWDZZrkwrNtaeKKotS2UE/Ua9w/KVtUIgbain9ALEg41tEhlQT1Mo5AfFhftpbD2JMenU3lyg4ovbFpJSmJ3rLGaVWVZUpgQsxqzVHiOZZVMgu5ecKe+ww9AUrZiQKQ9DZFlr7FoysXLxq3kJeVl3dcMng5ZbMOAcHbflA7kNk95WbM9BMWGpm+RctKmnstMwi3WgDX64OOyCJTJUFokvKyLoPc+jcQUZTXdSuUMxrZ6gMuCCfENJICAZ8MnYQZi9YmjGldE5wSEYe3X3bF1Ie2lB0XrP7VsvuCB2j+qJ+oEWiV1bBxWZiylPNskeMa1uizRJVvKlj6Zlwl+RtYPwb5T6XsUBXZ2Hdwi/KO9kN7MTKXiOnUsDMsJ3oTv5qKI1GMFeabFBV8DAYnpvDbIkKVbNPqoOSKexUsyX6aUBjLxbuHcddTHv2GVoe3b3uYw/ktKLzo0Nhoh4DPjbgsrLB3ueGVI0I5CAVTS1C8B/ew8yQpksFwqWpQFFmLDddQapAhukxb6N0nmWxcrySzyNUDEQYSu0wHWqpAiAD87UWvodHExST20F3LtHIRKwGOCN745VH/tjoRY0gLXGRyZ+ggy0/gzW5m8DVs5/apqoHUJHusnbMy5o5VMjBPFuWzTfsRi8yWJih1qo3rcJQJYYBcQizU9w1lKOL+sZvbHf8RL62U2HwquAQHpOPxXvKwwddYI4G2MsGT3soytpADaVl/nysPhQC+2W7SejZdJaET3xgNIj2LAYF79gyNlgkVCAqu0f7L1hQ/Cpy30WH73Ia4s+7YEcD16IB6cDiOaAxKDnBtJlsxgVvoIInqfjiNApsBItUcuKvyxX4eR3VTZ/5q400pdOX6EO0ZypT6D8zi88kpfAWbZ46Bx7m3Ka9YmACAAAgAElEQVTrST8Qx2a0Gtmb1lCW2UCsYbOadaYoSFYsu5s8GOWDpw0GGoFlZJvzmeErH5q99qYTUT1j7/3WTHhWGjwly44GEo/DdQisSJh0qBGIk+M1bIZiIHggzmev7j4xm+KqddakqIg8niUP9upXWCiO9GULgBoP2zGtScN21vq6eZSefZrSP4o3j906ox+XCDlSjp6RoZ9noHFWBEWnx3OfqBxsRidHGy0Vq+4XMvy6yxuIPIzLYldWppNGnJddLup4n+tb9l0340Y4hS3Z4D7DAvhte9qmvmxDzhWjmsEnujGJwrg2jcPUSQoU5CTNBg2OzW0uREbb5+AX+/HFXEY6JCNMXxv6bRnWplBYw/mDhtDIvWDzf2xD20cgp2H8QqVfY8Q8C/2vs4/lWN++hSm4Lsrrsc0C83uhhUdep2iVoxPMuVSpaI0zMwfEhEkmeQm8JTylov/HnsbybjGNbSvKpSrkJqIglhPwNkMBJWPavUwZD8KAJBVUpP+oPqTHwd5tylrRCGyxHgQMiBGjDXZg1qfB0qauLbDClqPP4wDwJw7Bgl4auSvmzqrObCtpJv1sATNSNag0DXat6JBrF0DZBMAys5U1AB6GToXdkAe3D62BRkBf9LgxtHH64p4eopxXNl4bHTJ3aXwcBJeyBH4Upe+ib8jxsjOFDZkB3oGOl+LG9NqONAGuLM3hCQfl05EjixDO21Ydc3FAMJMKJTnhqgtn7eLpQ2dYsp3VEHjHdjonGyscGrIIVryIL2tUZElGIiKwOO2LLlljAhFHRNqyrvsk+iDjB4bTfyKaa2xGKdiGEyoce4FvSJaACHN0gULVwLLEtwexJ8aC4sBoO6NYP56QjEdYxBEtfshqhrIXgAsRF3bhNbOd1j+SNgBbqtgDAQ43p4L20cyQXqoAKNQn84j6vDwAxsrh2IJjZmdpXo6IfqOzYLvqTF21lEVmtq+YaXk7NDwBwfKjDlPWSJ/JRhLEpyx+bKZhIKQcxBgEwmRxeSvMVCZfoAphbWAoE67K7gBclMAFsd0FsDLeC5FsRkGqwdwPhoHh8FJCL7WmY7crDLp0247BvnLvtLNoHOiPwOjaz9xFyGJB2KQgEnFrtbg/TCWUKSPhjgyL8cbcPeLNdUbDgriQYYE5rEnbwUMVnUmhzLeZa9Fy+dgPgHaU56OxEmbBHq+ZTUY5Acu5HP6ssVon+j5HuVTAdGAqQIgjoq6xQfBtAQ9ZiNGTmc0s+XIxdnYoAIZs3oUWf+lBShWrW02sM1FRIa8duk6cRo2xzc7TEQm3NCRPvERMM3szMNoDpZysps2bjW1z9rXjcPRAYogBL7XrJdR5Cga4/XpFvSaLgpoo57bA6CbV51JX3V9k+GxmtnDTatoqmgauGAvUPowaLWomIVSoI/JgJGPkat6KsdpASGTcHBkv/l2mMniSpgRpjahXOUCP1kq135SqFEOhN9e284QE1LaSOfdawZ/Ibo37FrNobKCFMKYQfEhhbPwSGZJYm2I4j8oh4JHtDcHLymbEoGfzpgqRt0opvdtM0VDW2m9FVCNovZohhHpJ2kgOjiypIL5smZCXtvug62c6syI9KM+GmiDAik6Ow8nbt/Omuu59nKxoxBk95W9KLKsVZDc9ozyjbVwoG07dcLiYZjsXjc66y5T7DPexry+Ll8ohGu7+GD0uQ80tx2QvWwlMrNPHS4tNUVPZqxF3GcKr/qPAWanf1iNdrBkvqpi34HuQODKzgehaOyiHamm8G4DWVZoBiLH1K5k9DVN7SnIndcPRNTJuoMp8YfpAc9lbpCpmOs5wRxJ6QCUygIwT3Zj4DjuIyNmsM7rjFIPk3QxaS+xatpNzGY7HBINmuuN2Lv1S6pxRMKeQEWs3sTBMH2yKlCwn3dk+Xw0mDAxID+W1qfapA0FCX/1YtxpXFjMCOWFNmPZIhkUi18+ukdzOJxRcula4ymOJCxjdaieNz58BYjuQbGSz05Ztg3U2Myg6v1LfNc1JmEXOTDON4tyPwWPP5gLOdosIYzBHcEopKY5b4GjBPdv0+6JSbY86LI0MIAFj2uAzdPqSj0UrGUszA+bDKdyP6NrjNmwhuGR04aG5P+oMryr8mZ3kKVVntDSKshkErUlFRESZDh33BWUxD2NaCwZ2nE4b6udpwHe7OLfw2lrK4p406afGOp5c84lyW2hVcpgW0lhBsJA0MSY3Zo2LLLHZJSmBQ49pvRdGyI11YIWmfrW3N58n8u4ciar31ktMBjI5ODdeLYDuTBtNrYPCWRmFstcFq4IAYrXOZBv6jPeKncdqMcYhYLNr7wwZ6k9oW065/aBkmtBFv9l1p6oO5QHMbvllR3kaD8fhO7A6H1xvPR3cmgcd54qBhizwqRHLbgZGK6ysiow5VsfThELKA+9c41xi4lHRC0+ZcrR19CSwjkYwXa0U5p7zuAlpD5/oHhMDv1LMz8W7TH8wAjThh6RTkcBathLYTQ1XtZs8EKK1Ah7H1t18pGMwHQjK82RYYIwBhOAbx5BhmDdmNQ7QFgG0zi5mhGby5KMrEWmimlBkYIGsURApKzXJKEWL7mSC1szejEg5ciaphmtdZaZo7s8+v9hwjm6niimT8zI+kyGRZuYKwuQxz18KVq4S5EZhbcT2MOvkI3bjjDbDJajIq6BTCXsV9eRi0ePMNKbFz8QlEPDcKCGxA8d74U3fsmcCcT4eVB++abYJMx8cu3XMfcxGrsGey7bn0e+P5dcBEmundlqQBVayfMX0XtknLSMeSLaZQijZwox2uSUQofR0/vu/0eE9jJtOcu7AbsPSlJIsDA0GzCj2wBqcpxKX2g6L2DHtha9MIHXpKlrMrBE9SG68jMN1y2463V/UzvIckjmawyV08ETyQ5HbLDZ60aYl58vF3mc8L8ecvVq6XkxtRalHnFlAEj/fcEgNRI3GpqPJn+qSF/i4UBkbB8xnVqVqgHXUjp89k0UWP/XTETnyDYfM31ZN6nIuGZs6LlcUnIwGtPX8WcvrryyTfVrfS0m11jRZPV/YMbF1tno7mvKKWlQE2ddS03wxsbbqriIhYATeOm9kO4owsZoGNymGr/vwT29/hJw9DhliutSMoQxOsFwAPOVpZxl4AsFhrwg0oueFQruc+6eyj3Vyio+liLKv2bjBDFPh0oX4JrCwBwgVmRWCVyIMPNE4/nKetCecyBGdYMAOVByUtDl6JbSgL2Lt4dh5PqX0WsleWHnv2BKdpmIQGxTB5WKBuTi7W487dOz8WHNuSDjpk0K+g9YMNS4NPpjSeVPCpObz3NlW2r+F0diucTw/t2jGMkSFt1lXmD5YOHEo5LwMeTGTVtfkRnTu2U1TJuReZwPYOzJhKAuD66gWHmY5wGY/RmeQ3TskYsSK90IoGHt6rWDT5CXXg7I10HX5xMzGtKFhrOWLxbgHqTw2mFQDLCGFDBFYIkPJgMEdiva7JcaeJnB6pp+xmwrGpqyIqnSEJd1oWH8rlo4FVh38tIjRXXC37aJbwyMntbQykwxJKC2fcdSG7vYkN7CmsUmKWI8ZbpE+HENg7kGWgeBAL62UNPou2+vEhhyh0nCk0ElAyhEj9rMqykraexQGrTHTMGYTDh9VikDJmIFchI3E0t07NCNdi5FFVmJZ8bor3PNq4FPOtjqBY7vICwsiBuNAYxpaFesdgAmBiSMzsAMsEhODUXVXEILbPhHK+FPGzFcqAlUurH0bvWG3ZRzORVfC+06WZWGBx2yoUlnLtUvcgvV1mSH9JWdwQam0dv1+M2A7SK4VFhMi6GdfVI32lRbRLDA/woGunCKDwSaCs7ir89Bs+xnlQHFGBvzkPCyYDTGGH3qGV8mBRKWYeIAZeqpG44ySBRWsO2kEjOi8/RB7KWWHYCjvbMLHPIKAhin2pZd86Rx9SR2Oq6ATzAek7c2NX0Yjw6AsI40kvLcZnfKcahWlvYqDuC0j+fUxYM6Ea490zaiqCjPfpihzbdIsEJfMPppBQo85FrhI90pDA62dEa5jylwqMYOJZCvrpIQ9xZjsknyDwVo5iZ/n0MFkv8+C9DavWSdCdmWsj2W+nJXJxmTA6eTiEQytFNyWNHIgmc+lWJSTPlBgmFvBwJjDZU79+qkNAnLltcmIczCee/upKKWi/ROrsQCB0Ag4arLqLB0N5Dp9ekBRSuj8z+jGblLRyYaOvOj69my/C1RKSrpsk86BXa1CUUzVyVDrsDH5xmHM6BRVSmESq0ufQTpk3hwSNC7j0cQYFLBqcdRlZIEXtrP9f/CnbBMy3nsoZtEOpZ4K0NvcINn5u7Z0ZihoaAB7W+UyFNR24aQhPXSjKK2LcGc/w9dmBtQdX0svylaEmwalflNFq8Bn2AIPDc6WNWd0wHpzjWaeCBeFxNaJSkGP5txKJDe3hLGHtY9r65uXlXEC/TSywniid7Bley9//HkoYGyOUx2+bBYfOmkZb788dOF1/mHsF8sZQhH2ZNFT7phjSxoHq3+5A6zQWMhTXAqUluv1tkbGBggf2ibpB4kuWQAcY4K5nqi2zsekLg564NfOcy1SfRTEmgVnWx8wrKlTn3ClgKPHmtDO6GAyt1SJEnSGjclqHtZ8WXLdAqZT040KZYmLMu1Fn1maItfMZSD8TGTZkz85G8sdQkmYI4xkasocOMNckeikIHkuQb78te0vSjWhJxycQavtLFTFHAfzBRUtaGmODMMoBLpuk+gLSKOFixIsbwIc4BHnp2iGZpGuQsnYeG9JTNMrGKAcZ4JbZa7VPTVsPeUPdQT4YMACtZQlMMaJkNHvonXEROUyutAJctBPmRSqZr68ebAVkvWMXWngEF0wNRPsY0kC1YarNozkYXO+OT6qC9jLZdLjTc6BkBxlP2lobQdMMzqoq/fxOjxRyV24Z3pcKIOi6dCP1vO2gShmPUIxIFvjTLGal6Cf/2uzXKSe4+Dvp3oLK4J7g9+hSdjEAuoNA8cd7sc+65lRRZB8BeXGC0SZCmkdgWR8BNUUDQkbDzfDC62Z8dYE6TAM2eo2EDFH8MW7irnKfekzlw9LUgKPBbFHoxFc9FiOM31tMTArzaPFUu4YMF0RTcaRCYTROU8tsATG1Lp3GHsMFSScnF9/kBFbUxH5NkPXt4lMNLlZySNN7XFkqIoSVpbLk2d+2RyAfR8TvZeJ6f5QrgwL4oEGxPQIQthwvj3CdedUBxHLfKUSJGQpN0POrzhxDp7UkDWHYEDzZEYT2AVhsrnQRWjUA2G7Ob+OcFHlWzZsG54Fh12patr0kPrgo0GGH97updaGAEtuTdEAgW4g1vKVwEg48vf+RwHE9WPrC8CUHvNpIHNhgiefzdm5CDSkQGxdio0ogbZzU7JkFP2yhIR1zR1V+YQhJqznc7DMYuxaV59NkmgGWw6yEUAaZAjCqDNHH4EIgy0WGIWJ4ATq7gEDTSY/DFaHTVAAxAyvibNZGCKYnMkbMCd8X3GEqQsIDFrOedueYphl0S7GWg5JS25FoLQofhg5xqJ/WKZCaS40yJgrWEDkdA4tMuUrKcdKd+dsZb/EhKXGN1KYR5wwhSUMgLYOWK/hQYXzVanS2BOGi+/aiGhkD/QdT0DC7RrD6dqmCoPVsJjHLZzkGMLezN5PWsVBXoVbjrSYDPLN9YM0qGeKlcRWTgQbJ7zJBGqFnIY+3bCzVlzya9lOBga/qpN9B0o1l64TGzWnFSxc+yQ6WP1kzgMV8aggMWIqP+TkNIRvbSyhgkJdozSz2M8DVqlD9KWkHX7VBdjmMKdy35SCHVw00Q0mQ+V2pJ962Ds86Vj0LqiHsf/yeUP7Vg3PXDYdYLrR6xnPtxYsyDod5AjDUtYxSvJBJkdcjajP5dKTKKEpFe4Rc2MAhtzwIabnJp0WPJBd73qOjBqlz7BCEPEEDguUIffDuVmwDtD2Nl0NWGfVLS8zLD6zkgTrzhNHWLFvGAeeeXcIGYaWCmuI8t81uMh4D7xsP9XT+YLnoT8fhvZDq4NUWI9C1cdsr3s46BzhQj+e1DKsYnUrPVIn1ChWHD4fiOy43MicjccZLiLTNrZjCpa8BOZ4trbm2EyMb3IHUNkeLhlMZQQ0DItGxvONo3NWbXofECFFqUeU9UbjUNu8ntElJ45FxNwKH7bkba+0IanYCpChAY60FTp8ywTumCG4JFAsB1TJZCK+o4V9GI75WkPblvt+ScVkWM8kNLcatmqdxa6Hwt16xJrev6dho5WZ324F34e8K2aLVVtGcHyIOLDDWS3LPLqHaOCaaGlvsGgqKFjDNQG9XZhrH5SbWey+inlvd5SUmmGOjNxbhgBIjLUEjGdoY72+XIsstpZSxGL0SWmotA2YHsuTA4A5J1ToSI5shc/Sf4S+rBGj0QlCU2ubw9G0be7962xPQYw2gMwy834lM+nNpCj5JZVHaTBubqTOC1TN7kb8mjHTwhhhyOaURxmwixl76VWYM/hsmeNrwiU1XiDvrNESgEhjleWpXgXL+uyLR4ylUcmlPFwrFjiaHwBmbHcZmSUZTLSidkxwVRwf4Nl5O0vFd1nSKI+YKw4y45Mq2iubUxKxvDNB1pvdlVGe4yVwZH/lk/2MT4RFY4TE1j6rwWTVKVl0OZAcmQFbsNGujFYzFeiyNrpkR2Y8oIRkXJpIncIwom0C0QlKFes0p6FjM+SUdXywmgSG7oKRPm2W3992qjCQSp5gGLoRGFrc6Czj4QwNVJAztH2BHIdWCL/BbZ6q4IItZ2irLmEx/nedJGOTLXWtZRgLSYPBKo4VNWWp9dFr5TzdIaSSKRkSBGbr/xpzcWYHcV+lLg5rZhveNsq+GCocxrkpiHIyLKEMpjhlH10qG/3WtHDCHIPgmIUMk24Ga6Hxt7h46WhemGEydztUHzkhOSbwqFg4VHBOUBC8aqxUhpHNGMKiZU1yNkgtEIcMkAoqMfmZtM6Gikplozsk2nWnc7FJGsiUr3PhLCwr0fZjmOQzdX+z1gU3+wZ85t7dRCUEXzuLa5qB8HmucOiiaQdkt5yyjtcxulWWe9WZ1D4Hs/ibscRlFK7EgV/983ksXT6sR+MLKwKHF2T7Qe3Rc20ZSZC1Pu96mz7G+oRVkx7iSDqUnJWN8WjQwvkE0Ey7D3NcSdWfJwH6UTVmGT0TtNm7N9R815T5jHJDWKd3P9BcVQSJg4PYA0XpTEU/NLwHFHn3lYYzMXeghG4MjUz4g3Nh7q9n63y6ofdKAlzvdIfOKBYOCslwuWhvg7nfJSkkJoPUJEF1s0uYBMU0i5jmuqrwgpQTa1rAsrqmAdMsRlqmo6caa2qZ95Gl32DbdmIqeUf3gUg2aVB3LLJC7rHe48d8ABwLXOuZFEfUbATH62YvmchtWp5is/2s0cHC31k3qhR2xCJKO3GvGfOzb2TkJg4jsnMwOJrbGlwq+uIpZKUan70bzbGdHT9KLZjH5Psx4Ufg0JMDVm7ReqC7RMmbUShBmkDc/CHrsgRueLJMwBrKahX8Bsd2FhU67hRE4yyhaATXbCmfImIYsJ4SJfYMEIXUSsW578gJn7fZLg8x28FZVx6X7ZrUzlhBNGImZq2uccmZc9DZZ2kMxkrQl+rrvJQlcIpZa+uixvRtC7S1mbeSm+kVnJ898+l1UPJOJz7FKca2VmIEOSNM7zYd5P6qGc6BaBkjwBkGQsk14dlODLrZrv1krt15rSaN8TAJLokdAujXKUl7GaeqhUmyUZxuO9pcQA4zKWGGKQU9WR7A1DelTZyYUasOwsc0SV7e45yho/edfFnPavKdTetlLKUSLslxiMgoFNzWEI0TuXHeceWNiqREYVjS0JMbqqIxRkmR0lKy5LIsPaMYl94AYdlGYq4dAB8FBLiQAU7SJ6UWgUrNplDiKJkkeIsiOhFX1OjROQXTbjQMTtNxUE6ctkZng5UHShrczI7eFD1nWEqDonrOR04VZH2zUCITk+u0j/QjSO21DAlJIUlSOTHMtiWwjPZp2o6/YuLIe3g37Tybi6kHthrnqOiYHJQEjHKdNZft3C9FUVwezoJgB6QYWKkwtrJYq+iDsOykRjdUihUsQZcpx1nKAyxCxRzG9837SyxH6vRZ0DJONWk01P8PAiA5im8tzigZ5AABuYitkByzGFQqiUUVU9/84zmWsJqT1rX1JMazb07StyWxkndIxobcu2168Fu+rzfaRBGFfRLhHcOUA5tjikzXJhky3DAHQt7hJOGTlQJXBQtI79iTHKADA0CDPXJkqM9MJCyPpTqIrwhnzEPvUeCDduPTavrsrBgWxlkDViYWDH/GgjnCbsuYJrh5MbIaWPrB6cqZPMcWOg3pk4x5c5vgWGtbHJ5FE/ykpe4EWQcJokgfLtDRFFigoh5N1qHY3W2k9vYqmNh5road8gysnaqFuhRxGY0StucwN1eVFEJvVzgaVtdYqNi+V1vukEgFOxOwD2fYUHA53cCVnJfnavimi2FhRKT9rpLPrMgI4BdT7RhAPHFk+cgn7EHMqKpZyLYutLZb2+bmizG6Zu7OGkvZUsrDBkw83LhnSNJkwcqI3MEuX3mowLKrzFhJ5gPHVkvyT7EEFpWxenqD3zgfWn42hRkKptDYFT1iNI7XPuZZYK2D3S0OUAgLHhAjSwCTydKqJ3a7MkpL7ElEuhR92NB24LgU/cwsJKx2w4dB5RQ9uO+U/rEXmC6oyskGDE7CfgLny8YiuaJzIrYGDbHUZ7CasmMzpflQcfrdrDBP2QPvqivfwMSyMmwUi0NLXp8XTRM5Qm7FJKSw0XdXW4X7CvNAdAbnY2+KG7uNenZMMm0NGUKjNA8gC/IFyT1XgWBp2WRyN5KTIcqPtPZ4uE0yjIHIKJOV9pjMBDvl39OP9fQzsJcxA4wpkR5Ffx8XBU+F0WYo3tosRHkhqlhIjGLpEvFEZ+/EgCHFhVrgf+z6uDQfIregmjLW+LjRSHNk/y6PUSs6kthyTk7fjDCRTsE+qydU1ptu/v2k0HZbtHbROHWnLgiWmYpcxW8NB1QEgXiAMlSrzKqRgbJs7sMK891sCAsETUnOhXkDyeWHfriUlwjAtxtmckEBE3ZWLuzSEzeNLrThZ1ebSMqhNmiMBBiZs3Mt6LYh4iy5TAYjujyx2FPLEgA1zhAlW4n1ZIIcTwLbkm0kbe8OSV2tVpEfSp1uTxWHxjCZosfMhcUEOw7zJpg/tEIWF22GKkt2xmsieBEyo6iWSfrAog2NMVC50htew+hObiLaQzHDdT2wjDT3dJmPUttVNGp1bGLF4lbC8Jj5MbivGLxqfBhhl98Mma8COaCIeSiQV7COPMpcjZ2ZgbFpbyYoUW4+tAzezHkLjcpsuMJZo8WakPQFBm+PwTKOIWhzchSTimwu7FKJmLEakm35MsBkX1VgeMVkt7GblLLC5PzhF5lvmjKnC2OW5i3oETzz1NqymMF8KEGY/navpefxWSaLUOm8DtueFRKlCaTBRHGYt0U/RaCafKFIxSbbNm97RFOmaxS3LJxuwmBthoNGI+ypwKopIoVrhEQGJgv0MDN936+rH3ziRztCRRli6elWLgVVzQC2WiZ2UwjL4zCojGUHCtGu3u6UI6E0brHTC+37yATWJKG3SZmiMT+6AYI7YK1AXoF0iWyOmSEzWGQZd+3l6gOJJXAN+hQn2qN4BxXZodWOKctnZYBbKr8pjRloz/rlsqaY2Shgi/YmDk/kCGifdGaInpdMiTKl7ALG+hI7QoDyZQ6ZAzi1Rs+ldcZmGnXsxiCTBRNekaRxArYNJhyDkmqWCXZiWTX3RQWF2rPNqNRN2ZJ1lDYT6jiOwPTknF2ywrKbSchGNDSlq+rGWlg3mSwdkRl67w66rAMK1wIB5oBzNSJr/0Y3X6uPUgwYXjwVKpNLOI2j8G+xFW7dVvdrPB9MAulMIjvE1pAcASInRz6WF7M2I4XErNk2rTjG0fsVZzYy+KKFsiVp0BzrHJlcZhlHmlAW7PQrrz9F49OgX8Q4PKEyfjJUqsbaIBz1UgEtBNLCg+aZ4U/kD6qZwoV7SlqjzKUq/egiV+sF/z8WMFtF9PSWvo7xOaMXxwIz+R3GmDL0k4s0k/3osy6rfWUYljQSm2xW2Ocnx4y7S2OsJEvyK15mjCfDGu7Jx3KJoSfrtr/ngh8ULFRNrBgshYFgUYFxhTkLuXSyaiQGo+NsoS70IUU90FXBLXaRZHqMRHZNLoAbsSqeCwFvApFrF2Q4595OuDiY0RICKtO5UBbn0lyGqRSzmaeVUHBhBxjrbFcByjDBmvSBPXNa8jq4Mi2X4S2izIDe71f18ysKovsm0s2kdqWzEnBQYuq2nVKmR1lRs8jsjTnM6VDoEHHWXGa+MtEnLooFWSj63pYBm7TCIDud1WaZQjHOcFkCoB5N0Ctq26pvdQ1sUN3qtwHSM4uiPueXoy+kvCQln/7H0tH0SaayJLF/KgnsiIcNgLXzy1O25KINQmRuNheS8+5Q+yjhpeF60BwELCj8CsWISSRiqd4HdPfMxLqM2JYpLSgI6sVDVtmHPx8jflFehyBGjgOzHH9dCKM5ZELOG9C3mGFkT1tihhm8MewrQreHr9kZJvINmCLFJGRX0XkSMQl55HUEv4LrD45SJg7VDO3rBl2ukb44wTRYzmlZEgyQa5euWhQlF3SDFKSINdxkaHErmo1GN3fcKfKUsAtprdXNyHHyCV0QEFYJFuZi0/VEGr2YEWoxUqPIIyZz1cjODEYUMNlfcXEwYjdegOI1r1j2IptTnw0xeNWVN4K21M2PkkcNgqDdPlfBUkyhfbGgNKrhRQpJtMlWmkYwOQ8ZFlVx6p6XkYhRtQpnvc9IrATaSBZ+s3NNHHNDrwhgYMtc6DOmBqh9BGlMyTxkaaAfl1TYg6l2daLLepcpA2RguEpcb1Fke/Rj5BqRHt1rU7GYxcEo8CZV4loxLLfOvilhU8TP/fyn8LNv+2e3j3/irZ/Ej7/lE1CKMd0AACAASURBVEmWECYSwiF7GiuelIIcS7zOs3FRVc/CoSeP6FSuTd2UIb8NSWDTGX+CjWe8x9KZxq9wyFZmz3nSm/8Ax/zU/0gbPCf8xtmYOVAOM3ClLL0pPTYYM7tQVbMw0Kk4GFnUJgJoKrJGIvZjD22XVBeMWC++5NsnZgBHppxktkuz4lNFsbFQKm78/VD+/iq5cOM+bS3xO3bZeZauVx3kO5Tvj19OR2h5jO5RG6Kut3xWlqF0qUgMHLMH/94vfwW//UtnIXTetyf/+ju/iF/7xS8GhTociM/Ym89Fn77ugnCgucIAbXnqUKrmgQE/+q4z8aKf+DCu+fa97sUvvupOnPLmv8PJb/4gnvPmD+LG2x6ARbmuvH4HjnvTB/CKX/hYKVQtPVeToLA2KChvUNi4Kp5DFMbvMQtrLWsM6K16t40jqoZ+Whassi2/6cwxVJmwugPKD1G31l8Inib9+yr4+Pmh7a4102JjRJYnJRxWLqwK1fjU2CpoStR5kE4hjot2ZtdHersr42Zts/JkGCwzIc5313N9uNljNAMsj5B1fpfv5fu2E4R5VlBBGOS3KnCjiGlFgTH9KMjRE6tytGUVygq8XGHjF1jmSSccCgD4xhV3OLebbVfeBQBYmc7+veXy290C3HLFbQCA5z3zCSWD6zWFfQqcc1g2Dr5VBuEylcrCTBq//WIwORineBaiyuQcI5bPloeBUcbqKmaAkWjoXnc1RKPgtSUWYmqMscEhfJkEwrGp1nrMOTHTDQSjFqcUbuSCm0kzAr5bZ+uE2ysw1sZyjWFBa9VdYze3OZbBY/53S0WCrCVozinAeAscFyx19lOPRpy5OG4WuF6SKkWvslRzhsFGPjOSNEbowg9eMqWvN3dz2YzrMyVw8vGzAHjh5Xe497jw8jsAAK9/6VGzgHfZHe6CbLls9vvnn3h46TbiWNvW1xvssWTkGhyBbVlxCho9/FKnSHB7GUZaShVskl200lRnVtNUzQD4keyHroncaESNKUaV2bLmNEpGV8bqguHwUDzEA87Tna8LGGNk7Xdo0OrH/EEeYRnlyTTO1n7WcqqiPTBYephs05mNsc+DWUZHwJUycFX/1voqX4eKWsZQwXDRZmPsU+JsVpQ+Dm3tWfnKzSUUZmF9/YJbcN5Xb8HNNz+AXTv34MDH74uTTjoMZ5xxFPbbuCGcXGMD7/rvd+9ZxZmfvRZfPf8m3Hr7dwAA33f44/DSFx6JN77m6VjZa5qticyJcNLxh81L3ruwuipMp8Ce3cLFV98FEnjbG4/HJ758LbbOAyIArK4CW6+Y/ft5JzyhLb6du/fgr868HGeecz2uv/U+AMDTnrgZb3zJUXj7Dz0TG/aatmt+yOv/AgBw88feit/+qwvwT+dcix0PPIL7zvyFsMlnmblE3HDH/fiVPzsHX7zwJuyzYQU/8qKn4zd/9mUpR7eZYbIvY2UZXvf2eGx2UrSXBNTOBTyFaX8y80KUzjE371ar8xdbNa3CCpirWlOkodN86c5cslMKozWtwYfAyfwWTWxwT700c5jAjmlFrapPxPjcCcheea7ChUnTZO4kklZ+GHSO0XbOuugsUxhccZFGa7C3a02PW5iWe02GN1iOxuAq42szELAhxffBNSGpAHzg/Zdi25bb3Gvcfdd38fnPXY9LL70L//U3no+N+24oRbWeZJnhUrt2r+J3/+BruOKa7e7h1990H67/8H3YdvEd+G+/fjomK5N24iswkgcfsBFPeuLjcOOtD+Cab9+DY5/6eFxx3XY8snMPjnnyAXjCIY/DcU99PC6/djuuu/k+HHXEZlzz7R24/8GdOPrIA3DQgftCAHbu2oO3vPvz2HLFHe6zXHnDDvzeDTvw5Qtvxod++wew915T9/v/+Idfxplfu35NTGP7/Q/hpb/6Mdxy94MAgO8+sht/+ZlLcfuOhwqqOc+CiMwsK3DWCOOVCB6ZcbN082No4JN2390IzpFUSnB6026qQNO5M5Tgw85fNWRcMUi19fJW/j5qn5+WcaJvH3TnehNYzwKyMw3u3lezgBkqH+tk0zO61XLmSAzEMNm23NEdSoKk5TYkiJBda5YiAGoNfE9Yn8RlrTw+HVad/aORYtACxebm/da7zlo3Hnn+V2/Bti234YAD9sYbf/RYHHvs47Fh7xXcetP9+PuPXIUbbrgPn/r0tfjRNx1XMTIZtSLwz5+/Fldcsx0bN+6Ft/3EM3Hqsw8HSGy56Db8xQcvwSVX3oWPf/abePMPfn/GttrJKZx8/GG48dYHsO3yO3Dc0w7CtsvvBACccvzhAISTjz8Ul1+7HVsuvx1HHbkZX59ng6edcHj7qH995hXYcsUdeNzGDfi/33oqXvHcI0EQ/7LlRrz7Ly/A1y69DX9x5mV455ue5YLT1qvvwId/6wfwohOeiMdt3ODM+W0J+P989ELccveDeNKhj8MHfvVVePbRh+K8K+/G2//4c6mkst2DlJdjpPOvlcRmJ63aC2XbsegxstZhMWnCYfp6F0gylXycikYUb956Yjf0RE7+Q+Pjx2TiMAugfUi5PNkT+nPUgq+VUtO1TzZmmH1OygzeWQ3D17tawMmEmi9YpKp8RkxYtt/MIp63hqrf0AZVWRiodS1Jflzo0mCAa+F765kZvNbwpIpcYR9w3nXRSrYielSHSFe6n3/uLQCAn/v5Z+GkUw7Hpv02YO8NUzztqMfj7W9/1gx3u/D2LM/QOBT61fNuBgC89SdPwCte8mTsv3kfbN5/A1794qfi7T91IgDgy1+7MbRqefaPIE4+4bA5ETILfBdeOQ+AJxz2/3b3tjG3rdd10Bz7nOvrL2zHaWI7TeraTRR/pbXskC85NCk0RYARgkD5U1EUJKSqQpVoKKoQKaACP/oDghSpqoIEiFaqVBAU/hEljSOgaZLSfDQ0DbSJHROTunZtIju+H4MfZ6/1jDHmfPb7XoPE3Vzp+vqc877v2XvtteYz5xhjjlGFoxBW/eTP/3qhqv7atQB+2+9++/lY/eWP/e9VVfVvfd+31Pf+o99Qb33z6+or3/za+oP/2DfWD3zft1ZV1X/9V37Z0+Gq6j/8Vz9Sf+Bbfke94fXPlYWGx8v97/7nZ13iD/6R31vf9Xu+pt74uufrO7/pa+tPf993R5+HdvhxAtR0XSo92eFdsq3hxa1lGBeC/dYA8WM5KLZ5iGpu17N0xm0IuDnxO1Si2jpaVIJtnjDiOLF2erF1lO65MK4zdJJowyzOMD9XpAQj2nV2qUGkFl7iL7iPIvg0xaGr8A2n6gOs7s3OjNMoQmPjFr4yLzb+u//Jd52uxCjUBVUXoP7EH/mRBbhfcZ9P/trnq6rqP/ozP7l9WZ/+9BfaOhYL7b45xoJPfuoZ5vfNH3y7nLrPnrJv+9A76gd/uOoT/8fnF2DNi13XowP40Pu+uoCq/+UXf6N+60sv1c/+r79RF6A+9L6vqiLqQ+/96npyQf3UL3yqXnzp5frpv/mpulxQ3/L+d5zj0t/55Oeqqur3ffPXyUPy7H+/51veWd9fP1H/26/9g3ZgfeR3v6PI68mH6bx+9mL/zqee/fzv/MDX2CbLd7z/ax1ySBExpnMxdkPYWUnbxuECn565WF+L3ctqT4WNgIFeY8pjkE7bs4rg8GPsHPSKsDEVYnuyxmhK8Wbx2fVN15XroX+RkRntDVyv6UXJXHHilrAog0g5BCed5gxsfNGIcWOQCk39zvk6aXvoJff8fQzAVRcXx+bJHac1HsACcWOE3nWQ7F3hGgH28zMm4S1eGQ/10ktDwpiOAa6XGF4+tm+eRGeHr79+0xufr69/51vqN7/wQv23P/LL9YXferG+8V1fUW983fNVxXrD656r9777rfXZz/9W/fc//nfrc7/5pXrfu95ab3rDa5pmCFU39i36L9/yhtfe+sq6CYgS22FhSghiaIxVXAuz1detH88UVvDd1xbZ1vmo/pHY8XNhexYOxs1oQfAtDXvS8ZgWQsVlNw/f4PDNODcTo+y1owW50cm0m1tZaqGt4+x8D+vhwEk/D6GeWJXOnTqtnZb5xN2YIjw1Wfxs6Hab9Y2sg24CusMKAy4lbDtD3QGnTnJ5xnEctd/+jjfWx3/1c/XH/81vrXe96y3Pqv3ROdaSgoA0y3TW/iH/mre9sX7lE5+rn/4bn6rf95F3nmaWZNVP/vVPVlXV177jHzLV/EkQHOTM9aH48PvfXn/77362/vP/5m9WVdWHr+zw8Ta++f1vr5//5U/Xn/1LP/sM//umtxuG/q6veVP9rV/5TP3oT3+ivve7v+FZV3d94f/DX/t4VVX9rt/+ljY6FYK/zHDk65+/621vqr/1ic/Wx37+k/VPf/u7zz/7H3/hE9EdZU4wrBk8ogYAVL1MW+8yDjHY4hWO3udf8CIkAyr3X5GEBPcElwaA674Qbo0y6KTewSYDMNsrinj5xAivHV6v032Hm8bkirFt2Ouvs0TiKy8ywp8n0MtjQ0FpeBj87ugPil30N+phEuDV1AFOzcMreQ+8gRne/DnRrh8B08oIs6+PWQIEOA5wVVUf+a5no+Gf/aG/Xj/xE5+ov//3vlBf+tLL9cILL9evf+o368c/9vH69/+D/6k5+h6u0BO385Fvfzb+/fB/+XP1oz/xK/W5z3+x/sHnv1Q/8rFfqT/353+uqqq++zt+x8pqRcdtjq7mw1dB9G985gtXAuRtxjR+8/ue/fnHf/3zV/zvHbaq9NHvfFaU/r0f/qv1l370b9dnPv/F+vTnvlh/8Ud+qf6d//SvVlXVP/t7f9eATvi+Jjbd0ke/7V1VVfWv/dCP11/52V+r/+sLX6qP/dzH60/+8I9uGsa1SZruNF6YpFzt7jWUOK5M3DLP71ezWS3taXPf/q4UgovNU8Mz05SVHm/Q/MzdWU1IH98YDxg0RniM2dTmfZmrbAyrEAasGju73eRDdLTSgLhutuOjZu122njfSwf4SvR9txhuPoL82NRBXj881UydOFAbnwU3ypNePtdv/Y7fXp/41c/Vx37s4/UX/otfeMQbarN0YB5V/9T3vLt+5m98qn7xlz5d//Gf+5n2U77pPb+t/pk/8A2O3nP2N//ge76qnlxQL73MenJBffC9X2Vj2Qff+7bzz58+vdSHvvGr7Rr84Y++r37spz9RP/WLn6o//oMfa6/l2z/wjvpXPvqB7Y71GuHQ95xR9a//Cx+qv/Bjv1S/+n9+vr77+/+r88//yW/9+vq1v/f59gAiPHycvFDD0OPhvl6jly+1dSBvpBTbQn53phbiIX3/3AUi2sJVxnE2aTilLMjuB0PhRCoXYFm6bd0UkDW0lfFMzLvT2rCDkSAtP+uU2UB53zBa2OyNIskry+g+CrW/JwvpmkSar24WOBr+x6zCNWv7B752/JmoCWYjg1Fr6n6RR3ByvF03zvf+i++tP/rHPlwf/NDb6s1veb4uT1DPPXepd7zjjfX7f/+76gf+7Y+EX97sZHH86dPnntYP/BvfUX/on39/vfPr3lTPPXep1zx3qd/5dW+qf/kPfqD+9J/4R+q55xAnqZ/Kx09//WtfU+/7+q+sqqr3vPut9frXPWeX6g2ve1rve/ezP/893/Db6rXPPye4KOr5557Uf/anvqe+/w99uN7zzq+o5597Us+/5km9551fUX/yX/qH68//qX+8nnv6ZALB7ERPZ+njcn7lm15XP/Zn/rn66Le9q17//NP6ijc+X3/4ez5QP/TH/onhSrvUBBtn8exy0MLQVxcOexg9o8vpC9b0TAMDRgNOGextIDks2eJdmdmBV3oGhikF5Er4AcOBkCudEBkO0SdK4jzA2MLLeDYSagq8kMW0mEhnGj1fXbhzZkkra13LNgt6MCFsvl7l/6D+6Fu57e6w6/AwBE0/1DHCghle/+RJvf7J0/O/b3jy9Nmvnz6t11+ePPvvkyfX339Sr7k8ORngyxXeeBZ4cw00wqHjQl0udf4e6hlTfLk+FeepXpeFCR7WTdeT3LBCrLWly4EZAHUp1uWwnBd33ovYDa2/9/qaBQ88f43h9Rj4jOvX6Z9dX2etkPP1Pdevv/76cvz5EY0poTiXgl1Te/3X9whc6lJVLxL1xS9d6osvPvv3Cy8+qS++gPriC5f64otP6osvor74wpP6wouX6+89+/dLL6Je5KVeeBH1AqtefOlSL7yMZ/99qerFl1EvvHR59t+XsX79UtULL6NeeAn14suX889efOn4+qoXrj/rgqonT1lPL6wnT1hPLs82bJ48ZT19UtdfH39W9fTJy89+72nV0wvrcv0aXK6fPVhP4L/Gk6onVYULn/3epZ7dN6i6XH/vcnl2jS9XDaF+rf68y/n/5fcu15+FZ6/xcn3Nl0vVkye013m5VD19cv2ay8t1eVL19PyeOq/Bsz+/fr9cn6dPn33d04v8/tP1vU+v/z55UnW5VP39F99cn37pzfXpL73l2f9/8c316RfeUp9+8c3Pfv3CW+qzz/9cfea1P1Wffe3P1Gdf+1P1mdf9TLFeuIcO8IFRVoK8K91P+ACBuCl+u05LCRnserEDn9AUK8uAGGQ5rKZhwjKkOxfmVbm/Y30Z/+unc4WZAAeYq5sGoHKfdzQysk7L43fTGXg4vK6Gs8vHEO2TcOIpNpqtgyvz51PYoBuHw+24qseJwrpRdXeBmL7qeLXHVpbBC0UbuO6d5ZiMkdN3SgV23nNi/OVjP6QoK9IAzRBmNXKeNGf5MsIoHgFfaWJFidozJ3q45MXgOMBIo8IocrDOECKEzlB0FVE7M3I/HeClfaab3N3wz3kcLthQ5NpY68PU+cftYxp68uxWHPdjk6r6WIWI11y6LarekfI1zNsgSa6MtJTCFZ0xXKlqgtfzgT1GGw0yN1xnPdlHl6dKOPtf0X6tksl1o4ZvVldBpjqOtq6Wi2QmQ4kzh9f/6Qv517/rKkFhjkzUrd9BbYTBCVrcjWFmt46xcLAlrYE0UMlLvKuySEhdKTGzXyn2pkmkbJVMQCctka6/inWoQnOAj4OUaXbr5g3r1+ldiTgEYO9BDaGPIk+Rx7AiEwR3UwBxe9tjLFocdoCziG6Ca7jRJjh+W6O9eGOuBkeQeMBVVO3AP1yoG+/XF5r8JmC422mXAGWAD5Zy6kgZGd0YQFMOYWo17cBjjNI8Xk92ehTvJw44bMPnDnNWdmv9vA4eEelFthuZsqYUWWIhgbZMwsDX1CJLQnx4A9M1Hzvt1M46RiHvJVg+D62B2AKnj5D9udAij9VdN+PTUsfx24YdBMzOi7W0iEeLCP3Qbuw4bMzW7c1BiBq0uL370QFemqvKdGFYw7ob2l67fy/3kwo26ofDXdcS7mu0noIgx3oSrQ4fM1GTGQ2W2SvZqdofAHH6wkbRfLyIea3Puzv/2afP2yEJSj86CtgcDxojk/PM4kWk+7KqMwYM+3SNC+3xo2v/lMsVGcufD5v8ZzUtte4YsYVwinY7BMLRftq9m25xbsqGImWCpwTmWvSwEx277BwnEQAJX1fiZTwihOo5vCq77Tgb053khT87mTfT2We6uUFONJsiaAcVnOs/DzfkVHI3BbBqjN8alwvkzuVmqfCmPAbDRL0AOntubASwEugNYtzhVNCqVoegoyhkxD4xkbyrpWjA3QzKzSO1iaVAO67iorBmllFsxpVH7OSyREJq38YNGYgZAVducuJ05RCAZ0HWnI0x4GOe6+ul6vCWQ2a/TEqDwdPPks70v+hs/aiSihN2ypVB7ACu67XhAS0BcPlIHk7mKw8YYxd/utXU3MHDDPVdk0p0vn2olZvODc4YEyumYEZPo+RymLo4Rz4Yc34vi3C3YjF5S7rySAt8PNhcWzhLPYCfUoAmc6kY/4pIFqPjFyuxTULGp9wR67AYnQcibKa6CWvYBC2sxMHqo1tLGRU5FIBhswEhXu2Xf7XHpxIQGE8wDmH0CwMUSYmdnRNZ4QcB2M1bBxSk4Ygn5nmsrZEBRZRv0HHWujDjlGKy4Y37DyXaPLX9hx4JHEmktTPsvVE/RDPBGqckpnQFmW6obwd/6WBDDY1duCt71URDvbs0at2/ue6lg/ul7gkEvDzqq4Y9z0aa3BJC33CGNjsm85WDjaRLAnfDuob9WPQi05lRfWKbGz6zw4KNrYjXtLBwFBJrUYOkeNKRD61aOMG7NXcZoXkJlLHYODEtt6KSk9vGf4Qvn82NDcLSywzd/52cTzA9WsGt54pVrnjJvuyKOUXLNwbKVhuzkHOYJCZXa3+9tGJuyYP2tdhmGcZ681mOwY4xKlpxQgykB5xTEjmY0wCCfONJClFP9lArVGQa90F2iNsE5OF6pXkarxYS5KF/cKMoPlTkWCMWmA5p580o4TIHkItIsK82EHMlWMVncZxWkF3j9aRUtyvirX51U/PbDjOHm2aNIc0+HU5uYnMj7fDQ5HDZQDNUF+OIo4k++JmYxn6+IEfk2JCAni1RQREFwDvy8rwWxL0CN1pdjQzOnWxT3KTNlpfcqHp5yGMkLtbrzIIya/zD+KgyBEe73BYrUBpSj5MIYowdDNONfn4f2yVHFyjFNLsCk1XNBmEGiRy2O4lw3I0MprUjN4rdK/UNrNrogrDZeFI2GDLWdTLBts2OmMID9zIXEkbr3m2TGpiizBwdekbo7jojK3ub+bBxIMfYH2Z1vlY9WROUcIpSlCdO7LpwI63V9h00mmC64XUsP9fGELOXNtlcrHO5Yfip/0P12EjcwFTMJflq2imiNlLJrIFn5hQfOYcTwW4Sh2qQMiAIu9wC+GDXWMEB2H0aO0MMEgjVV/HSrYid71ooyPVo42KYaWP1RAfQ5bWUyeJ60Pi7wZ2RIDsCZCp0NY9H2wLK4a5y9MbTpGLPsZ3AUqSOMQ6T9AiQm807zdOVONk+xV/WTpQEwkCW9N2XbdkoojfGEdjOfNKMMKA7YovmEIwHsjV2zUGzjc/MAsP8c9oyfGiY/TOBkiH73GM7TOjsr2FcyWKmpoy5gyZ4phWm3oEziQVKNb9OBzhwWfhfqa4s8w6xG/ce15fZUWFhmMeLYNXaF7ZsFY5Mr3rwqWUddh3IxubOIBpiOu2NrfYpPXwMs0u8EwnMIkGm2Q5DL58XkrVdXn/o9xD7nI1VUl2TqPgV9AH34805YHFgAdWMUs04MefLHlZdNPU8zVeFk5K31KNwzGT0DoQ5scpJLcV1jU/sLU1bhqXB88ozZqTm5C0IVNuXhlgFq0kThrGYuzGK/SB12IoiK2mAoIEoPYLZ2WVKZKeLGnzMPfFLhooK04KRYrMedENxTVHvBRwu0PA7Bk0xGd8rPoPHe+MU37q5xVSVxpHzrWYxqylvzXtRnRnK1Qrkwwbxr7IRuObub+woHmC4H5sjbFsDbM0iRW2vgS1bA4W49xUYxiChOYsK86263s8bU4RPLNYWR7MJkqULFbKm6FHeBERvw4gknICsGWfJEV9geXGbgPKVsXHAHq2xruNgnIuGelLePwfNoh9qjM7iFO2qKJ7+Gaq/YuJPlSSwkVuCoqJXZMh1OqVSQT7px+cLVDy3dCwzQ/+FCNARgeJwORLICDDH+fsZ57rW++xXV4frGmbo7FTXgYYgarREr1fcC5+GzPOuhNA7Nhcb/I61N0mYRuQdKbKljKOAJFkyGTJi/1IpYmZMtVSsjOg0nWFbbNz10heSsJHuGK+xOW01ZtD74VkA3Pc2afIHhPicgQ+eVw8l3aAnAy1BK7s7VTO9Da86lQtlcfQF27VVo6pp1f5Q+8aN7lH3b0+fPHZmWT+vlSzkUCVDdoKelrYAfx3VMcQdJRMf28P0btu6xVRYDZ3dUUQXMei5KLknbQoqC0Oi7VWvnxV2ZvR1SAruO/GedpjdXQe4LWxfJp0D3iRPMHVNMlBx03KujFPETuPqcBgYINtYz3BJK5t3aKHe/XsrmGMdKo8NCsRirBpOrhSukvxZhMoodP1UphZnFwr2ERDTHA/H6kjG++bNg4vTZ8CZ4HIca0Ow2bXlWUDReP6uz6RKEc3cgT4qTqSbmV74FlA1u6c11vlGjkIgnYl3nWDQ4wcWyDhlMH12vfOA0RL79Xq1PGPDHg4D4g4c1XgrLAb+LP7wNMC6ujKd0p67MkNwIOBhR2hWywm9ifsNX7/GlBRSDoVUbwCsDQrCbMgGf0BdXKfsigoSdm5OSNEjO2NpWE8QCJpZK1SdavbadirFN/DAKhF9ZkvimiEFBCGsCwm9Q0msMMobfRVxwW+zA8pyckFHKUzyQ2tJ6C3RAiq2QU1ayyV4M2Cb894w7O0BQg9+ndoWSeCB/XVxc9bLZpBZ/YhtP/1AUDONPAQh98uZKtdnpxmTEsE/EHjENXgKTMCI5xocEl45HKvphRu4r3D0S7shmtkj5oeG9Qj5y46Ngp8u5/mD1jEshxb4DRUjJTdVmvRxzOQ2qZs7CTHfi7b4RHgGqz884birujoG+6aL5Cy7uTWlDPFZcNB6mbgYC09cIyqlGxARN2BW8g4nbJyTK40h3Mm2pYxyZNDOThbCriPf1y7YSGdps1qBYXgnsgfzUBG8rLo1vuruJCJyEWoaXK4dJtzNpZft289LssDlphsGN6Sc3MTYx3YT1n68QYBs7jVcTh5xaE+bAQzWP7ZxiDtahLu1CQJ0IoQPTcW3WONE2hVo7eIr/bPJGVhFgBSmKs9iGGq3Im9atoLhUWzXQBcaGNbvnjkkoomJuLE8iiUiZbCRHAgiBa4bZ6VuN4bfIAL+zFGw+jZDGbtJ6QQxaPNgZoswfwLIylzrTpEekWwFBYzeGf7JImna4hBIxtk0IT5A7YAdU2Vn8CplKgzsluIOFO8PbCweVX86WJzlZNBYKgz3B9n0CdBxSR304z49ozhPSInDXIzonqXTx730f1kAJ40TbhTIm3PvI2hhw/OWJ96aBvtpQsTF1uUcTAnasAE7DBoAwAAAIABJREFUrelTUpLeeoUc0GlExqZEuWQiNlNY+XP9WdX9Wl+bpc/ldj6hYzixXD/9ZbgxW/s2CE3iwOpLPkeeRtdCaKAF7aFva2gnPrp+hu/bRl07pDicJtw07PI/YcR9JsyLxIZQI6544LoMwaTJuPSGApqRR7uf9HxQQis3VDToCMN9BN84SmI+x2lU7PpCulvADzeZ22xIvpf27yyAuzjMAQuyArPLAkbVOA+1MUMY/aPtTpvA9FAaWGdV0zv1r84Z4uwSb3Sb2DlpQ5HAIG1VqzTN0Rg/GNCfu5j2PpCFcgavToF2Ylg1QRhwYwLtJ7ArQgreuzkrovAtCcT1gW5dJ0vsSqNLCASNeRPW6LwM69494BGpegvM0cir6MracHdYPXGOPVR+g9NHwGXSalBCIsMYGteqYd2tmuWcmVKYiYd8ZtfOArG9xjweyAE3vQJUdK0sVDYEOMxwFwXwlhM0osqkceiUAMXNqJxO4sMq6aSoQbJtA8ZLOhwN6hhVfrMhXFyOWyATaCiaOHVCYd+QhGlU6JIQIKJ1fFvNCAco+sOBZ2IDYz0+l7HlQduzholL0IwHzHSUMCMKe2TRIKta8giaMzYVFLfI01XIVnPEQR+KFfnIdU+c+DDSy3G5xkjUeHfvLsf6phZTmXyEr8qQNxr3s24b9ZVJRgF1F56YDg5d4ANAe5pfbOj8ItLwwkkRQvKcuSkLU3QEHz37vYoKILfP1jB7coMDcsMuNrIsqh2m2XaEDV2gOl11Z7BOmr4iCcw09+yVtNxdxQB+u8npPSRr7ia9OovWjEawWC9KxM/j0DPM8CnTtzGwmkb6ROfB1lXWNqMlrRRPORIzLL3kMFCX75UV7CqE7hTD1pGbKMM6dN1MQa4AhrbPuD/2jv0WRMSEcpjgYmKlOab7PjqqC/eb1jUnEawRHBklILvHOrCC2a3iNLhdgw3dZahh3VXzKha6W/qrewTetGHc4Hu4wX+kNf5EirAmd/ielNOOHR+/zqIR3pM2TtKxMXeGY++tMoAH1d2SFZuj4F7gpkHFuAHhdxI7voPJfCgQfMzQwfLPO4oaetYD/EHmY3YaJ03yeLjUgMzXknbEpj5kw9+DjEIDWQuwP3R0y9y2OitZ82R97AObk3iakUKJLPY2DhluVcvwtjUPfVPGhin4uccGFR12VhHEdaocBuxdPnu2TvbGOCUX+pZzvtd6OMhY97QLfAuxHOXee0x9V+TGwjk4/O5yZu3Mhxt36omJuJtMa8Z4RDmRCK10DX50ZfvDbfV+stNigObKgMsInKbDQBcn94y4yRqLp1h3YamqM/NtCESnj6HxP78XG8LE6irmbgeeB55LimjWahKgjhVkRcHFuMOYS9ylyZDbI4q5uDToiqOMyRxMaanax1wWnk0NzXGlua/Qrwvke9g6XV0fwabPnFP6kNsjKveSYDAObkgOd1Q5NhHGJnfFAvOB8fWxe75Z4DaiajZQpree3MlqzlD0G0UZtHsDtp7G9kEZbAIGS5tPZgD4bGigiErZxsQFHsPkL/2YxYzPoZ/0aH0iQ4YxeNYpk1g1TgIaB4n2kHZG+SYByBtR05USnBPR9euJ9KJDGBNFxxNsusmwggUyAwb0NLu2B32Ovh5/4Kt2fiqOg46iABDRCb3gJEyTBKFup2B7T10JEGxIT7BFnTa2erK3Y8RI3KUj9Bhu9EDntyuWuxU7DKMTet6EN09KLMynXWSetfUwRpUj2P9uOgsIpqaQLYcXovfjY06LJo2QxA8MItoxQGrybYtO+Nzt5KnH087Gvnd8zuWogBd6Dkwloe7Q4Vh9YEiyYUGkt+A8ICx5CDuSn7ggV4sJbeFTmQBsBmaYxCmgtrY9swoaG3bduugJ2mGP48RZ7OVuuo6xjS8bCEn2mhs3kqnzjehIIRfAYWNGhaw84QIiiuDdZILseuVbYmc88HW8YdPDSsvgxuSuMuhrY5aTtr3GcC0dqxtvDU4gBQ1cQE0BbC2pjhMZiM15sIkSpS/Xq3yD8PG1gc7wkV79CBVfU+cRBEY6haqoFQSasycHB2icDi2NIuJEA6AZ6HRjcTfAQBiWcpwYeJ6sDjNyylXqk6p+pkj3ZcEu2roj+mGiGB8lR1CF4UCDSnW8d6EKAsOOWo7ytcOUTFXfvDoPLuSBFBi0daK6RrrJN8H9uCFcZqO0W90eR4HvzeqZe6TMf9l437yxanDvwKZvcZzEU9UsKnA4hY1kEdfcOpyHqb58DYKpCt52rXnV7JZjtk8eNwj2y5fjKUxH2LskHaUYeJSrfnxUJxDCnRgXEXBGVDR9ILA5MRGfG9uvRM6DISR9RF1ELRDxnNM9xY2N/NbTDlFsawhnGrguGvgLW4RB3Nqs0LNS0nN0QQCH9tXa5YmNvIrFY8OIixFGgy4hVm4JcsPuu2cv/FLmdXc3dlhto/w2htMYX9YDcoHqnm2wYLVnBWWwNWEUm8y49aga5XdFiRygCz2u9ebIysAQIaMxJ9pcnXUxknJVbXc2b1w0bjaxsRw7U6SsWjp3t5sfTjvb0nlFMS75jGaLyLBpqV4sNPiKQ0St9SenpT+WN2QjrTzRzQYNqAfioNpQd5bqWbiqydQ85DNdrYnMNShLXpuGYh3ieUlsOw/q3H7SZL3DAAJwgkjFQKxmGssRhMW4rUw5/CYmdLnvhIYi7wvgbojgy2B1W1sdQT+qR3D4wSKqQTucOMNAbGsKak5Xd80KGVaHylPsKbgSZa0qQV99Gbry1K2/3axhFIRD+O6E4tDn9sX+iR0JNNuE5s7RrMshwPYxXLcDK7wPTR7C6LTSUzqws9hSwCY3WtcZwd7cz/aTV0wznFpshZEd2mAGu3O+nS3wyDpJec9A0GdyiJkvYkwl7MEeAMyMoj874VLUTlLWqF0g99lskzG5jNyTD6KSMPtVuuhqeD/rcJcHBdBJXU2t7T6FcF8U8yIziQQfRc6bDWvFm0lQXItE28rlOjEpAJglcZnLd1dKQVxgoB0CNweARhPGAi1rXoNBLJxPtlC0wCfIvmwUmFPK0N07VhcboxOSHEYUQfponYx73viIh776g7VW4jjAuxD8lG38D4R1FcQ2sbPBmDacsNy3r/zQiNVYmQwi14bsmCFcc3CuTzLctvN1K8iaOhmbdiaSrY+gaKRZNGqHqQj0154KqJ0tKqz6m4/eva3C3cIAVag17bvcGoMfSo3zZ6QnkcKxE8cA1dQUfloNAt+dWWNCjRxTlsJ44SAtAIfvNl1PG93IgcmV+ieJXYpvnfq+YbcKo6GDWpTjQZ0WY8R2xc1yQsEYSM9NmBv7RMURdbNsZRun0+d02Chig5thbt2W2XEkD6JcFG3Em8a1wjBj0A0deBbzCCqmF4rJooOp5hd5KM2dBhamNMEAYG0wQMR3QHSg6OmfgAzZyXTHUoId8ndkB12ZCfJg11bDOtuNURe3fq6gPZwsOzs3nyp2VtpqrU4LSO6RFnqU3ejkJgJ6QloyjocZAbSDvBanlemwOtclCZmyLGEg8lr6l5NeHMqA6dxC5K1I93yakLB9POejcePyu8MM5s/a2mWtf1jmCtklTQcRhhsImAaIwkPjiBYPiNTtjFJVhnZa3B3qbXTvwLQfDI9tFYHxIpno3x/s8bLmQuiCcBYwdc9BTaHw1eMShI0+qvkRGUGqP2ey5PCzDGEurHj53WCAt3C+W2PuA9hCb2k2ALRhP7orGiC6OUjD+Y0bXYxLCgZ5Jvvy1OGtx4HSU4NMA6lt2o4uVfNigZpTei1FqcUgm9wB7jY9F5Llkui42QLjqetU40e9AnhOkW0IzMdGT/SUSN+/9GE8ZTqqg1MWLJxdGJOC6eJga2e+JTbYlZ3XIbqvSbJnus293pMIgoHHoUjzL8SGKQblZwB91KXLFdSmf8Lu3bZeulwzBBYs9nR9kc+/LQEvJF7Ne10acG+O0De2Nh4kOvhA1zjRjiG+4rVrOtLWWreHga5EDnzS0dAxnhrQRVbP14UAPVvXi4q9TKzRCZTMEQ74Hh3LtJEBqe+TQsFOB4NNBBbUtbKL8jDGPq5KRmAtplhXxed4i0RfWOnCutyh+Nb+uc+6o1qJM/+cK4ftVt6aR/h4Pm7PDFHMZjVlhqISMKRdIlpMy8Cmc6kMFDaxmM8OTZ1JDmLmS6qw2kdhBnFiRD+G7tnYwqnrkVS8O/rn8jin5023h1dQJEfQL1grLHujhMs9IhHNZorijtDs1GGokmAwas/uA/Nh6cQh+Jngfk06d97SSXcj6nNWWWBzu+e4zV5FPOkMXK0iBkrXAiNHdO7EMRQX9LfoNpIrTpEYTpr11FqXq4H3EF8XluxHw/2/V3GPm1WzntFhGKhRBKuF2KcLnIZJAZivOBxWOK8n3VbfPRaUxIvCHpj6xDnAzBlWx0kdd9n34jg8A/35y65/I3HokoZ7GoEfKHoYxlo+VCSxvRaMNh2tguQ4Q+nb2G8Qv3Ml5rGp6VpqViHA8yY0DjcYwuc9af+5uWOZ7QsXcM4E8JkuxJGTJsv+E/PCyLKEjqS5NlYh5E1mshbDy9gwAFuc8poQ6Z8aJhsgLLYcxXDeSW2hwANNy82RWAGdpWfTf9Lupm5egxFewFQYrMbQN3gaK7iIFc2oXi7OAvNkzSYte2PKoz7vOYTZCHoaFOKmZxfJNvKp2fbTaZ176gEvD3ZtWxaXG2IjWOOapREYnH59tF1ecYqrnfgI0VA9TnT2NNn7vk/fncQCxalJZhYNCcFf2LSJwP5S6QAC1Q0HWN+jAERuEkGyNBlH8IPq44nZZ2dlIZfpz9qGdhf8+e9voAMy5kuUhQhx4KcR0z3i/upGBOjSzMrAo261jCQETj336tRy80X3qnUN0MlpbGZvFGaORlAAN9/lGLnA2D5CH53jvsPgJ7SEgGKCW74O6pkzffTtd8q9miHUA/gebhXMQWXJR1iAyImY6zwZUd6E1OJE4iHckIcrdheH0BYHrtFwS5gbi7g1Z9ZH1vHoKk9G0P4Kegh1dIuQLYH1XEWGcWKuMuh79C2a1s7HJNjrA8PVQpVQiPwT5j5VJtPtJoXVSc0Hx5LfVPgemp8i6FnC6UqS57HAKlNEgO/2e5eFwyxg0yuiuQjoCE8LuLdPnlx2Vep809ZVKYSLWQFv0gQ4Q/HlsIPPGgzPDncpB2dGnnclg9ki2XUDqI4xZjxubxfWlkqG+JAyJDyc+ZeQemO6KhgQiMZuji4kJ/Y/heCUr5u5CX8K9CtjA9VpWdnhWWma740BnfV9WhPWciLheaNz165Fgt0hZA/Qu1HWttP0DJWe8KZdkjkwsUnelqKT2KwYIuIFpCMCt8XOHGIEGuCAU+t9Q/jI55KsrmKAhb+X7fRqWUFt8tBr3mA5OlQd42HDQTQO2Fqb9s9xkzRM9O4RoKC9uLMRGI8R9VXN1bHcR2zAJGrLFnL8Ocwc3hugJPun1uIYM2nrHMcwcDJQA0w/jRliXujNC8rCFKMHwMgZWdSgQQWUTiUPhL794jdhHsWO6vcsY++mRkzpXMm64mUN+JsPOwqOe4a850hm8EI1j0eMiP/UZYtBp3UtYZsFL47a8TvRdP1MMURByrwHNiBADtu098XpUK4bSSkydqQgjTXiekUxTPmkTiXE3qeP4dTjms/qZgmDXrOrHu7GDIE3itSu9sx26TGH9R86uKFwahN5g13F/AWUjsJ1dnIjc4AsI7nNz+TBxADbEu43Kvpr13G9b7ek7AC2qjemvtWSp5wYm+wje1/BDdnP6GjdTol2svsKFMAsJdEs9zw7feAJHxkh7iaY/MYmrdOVgFKZDIExiAikj4vsqYDr8EPEezBvGf9OMogHzJIhusciGwBz7Kkj9pHDRNeYO1R1sKhNPS1WwgiqRNI9KJ2aW8ONe7roiHiXGOAjjEzbNshD3oDYVZ5lad/yzxBMnjzU9ISh2ml42opODQWaCKyuwbsTZl8VJWLqMqvDYWcBYY7UA94KWz2K7GJEZxAGAW742Ue8eVsNa5NFyhXOcZhhZOMuJC1cO28nOtOrYyBj44OSDrddbBAt4wqyw4BcqFEBGjCbRX1Jc/bLglOWMW7s0DMS8SowaTzgju7jtWss9T3mZ13jsFtmRDIkzqzDd4ygFWyejhWq5dd9BqPvPMp30MFj7P9HzLC39GVmlrGDKTcTmPuP8E0sNn7LMLM8cXF0jKwh9OdEbEy31TlQDoOuq+kXox26OohsBZF7nhsSQ1C8SiKgq3RSfdV2n4l7ib/fEi2z74Xy+Lly4meYvWKVwDavIw9HtRc7xPAzCcdNAKFm3055JCiX1nuOsUZ3MvIBWv3lMNFgsvOPN4uB5IJrWCcTJsuc5g2GEhgOfbrHYpjSADtU3jN59NA8CCA1U/AAdtyVDuaygdce988t55ddcZwweG6cPtLufAjXQI91tuJDVjcsGF4Qgrhu7N+xxsZMbo02ZCIFWNt8CRLLfohhHEO28B5f70J0JZy7UBmPkYWC9I6TcAAAHlwPI3+cVvUmxmPYEU4rPeJRvQ3z/MWAO3LIjPQc5Ir+2fzAbHsnEFvFYAVKRWtJ9aZBj3QgB/QEQrRgYIy13kMOiTKp0XHINERZI1XpxIruLKMi8CnT9/TaaVQqlH2uteMdmOH9CqFzkfbmaLvp8ioLx752yhDiCNFoM8W2NeVFAJGZCWfQoF6dkA/Z5y1bbg/ssaeLXXVkYGBqiAk9HjrVwcnD21bgxNlm2vlUEmRGCxH6xTrHKQ3OIQ+PuuPrRPZyvZYthkNagGjUxRIOQoKE8WqsxaUPZ/ortOLJPuVag8fJzMGBZQpXQ5lnmZZaYxj8jBFOJHNWF7O9RR9BaZkjErVJYZOZWDCkKNVp1rEWPGhSIAsIYw3piTmMsCrymJ/992Upqqi7cUKoYxUOj6ls++57HoN588fMLBcHR1kuX7rAImKt3YKv7eRFNzOo2DEhelD1aNVq3nBrbB8LPgfAXgM2QcdaIiCWuXuLHpx+dG4kIh9krWWYhksVzSn+1u62CbqdUTXWmEfHx2rKnSh8Filq2hdO9My6QzhZgM4P62l11fn2sWgx0/PI2Fpk5Tataz5DDzSY+HozQbtrzaKN1IVcz5KGxFY2yCcE9o01ZyNbUlazrORmCYxmm+RJhXOtkFuc+VXKAteNBeeH/rmVJYKJjXiQ1aQUFEKCnYGbWBIjcLr54iJGjSwwnOKMJtFsNS86qnZQj9c0QrWvO5/Ua8eJNjojO9r8qET5DQz8H0Wfdebd8lyaZ67/WYaH7yLT3s8CvrUsMB7OxlFhXMvpf2e7lRZBYxAcYHZfiggSDjm7pKYaY9oDgFT+54wwyWn6rgBsT2zUjvmYPnxdENk2XovKVKw8eiG3hzlMApp1s3PHYWjWDUvGjd1XxDN9N47QE1v7EHj9mK/fGcpt7DmI2PCHtNkWiUk70JgYVG5UmKMVhnYuyIF8RA5nXGyK+1ELuPAS8/K0wuwjhHeb7MFM0jUxHR5igZX04gNzJhfzA8BH3+zUyUq+0rM65LVLQcJk22XFyt9iZrL01zHA0XF9kESZFjzryg79ot4/cL07nFCo3WDUv8yJE0RTi95DuqG/jq2M5DnZDmmXgbLlwyHYGcN1VSOGmcg5cL7Jsxaj+8ZCxFWofn8ymK3/Hx4xAr9y5idvsWM5n+ICspatQys3nHi5lYB6AKO0w4uuzNduAvvVFhVUn4Mt1/7wwS6aALu8aO9wUl9Z9VBw3diYOqWz+8vVqUqDWH3P8/tEW3afgml3yAfMbOZkjsFdHI8VF2YrKYFT6m0DUSuwnEny7QS1ued1S8j7fTU6S51iwiD2kG+iIrCBg9ZbYewiV1nmbmBzfT+zKj0dLdOli6gE8kBzHT8F/RzUhSalgh2a3rje2y4wbtzI4COIEN72BNzWUHYu8wyBVkGmipm7N5/folcyYbK4OgkO1mxKmQVFrN+PcB2oKn5uadByPjJqPKyUFKNMTU84jcgj3K7tiQeCZg92A9XuY3/T+i1hBFJHUf39eTAZNUJXChraPQO0nE5/bRPzzWgHOeEErRoZs+tkWuT0WuxLS2cZ1wu5yThAi3EqcQK6tc5JgSvochf2CFIlidKuDLZLHml19H7O85rDeg4vS1a1wARja3MXHSAfX7W0wLP25ga38MDBD5NxyhRnxi0fivTKO7IMoDGO6DcqdlhfvBeGeBsxcq1AoBm60ht1jl0U/C8ycHCysCJyHRtaL0wQLB5h206gyWzPLmLKVq5IDDPPK5WpTL6GB7fS5Su5w73yNSLq0YFdN29trjkMJjSKVGRKO2kyYGKC5aEto8P3h6YDWjNBCJeWKCHEAQfF2mrRYEPdRS5v8PvfTy90JhEK6Q8xtRVDyPz1OqaNvzwhdU9CwMvNp3/3Z7dCjsYNkdipJJYrSoD/5wMmqv6tM3To15TJpFrnt30OhFfbukkb+msYCW7i9I4c9Aw0KzSEE8QHQTFQMabnmjDIhg0NfjpcbeQM317JGGKQ3A3d4unvCrvWTS4kxQVpqjp4zDVW8pw3kbj/jM1R7Zmw7O7jkG3BV+4a4LJpKt++p/Bat0mcpMloo2nW99Obc/b2vN9ZXWMppNoSrK+1PiOgML3fCX6BO/sgtbtsYy/urAe8jJ3dK2gGm/3XxAo3WzSWisGOMWCFx6w/Xw+w6NQHJ40D02juvxb2zWC2YGtBS6uGMSYAYTuuf1evJwxLK43cxLlyZtl1Eao+J6C50QMSg4UA6KpPQ5i6et9tTC1qZ/CEIXRH4kV30ijDQMcwxzK0jimNkgc7bQXnXEhvrklb31NZ0MjV0YPKq6XFSRc/Rj5EFCl859in9UW0bZY5W/6Mbmy00YMQ+ZO8Di65zcqCSSG1hrcLzIiydU+gd4pon2TdYQHkhtx4iNJ+KAh5p0ZF7Kbms2WCZPp5NxQYC0qiL0GVECpZT7LSsMllyhfdofgfzjyTGh5vxkpSekbYLgCHQ0QlCKHfw2hLztZVIXBND4kKHeP5UDqjfNqss1y4fnWKxk6pxNGJfUBO4HKbwEgR62Ldv9ZFwHYqDWal2KQbGmkFmDmojbX2+Tw09nHBMlzpeHqQYngxdm+gH0eVudYNb9ZxuiciAoGRszv9MJ4/gw1OiAkGnrR1xrspgAFq9jSvXXEbNtdv3RPmicc2VihO7tkQOpm6zoISJ3k8NOoQrJm2hXnCXytp6IF21GSK2i0vOXutI/3VAWRlFsPOjRwd0v14uqG6hZFQrqLD4MhEskZnq63HvXol6gjE7RnXzVQs8rsJNiqLGva4imVA1eSYHOoAlIuyESuPnWI5pTQzGRMVFLPXoR7GbkFrNt1VKcI/tc/MoBVHoFrch68NcpfwSE19wVqVAwQm4TADVKUUrR2zuFdHaFQ9KvR3Wwz5OJyQiEmlp91j82M7TO6xmjCnimpGkKAXM/tAJYZwmaLQTlErULh1YdwhI1X6oLN1drKHsHsEuBuZwgGTwMBktESHcdymjeiIyV4C0LEIJ+6MM1rFc1ySk5cwPB5z7JCb/A5GxDAsxqy4EC0ma73v7K/zdcJeIEdn2dosQ8noiU6sINfi0giVaXo63BHgWqzUvWHWKKg2p6Uzk7puGBv0jGZ3rcYG5riLEXgIrpmoxy+3wOvswIZfV1/jwax5s+4oGV02yUOvVWpDnsUAzS2lqswVxkwJTqaOSxwcD6ha07MyzH3CoXR3Mx5ODJXldGxZA6UiOxwwOsYpk6qSCd5YsJqu9XFjgIkhqyCuW6sUG2aeJTuu8QhDr+eSTfkwoylpx2elmGFncacAv/Tx4/B1LRs4Q7fSl09crxVyGc9XHfHtkqVnUCf1DmoHUaSar7ASTgzv63OfnS1QyeAG3OsIPHVrt+fYRxAnqPGuxrQ8TnFCZs9jiG/mZhzFeD4yXM7lJLbdUFYNkUsaR2nSAbWoJ/oo0qFMG5NvEYk1Fdx2PWkFYmq9MCTvIHBQSFFyF2qOnd3a+9xYX5HxgMq/2MXnYMZBIewsK0E4cbe5GoqaSF0tb9m6qLaBpIv9gZmRyVJ3qSXCWw1T2rpBOQszo4i7WRwttkCfmtzDkdKRsQ8BRDtYOE1tGnYI714d4tERDCsgnf9/KYBbd0UPn2lHyc4hOhbg2Zi8MfRB8A9ER8PZfIKo7qqV+8iTgr0absY2k02fb1tuCicX1kw7bvwkOHQM6PtYpsijmqVC9Hls2eurfITjzNTlHy1WAHc0WynuyqD49TkuxVvyUyMiXEmthWgZJdC2Q56N5Bj8+SANn3ZiNKdldtN5EV5jgHmaC8SIbuaKGcyea5h+JlQKFfIepKn9/EBumDaAHZSt7roDMmJpxaCXF4cf74QIvvw/+3bexj4eSpkLRTqHdv8Mk4GPag2g5qTUR2PZToNU1LhfOdzOxjpzu+OMfZE6/j72BsdkCdomyjXpNgcCH2DY4JgApMFfrNnWD7KZvg6VwP96jZSF+wUhsWnOajfJD5sgBvuDgTn5Qw7Dalc2DKNjasQG/LKDdHGzvH9EXvXROS+DBJirDsN6n52GDblNYGhau8AbwQbIdXIPMsdmsGuel0LctUXvQ2WDLTF3XzLoWyzwY9/VK/19bSA5qYfkJjdjg5D/G6gs7AYxH7XZzbWRsPr6H/rIrfSLbCfJzU8/4Yeg6daDYXWyhO4MZ9ANA9QMf0I1PDi6J7hmkWKnzy7pk5GaIyzs9lZo1AAHl5i5/4mQeqb42dndLFgT6bREv/r9CB8zWjlLhoFtzQariGMuoBMk7rkvtNBziDidgqP6NzruiArDUzugNtKn8UdGWM3husQZfTDzxgw7wkRuYpjq7qEAJiU5PRwziOEn2i0SOY/8cc8YDbPoAqYsl1oEVFi8hy3He2KA1vReOTGk5HK0AAPjYLyXUy4oILf2zCyUfmBzinks1x16CJAX8pY5tMl4wTYZQ8rVFdc5AAAgAElEQVTb1QeOad0V+SmWrIb5VtPrHVFMa8A9zwGOmKPuJPSQ9wELsE6I7kRdKop2jagbuMLyehmVB+cIvbaQVvFCS+XDELmJY/wP7I6bbrq3D+wGp+KqzrV/1zCa5ZhDMT9wMueU02TOy6u6AGLqlupxfl6NleTtFbkBfeVgNT9WIbmh0ytg5WusG78FvOTciRuv9zR+RDRffu5zrJ4uroPgkWrMqmthxODuLg3EWfROFi9XIWIXN2UK2O9VNwghCi/FrYQ3kRCYbMWG0yMJjWtYduG1jK6cnO10uZEm2Tl0nYch5xqT/e/JXQvQxRqQSNZcmvFuGRNhLXM5hiZVjGljYiDWaDsm1zTFdvpeDvBRpX0Flha1NcMi5rtKYKYNpP4Ueh047s3CRnVxFyPwK12D4zzVjCHpTYwJiQecRlsfW2nxjwHimhhUxczJ0MyZthyKGBOph7uw2INSFUE6MxtrecS5Hj1MJyVsIOhpwAsSFcaJ6gDjYzIkdGhVYLdpN4Ul+vXBANlRLEeYokK7rJwT+qZw9eywwqiVe2jrrFrNYDVso9pWjnZMrMD18qwcbLCATqgQs8Y899WJ5qXgDDAlJiKG750oMCRiGEf7au0kJCDMOkBoF0v7XrfAwl1Fo19uJr/dOvYxzCv1iC7SgFm6RCIWxXWkAxgREBpMPoHCR6cE9y2Bd4bEEGEu8NCRGrcegiUQZs55MYY6QsbYiFAyfIly3cU49YwckAeaIw6T9zheN+jjmeoLByCexqJz5hipXS2kSDPs5LVYSNbI1OifpgxDNAGn9lX0lYpbhqdfWwtE7+g3uadnoBPkczhFOUgskAMRFu/BzgAG8mknUqUci/DXdW4vRcB8Y9NrVyPlYIbY+0cioRGAsXbHaA7uyw+wHbubzm8EPON4fOh7AnzNjtFGIwWgxw6k24JAlcZYC/RZ289ikO8F3XGZckIuNpkW89HgdQyXqFq0tuT8+l1rHmwcOiJb1Ifja8qJItjl6ItoLHXIWOBd5lm6NFsCuhGhweBoxWvKtQ8zsPOincqQ0FnO0blshRPoLSYVtzrh2tTZ4bTZP52FBsyt0GPjVaZFPsQeBp4Jrp0bcuyG7bs4pOnEvjOGVCjblkuLOFbb58am688HHI3RvhMG5OwAMSCqfGgM5sO/xS3gVMteh74ShWrM3iF+pYl+IxJSraTOMRCCBznw7T8rPrjISIH8l0KFpFkkboLSycLCcn4p4zyuGyXwhb+tTEg7teXcEWMPMBIH1vui21K3c8GKcCJXV2E0uz2YduFm1FLqcI1z0yAf+6bmHJzIqaMiOn5MA+AwWmstE9TI6oWHP7K8S3Jim33sta7fc3VXFOmULRzXniI3qi669xD1sr1cTKRmQh2c+HeKFDQF2M4UesrjvXSAo1ITMwv8ZcpeBtBFIh1r8MFzo3LNsoA/OeElqJBi7HUeZIQEifeQc/k77DRADL9sGEmHR3UfE56wFvvLa1SdLxwH72vHuhjjj/vnQbpN7k4pOq7KHOUQurw42c6+FlyWZlaHcO4Rc5BunFm2QMzG8M767Fi6gRTUAsuSAieaIKIwazJwMLJ+FfoTv5DfwwRqxucKfx/GhLMMCtFqaGJ6uBlBxf1ATMx4rl2pzVy5WYSG9JHp0h9v0Y1esY+JfBV3gLcY3h3x8Vis8KG6eK4eMc8ce7BAd0XuCWMr/cwZOhijlrrPOS6EZghpyBJqcPYtV5ZSe8cZF0inYjSPww12VW5YSeoTFjbdGlSn60rWbQYwBh3PNEBnMjKF51kYVuahPEi2DBhxsCWz4NA1y9WZtq18084ZzsJMAkz3PfyoOTSVueiUfF+KcIxROX6XsO0+tG512d4Di+AHA+Y5JqjMxmaNoZapz8y2lSI5OMXOkOeqsb4T1J8d6V2wwJhX2G6NwLc2PbQFw66PNx4kk32L7cHoWuCtNrE9kDomwm5KVEtKPUcbDOzjia1w6lS96DDFvGTP86hZlIzWo/RcE80lgRSOFrh0PBQIUQlqHrko+6lhfnpEaqI1jWzxkN0ggN6xskFUC8gHWhB7G6eHnavT/uyYFOgBH+DAQFOS11i+/1yO9fnCBMdKiGBw1eOW8hemh0+pD6JmBzMkU8QwFgd5N7iEd8faafXOXxiu5BkyPdBw5+mAqztahQMfYXj6Cn4iblTHKfsFKFd3sj8cA+69QF+eN4EKRGCBEavwjaxWY2oZD4WspulaLDD4SfUYSRs/GE7WR0GrYAYtiY05ZJauoZj9PzgiF2QH0hPYXh0W/dpJRq2vJapBLUbDILTQHReqW5ASIkM+aB3ycL0RyIPs9x5FnI5kwqUgQUOuZmyQ2dHAV95q2LCZOi2eAehweRIm8wcOXVRMQLY6OTwloTd18ISbKQ/j6UXQDIcT+11Y/b0Z4ucmSOZgfFlZlzeCkZknhmMkRmLgAf2N59/YPucJ9iPPLG+zGqvF7uV+XhKugugPM8bxmYmvVCR1r4i31TGAW3SNDtF1drCZC9JwzvM8AJuslgrtQ4BsxEmUvp8Q98bsvDk06RigZYELLIBctmdoQmIGux7QgxQqRkqc7VCHacVa9VrXIGvEEevJIMMagpwEXamYPZUAXvwsMVjlTKhyPR87naTOPNCtlo1fEg0ktuJ5frKMTULU8oBsyB/uMBaTN9q9XRG6KY7mvnPsNsCRJvaYKXsIpFG7nqru0cbq+CITaytjtxCANcJn9Cbbb8utuYYhRzM4hk6n+/X5zgY/QE5I5mlS2gtNRfKYaRMt7Y19sNEuzR49HS/dfcbPFtgY6m7yYrsFNvy3QretBgFJ1JSRZb1TqQZA6OYLtjHSancF+f9mgBD5Hz4myvtp9wpilzfdjLrtGIevMjiiHfscQBbJkE6SblvHtIunWeube8/ddIDYkBt8hSMxH+gODTu6MoVmuc4bFxy13bKlkwvT2xnSPlanGJmtFIpuvyFIe8BAThHB65RuNh1SkgmjCpnXIbG9zVZCQ6EEPMXG2JbBKmd/Ylf+RgfqYliEC/aSGZ3ynMBybXga/PUPPGrvAXkVrQt7WbEOxnaoc4mJtcAy6U7N4+3ZGO3kZgYbCKGEaWSFRUA3bhuzLUQ/VOVaWT+STwQHu69SC5/NSr/PJ+syR/DXHdnBXB5NajzU+d1iv0czTVmtwSZjgxO2iNC4cug0aG283vTtRBQ2kpmaI+zjIfDV/FgdcVMXjtShqkGDSY06cIwMgj3Y1umjmAgJJzt9NEZGHpYwv3VqCB/Ssw5mTEOiL2xhY43rCMuqVF4pBCEOIyVRBmHG2YB6oKZ4OlCT7SSIKReyaxgj2sujMeSTyUZLombNn48AKkgikLgdyRM/cEqP7vvw0/KD6AmJhmf38HpKljUSgbkzDHBb9xhJRQ98EjOd2e6OEw8Be0d3EBDw7oTDWM5cKhUPs8TQjGwpn9gYGFErwLotcMoQKLv7yX5HuCS2d+zWQee0tLqVQQDHPBn284p5ncROORZLqm6x+oiM+ZnhAH1wGa+H55xic/JAQpL6zPlbCiRpNji5OqcjtrK0NKNW1ThG+8mcDtjuLhv8KMV3GPnPrGQOh1vVGCBABRTOvfd4VrJYycndsONgSFgeFeHbN7nWGWTSCdLrtcR6XuTF4XCFvstQpOE08c73MWV9FwvWO0LrAs/1OAq7WaOzSit85axkK44cOAJ4iPkpbMXckQKRD3t+OdoDu0Sr8ELQLIa6IwomMxwRcJfgTC6mTl2dYDRqea+9BvsQDbDVZPIG2aU/R1+PZNJaWSUGNQD9/Uf852mLhkOPh0bw69hr3nnd3yuCIxPO9z+dWOCFNXP4TslIgZMd5+oka3Acl0MphewqZk+to2bSqw+iWmIRalcbYNCUlCSroYioUL0yoJmF++19bxjgOLry8WQwbpAkuQXCTCxCYB+oyUlv6gCb3GDI6Tg5M67cOF2ZYqIiExTJ6jdXimA550PoupMv5/sX82TDIwMZcdMqWz+YK3gBRWSRYO2JlmsQuenujp3ZzBc+bcOwxibcuDlOfaHKjhgzPGrs0pldHKZsqH4ju7oE4SbkeCMZ5hp0KwtzQAm2OyeHSCIw/b6RIpx12i07TF8baiycqatkrfspbxUMp7y6SqscS5uDybYN5mEICQm7Jwxw2tvlK5jnuR/l8kQ3gfPVTBOD+0kNy9ljNCRWh4OJhcbaPZ6c7c4OQMZmSpHZqfxtST46G5jtkbyGY1wZSBMUG0c3sB7hdkhzGkbKytuKHqMLhk9VVDssF6bn5GiFEUE3aE4Hh34Pw9rW+aHzNCVoXV4Tht9Y60F6EvZDU+NGPG3O8a7dqqP+MASj687aXIebXHBW95FENANLI0kvhKwZ/z4Z9+rZPOd9yTaFaLLKUT2Xuw/EWgsiIN8Ys94NBjjR3XwEo1vj03VjKp5XS1DuuoFwKW7R2yYLUe1fF4QAHBlSc1JiG8SWT2F7ONi3UXbkDyHsq8DbKiqteW3PjPBklMFginDqJ4k22jrOyF72OUAWYJP/tRxeVdJgWdRSc4SxIiuxCV5Pbn9BtJBgdzkIpSijbSyhdyhHxu0gYeJZtDOvSvEw2V4Jey5M2LAK5voatFA40/PimJ5epeOwNP5VU90ttMgDTjAM+xwf+OYw5uYhtq1EM3WCBDpxMpl4VRdA3ih42H1At2bFR7DCegGt5UaHlpgOCmwavv73xok9bCm0qEF0v2dW0xMMPhGcCQ3NeBVPw90BgzYLVzxICpDBrbyG5Xm0AyHNFJjQmahxrie+BSTNzieaG7HcguX7Rg/abpTgzUtqd8pYSWvLsd9qoHZBgbEZbhfOMkfhtFY+3BFPe3vAslBsV7lSfuRyG2bXDxfway8PdhNfZNA8qm2hTNA9QL/H21QzJxfGyvgiRYYFgvsmQcbQjI3JKV9hN7ghQUnvwRhdW0nCW4YVcZN2n07GbEI91GiYjgnrF20b+0i+Apw8GEmXWrjNoxDUj2ql6mWM2/MFKz3PDBxUw8fYXtDucnVMxiqXh5/Pu54cggJW2LiOVBrYpOOutoC2lcKoTQLccoKnh843t0bAKAysTYZ02HPruiJgkwj1TrtqEWGKgZq7vmPUt40RmPGtqx/i7w3rKt1Dpqz+cTQ7xobzvlG82HnrTmXdkx/0rgCm8+OtanZr6H/QV5CrgAXuiLlnNNfhaYslUnGDf+lSUu2kkGJlbrpdVE1bBRBSZ9tBqzVUjlTgKShOJ+Osm+5hzaEQhP8K4I44qURm+vH7nuEU/7KwNZjOT4mRlljL3MCWvtQkOgOTpoVnMomGHxbTRiWzC256Tbfg1UQSjvm8UiDFkbvll+w0ROxEH6ZshJgqGHDLQlo0Skp/nwPJiPLop37IJThk0iGBOarU1fyeO0DcaOOAfZf4Siyy8u/gfBOmOJYT+bH4BbNiYmzqMjpBDsWJupmfluLq+DtgTuokoyOdjeU6+oitChieGmJqOgzIcWYPo6RBiDNMkdZ1Lneg4UwIN+zeYeUci/73okMm5zYrSnwCuyUxYr8HA99RPGz8fW6Y97ITdNz8tmLIRFcaBEN/oLQWdhj6VaoJbxpd3Pr5zeEMpqFVjaJLpGq2l7YHyeeFZvCKcH0+mhZ2A+H7skNVGcyDY/D0/zF3etPPSju4oa4icD6QfepCtfH17Jz4bAG+ZZxiiEGabvwSeyHSyAe07tSddzneOW6RfnQRoKYFwR6GlE+YzT56EWQ4nDbIi052aLFNjZey3yocN/t09FzaPEzW+OxY33o24cccS8gIDhIhGuthYWaZwIY+wKAku2Qak6ObdsmLuPrICWomoTwKZBJ1aBo5KESRICFmv75MPjwxbTbAvMUIwEiS8naA6V3GYhw38/xOJ6FGx5l7KoDTLMdNy2DvlfsucGKVRw1ThvcIlmLzCe1EZRAE+rGlmLcPvxhDKmzwupoV9L2JHQJAs+JXUkKXimArQyxd2zvxRHRqhGm6Gt6GesooKz5t4FjGa7HROZ2n9Z1W7IJ89RE618smj2CW26dPQIav7rVVLnCQ8rF5gFFJFDVWbScjR7MC7UqpMhz6GWbXkXUL9zmLNpMM0oIL326x99Q8Lwe4xfDDxJ5nTD+fAkzwDXyLExrQpK0J72YExsZx8sY4u9MHtus7+R8FdtTwBmcVT882ofDBARujMpY1yziUIbVqKvukWoyJts/b7YtibcwcqBM0Zjn/gAGo96jJDEV/iGECslj39jAL6mJYJy9BRo5Ih0EghFDbh55YG8yFFAP80jNmEd1Qd7mZNHvJ9FKEmm3rA7fw8Fkg23Kom7GCRl9iey1tXA+jARszNZ7AGHgpwqDBGWxqaPQ8IPlradjqckly2Cdt0LCHxF6dBfBWxPxD/2AzR06tX14nOiBOP9jTesmaTHieBE0omsYHSKDLJo8xj/fMQZWVMnJ8mCE42VSc4Ob3doOiicUi92I03exR2LPMbqAaEGNj89Xzbk3X0txP+hYrCBdFD4LZc/snd3DZPFR60V8m3yKlQutk2W5PSYMT/7zc286ru0ikuK/Cf9KniY71+GMxPHCHblSYcfNugUvBXB0mhBrdedulkhiIxtWtZ2ZNU0+wF+709ZzMce+LBHko/GiiAvlA9zhSu2gplIgiSmW+1F5PvoCZvNsaThNWiMnkTiBcoq3DaViq6WDcFH9u3/ZgARIShwipOyU31lWyQsxaIh1Z2AzCFaSZDmNCNyLhzfJn9TObO1w/SFxc1PcqlGjBOFowhdpiEDAp/ioiQ83fkxPoD7GnZzzoqvzuUQh6Lhlxdr4amOB6pxSDsOK2wcNIvhsaiGYkhEb1rfuJ6IMZN00Fs2z67u95XzIDK8ps/S93MwLf8tl5lAfPg1PZZnRmc8U4HdwkD6JJ98iugGlTHrzTOWIiwUE/lotu8gDmA4oagmIRjZqbmZ6n92h/5Yv4CkajzDpww+Sh0g2lueMcmkjUOG96UcSJTWLT/RhWRNrKGcMsApKu5zwjxuhJ5bb7Ax3Rzdr9pHFPX6QZXG2EtcWsOOnGhUE7H5guOFTZCLRCSNJZzfA02XEDDEW2wxsJiw1uqGXlFpctUvWW9VjDw0ZIQYJhT2IPK0wL926GgPp/b59vuy42rgEIo0pLzVL79dyCYnUrpZb6QWFctdhwksLSuQYMYxn62l6WoUQBMv7EwHoOD4CAk6wo4Aj9HmkavAqPPOS1l07RDUiwd0Y4uR5YJ9s/XlgkqMpFEGYGNqJJwNPprszITg5aOT0HFcvQPeaM3QDVLRvpFnDDh08SXzCEC5k7RsRPVrokwKSjdv8y6jADEpGCfzrGjGMxElK0rlU78km6i10ErOCoFOeae3JE3a/CsW4F27+yf7iZcwfLq+PhXPjEkNuW4lbJfVVyU08hd8nwWYZZnM5OD6aFy+/VuE4YI7NOwsImi1cFY8jBNrVha4hskbnBpeaKINJPgV43m3RCIz2bvrLM/240N47XNe9XTFkaYjemDgUC+CPl7KaRixGVnNDJPkYq2TJ02Rw7NLaPcDzYK4wu2ACRkwzKoWLdkB3DbZlG6iKk9nFDDrXnwrPSspeh7Wsrlhy6Yn29uKc9kFt+gLUpjI/qDDHcaB02bM8NOdxI3vVwNKiWlTbANwYqOklMYwzjlC5JWENAo4j7HM3k0gaO82aFsJpw70Jzma628cBdR6uncGW2CtuoaILlAbcFNpjtYGOMwXzCosOibGC4h/qWAUrCbpv0ytdpYzdbzQuYC0xs+SAsD3sBdB0TbesGG+bXlQtyj6Czw7ofzKlrRXn05WQIwhXAPhGuHDzZoFtHFnCFxtw3US7hraUZ2OpBCzkbsc80u6sC+BD+t/UAjAeNE3HM24YAoQVzKAyRRS7+eawxtJmRTcQ5Vckttwb3D04bDoMqnjYvCo7W0rcG7snyacsW4d1PIbJFAkpwZnUaaNiwN6NaORAtaXpaFTKxZc9lOKgWjWMLBkOnQrrp5sbQFNFIW7AR1DxCP3NsxcZ+xNG3jKw26D2pn2M3Gz2NBUiLAOCwfwzVD6bRwwyuNPyvGYrkWZcySQT5k3vulUtRDIAGI/D04ObJq7oA7vA67Lu5kT1OIql5Ak7gUvgWKa4gM8ISrEZ3FWCzLeDTnTcwzImAOFnKnHEupSH8lOCCbP2LgQUE024iB7gRnap5up2+axzGoM6iOpEiOCEmu3es/Wd0fKltgGxG9Ok8PLYUDkFsmm+23DMd6RCbCMMtMwndgXjvEouJKQjq8CvkUGoxMYFo4X4u15pWDmE70dtNqpqcY3xK8W0dmDDcibuhOLKatMbGqSCUbI2Q7GsC8wtdF4P3Mwpfqh5JgHDfzfW7v2pK69u0fAPWQBuJidUJsTdbhiRxAsBGxszHkiz0kHWjcwTiYlwJDhb8NYZrwyDHoGsMF4K5ECOU+wvnkc0XcHOJdXkf1aMDfMLpaWm3P97GYdVQeFXThgmMhe/DCjt7klWU0B0GUCtdpvoHZkzLSWCFE0vGGbCCLamhcBRnCZB9fmgdVa7ddZMRTBY1jUwjSvKYy2U68cCyQlybNDo62QvScUC5uSjP5fJdYDO1GBjFV3EBnDq+WwSIzxQP44JTPqU+fWPWiFpcrYeAxhwurAk1YFohcp+a1HYSC/hO9JHPxp4Bv8vZ+sD7GCaXbchXhxZxCabhTz0lD/6n3eOwFq7VrcxjV3RUe8wMWHYzh6j7wC+nPI7ufl1jaDdi7fGUfWB+XZld1mM2rz+PaHKe8daNNLkm4k48mCGjhjvj5EgI0whOsQaonUHX7ALRx3gK9jeeWkSXk2EYX9MnsUIuzzLBKL5MjvT/exZ46tw2SxxezB5Z5odukoesYzpJQRertodfluZ1my3Aek7sKCul0/P6NjsmuTz7xO0EuwTVMMqUzRfUfM0B3O6UMS24sXcYgmEirO2LqptbIz+tfqjRZ6XkcenA4KM7EDPmVkOX+FEUt3A0sbF7yJahllLFAPOeieK+M+bSLh0GOA6RCOaeIzWmhpw5LJLCbMMQ0wg2uCy5KXxrapmfO5RHug7SKIVCbB0rOvgKKdO5bncpRsDU/YzAfGXFa/PsPYwZTq3ARENoRNhZ/9DxFzF9g3WLeeOxmoEjc3ymFAaMMXKoMKfk4LgMzbvNURKzTCHyiC2TRE/0DPSZrE/k9OLIVuozB9E3hjUUm33EGldx+7NcxUakLExUneI9oocOpWhz61M6WOYICwwLqk/f1cWIYWyuoHif+hxK2Lo7L2PMSUZzolAnatrxmxpNM9mokEsNbufWjG39TNEOsXMn3ILvPOgetlvI2FuuDQJ5byQIvozXjgdygm+ZodKbpzPLFX7jqikjdowzO5OXX5darhnnZKaoVveR6VKdFkBO8fTjrEppDavt3WoYTYnsIdj1HI05sYJDf5MbOcDqYtT3iy3v0smAFlg1ED3tofEjg8otoResFLyvJDk0uvK8auiPYzrMnDInNdXRwgx/rNe0AinSDAejzria67bCF8xbkS1ZDpubFbaBdGgn82CHYYCKB2t2dMvexrP7zMd61xVhBANgvTrvrgA+tM87dnMbF+hbRaY6dtRXK3zksQ20SdQaTh75fGrw92mgYIr6zawvIy51PMyshNC1Ab5/iQSlppyMjF1DxcjuY+yt+AVoYa75s+yZujFmWeHjfiDAMEJpgUBD0qsviET6HsSJGSJ1OUgTxHYYBu5C5RpA09PhFLqv05hMMk3GRsBMavu9uPZk7b1xAFIIix7gprvN9bZBBXrmm3DAUdfnp2arlIBzMQdmNffvXH5Gz5a4SqHYMOL7NETljUq2E0U/BNK2zrKvxLdR7yRCJIc+MLksIBgQ4zV19RUibOQ83DFZjA6UPU1tKf0pMYhqP+Umqgr2Y+yoZRRB4JWMABssURwxjaXdlsnwOnRkiCb/0S6c1qm5QL23/Wc3gSQF2EUDUOdnOFsNfe9szT5ixWyZSHQDXBbHHGrtZ3y6ztD1zWmiRJq9Pma/twpYM4kNGKjy/MjDhH5Nh7hLhGaIk3O5ZNKw+RP2g/GQYS3Z0+TXfS8s8E3wbzPL8gG8cCJVtG2Gpt0P6nlycBEpcxBu48WB76CHOk9tee72tgDowxka4ayLvlSSUUuW0ZBExuSqooytSjrsfl8d1lk07QZGoR0weZiHOyox9NRyU6uLtPr4gVuKnZE0AenWVrgUhizkPq6HXNg3TmyjBA2aYPB53KFjdGhhDMgaOjIY5lvd+l/ZVsSyjKIYp9MzPNKSsMCl5hIzrZPkOAy05xCkrWrqxxmJyAt/nohSuEsTt+Piq50F3uGA2FS0L5fzDsXCuuGmS4dwQ3GW7FhvYqqjUUGepEUSN8AxVgdC1QJxYMQjvgfLSSWTPKCCW65usDehEk0jRgBaVjGcJ5DRkyr3xxTLoVjYfMitFWCeC//QLlN7pEGUnJjpztXJTRsSrqdzJtqFMwq0Yk6R4qZJdCwdS491PqwioZhd07/EoVWCh5rBBSVBMA49ukcCbEyVyUDT3uBSJntmZO0JuhqV8GgaFyL8Js9njZFH0m3+V0e5doO1AbDXcVcY4M4BhJsC1jIBtk/ASI6cIufKAPGg7kyxr1IRdHEnHTO0tR2sUWHVScXVxN6JsZPKjg70UB5PGUvdmr0/GTm9e0TLIR4aRC+GLSEc5tipGb1SG3znNBH5XHWKdshwK2KDiLDz0bdY45qJnBHWgJijgEuWIS0zlcdh8M3Q7MDQkpLVnL9bV4kJjdNw32H4oWGeypJTu1A/Nip1Uy3iUycOsFoay6mlVOzUY2b1angzQltTBBzztqWPAyPFjNK/ukfgZgO0K2iYsb2HJmjc5kpilX/IFnLQWBFwjuN1d+rlgKNwOOEP12WyuoHC8JBmDCQFD+QwPgFjQFqs8UEW5pkJlecmwNpU8Q+EsaR+Xrc6ktO0DqHVPwRSdRRPDUpPvQu3WU0AAATVSURBVNccuib22s1eJEa1vpW1UFJIF0O38Ecu80cR91VDNN7uvBfYQZGMKlA8r7vvyDrlzeGIvfBQ8d7Eud0JhO0A0YwZdBgG7vBCPZCTmzogJ6gWAjODrx22dt28x1CkGoiL8aFn1RD4dbMLbPmWaGdZLv8nlc5t4cwHEIFJo6n8l7mFZ5o2F74EaXboEViWyCBuGWAfLpNk0V3lCiBcjR+iQdn0WzK5VBIuCCJJfZXmwnD0R9RtB9ijUbwBAnPyeBLGEWBz8fbJWvSW0nXg3M9W41w0P0APzpKCyNhuYJBIGStp+BqdXImjQ30S26qgiPPBQWo632Dnf08ogr5ix/w8KdOPWIo1Dm8TcOUHKMN+a1KXUu735Rp+fxhgDe/tVt7vEGi+fx5y7YFzq1axDIToDwf6FmDLObCVSqaVtxtlnroyWzJHZXfcV9jiRhaSII0ZmNcCbS22anjwaMXDT+71GpDBZA7ul4M0sMxhNjxSnxJ1dIaYcppG7HhQAHNNsRAdLkzxjHZkjp4ZgoTNpmas/6n8hEk21akRTOuqXa/S9OkiAj4t+qLInrvi6rDSsYtqqVqT2g8Y3vOyoQeq5eL0kQNxKNArPgJMT0dxPSjA1mUqKWbdafvk7qkDnJTGfITY+bG7w5HbYEVqKKIJC5rsQ4UDwnxqQE7Li2W3nE+0+ByxM+tBHGWAobNhh0fVU6kz1eUnJTV83Q0UkFXUApVCPH7KIPybcWB3lJQ9ZYGMXVlyCN/koGxtRYq3dJHqq2g2VdSHh+NJi8hcGSFosgUPUkPl88Qh5w1P+H0OgYGVtm2fd4ada74yY1sm4EFfd/PYV329EFswCzliHIoV65aV42oEzofRL8+/zz/Xfl6rJNzxXFRGeN6bEHpielEbFuQGtscHmF9Wt9GFWvDQk9N0vGF0SqdxAIOdVICXpiNMRgGcWH1YHOa4laJGqOF6cjBnmU2nVlrNhy1WmNrpSd9YsLxzFbFm6FTDGJXh1vyzvjrIPBjSMwEZecrNCDEwzYTt7Y5aJ7AH2puWCGdHX2LGAHaWvrD2GiCYpmf70jdomoFebF601pQBqVQNsUGlimIkzR2RlZ4PLP8fSbTAIR+OLWz7bPoKnspeVleN6mNxbkzldQd5RwXwlusz8LhRuB7AMYaFAH22TdMn3QdjafwYwxRJ3CJ01GjMUHKHKw2bGaTGXWLfmub2AGmbByvVjo2YGNEBdp/ObhYSfnCoOV4cXg7PG/eU7BxicwR+2EewgzVHYHQQo4Ddckq3z1SzAZf7ECJrGahY22HduJZMG0tQsT03LJy6gAepv+6H2MEmZjURHdJxRo7GUDeRYEw8GPV3OC3iEBgglY1mADjorjR2Tp8H4PEZCFZODge2/v3YQFX3MgKPhzZnVhebA39qEcENSaJmo1wHG/bUB88UMIQ95wLGVDiqNyOwaV2xE80t5wv9gxHBYe9WMe2OCjEB7In2alsN5TIGTuB7PHzhOH3EBaQbNzQgfPqouLKXuXlY9fWZ1ZM2npiIjn7OehCB41gZnVrdvLvxeaUbRTKB9MJU7p2IsDCjaCL1TwxPy8+uxo6LRrxUZ8grYQy0bRtz3uYizsDcAtd0LzpBZMYGjN1lyvqosv9lXaJh98AsIXqV/vN/AwfGJ1FgGOsuAAAAAElFTkSuQmCC';
diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-0.html b/dom/html/reftests/toblob-todataurl/toblob-quality-0.html
new file mode 100644
index 0000000000..7e7298cb6b
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/toblob-quality-0.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0;
+ </script>
+ <script src='sample.js'></script>
+ <script src='blob.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-100.html b/dom/html/reftests/toblob-todataurl/toblob-quality-100.html
new file mode 100644
index 0000000000..34f318e11f
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/toblob-quality-100.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 1;
+ </script>
+ <script src='sample.js'></script>
+ <script src='blob.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-25.html b/dom/html/reftests/toblob-todataurl/toblob-quality-25.html
new file mode 100644
index 0000000000..ed4350e6eb
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/toblob-quality-25.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0.25;
+ </script>
+ <script src='sample.js'></script>
+ <script src='blob.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-50.html b/dom/html/reftests/toblob-todataurl/toblob-quality-50.html
new file mode 100644
index 0000000000..47e3b684fa
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/toblob-quality-50.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0.50;
+ </script>
+ <script src='sample.js'></script>
+ <script src='blob.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-75.html b/dom/html/reftests/toblob-todataurl/toblob-quality-75.html
new file mode 100644
index 0000000000..45ccfe1fe0
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/toblob-quality-75.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0.75;
+ </script>
+ <script src='sample.js'></script>
+ <script src='blob.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-92.html b/dom/html/reftests/toblob-todataurl/toblob-quality-92.html
new file mode 100644
index 0000000000..6a7f8788fb
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/toblob-quality-92.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0.92;
+ </script>
+ <script src='sample.js'></script>
+ <script src='blob.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-default.html b/dom/html/reftests/toblob-todataurl/toblob-quality-default.html
new file mode 100644
index 0000000000..c5b404744c
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/toblob-quality-default.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script src='sample.js'></script>
+ <script src='blob.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-undefined.html b/dom/html/reftests/toblob-todataurl/toblob-quality-undefined.html
new file mode 100644
index 0000000000..3252900200
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/toblob-quality-undefined.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = undefined;
+ </script>
+ <script src='sample.js'></script>
+ <script src='blob.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-0.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-0.html
new file mode 100644
index 0000000000..1d4eb9b7f1
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-0.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0;
+ </script>
+ <script src='sample.js'></script>
+ <script src='dataurl.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-100.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-100.html
new file mode 100644
index 0000000000..66b627c13e
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-100.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 1;
+ </script>
+ <script src='sample.js'></script>
+ <script src='dataurl.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-25.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-25.html
new file mode 100644
index 0000000000..15237cea8b
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-25.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0.25;
+ </script>
+ <script src='sample.js'></script>
+ <script src='dataurl.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-50.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-50.html
new file mode 100644
index 0000000000..93e820e68b
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-50.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0.50;
+ </script>
+ <script src='sample.js'></script>
+ <script src='dataurl.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-75.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-75.html
new file mode 100644
index 0000000000..acdc7416f8
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-75.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0.75;
+ </script>
+ <script src='sample.js'></script>
+ <script src='dataurl.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-92.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-92.html
new file mode 100644
index 0000000000..ca3de4ee0b
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-92.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = 0.92;
+ </script>
+ <script src='sample.js'></script>
+ <script src='dataurl.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-default.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-default.html
new file mode 100644
index 0000000000..cc7771dbf0
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-default.html
@@ -0,0 +1,7 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script src='sample.js'></script>
+ <script src='dataurl.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-undefined.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-undefined.html
new file mode 100644
index 0000000000..16801e4829
--- /dev/null
+++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-undefined.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html class='reftest-wait'>
+ <body>
+ <script>
+ var quality = undefined;
+ </script>
+ <script src='sample.js'></script>
+ <script src='dataurl.js'></script>
+ </body>
+</html>
diff --git a/dom/html/reftests/video_rotated.mp4 b/dom/html/reftests/video_rotated.mp4
new file mode 100644
index 0000000000..38a1b77f93
--- /dev/null
+++ b/dom/html/reftests/video_rotated.mp4
Binary files differ
diff --git a/dom/html/reftests/video_rotation_90.mp4 b/dom/html/reftests/video_rotation_90.mp4
new file mode 100644
index 0000000000..85aa055fb9
--- /dev/null
+++ b/dom/html/reftests/video_rotation_90.mp4
Binary files differ
diff --git a/dom/html/test/347174transform.xsl b/dom/html/test/347174transform.xsl
new file mode 100644
index 0000000000..1b201de3f3
--- /dev/null
+++ b/dom/html/test/347174transform.xsl
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+<xsl:template match="/">
+<html>
+<head>
+<script>
+window.parent.frameScriptTag(document.readyState);
+
+function attachCustomEventListener(element, eventName, command) {
+ if (window.addEventListener &amp;&amp; !window.opera)
+ element.addEventListener(eventName, command, true);
+ else if (window.attachEvent)
+ element.attachEvent("on" + eventName, command);
+}
+
+function load() {
+ window.parent.frameLoad(document.readyState);
+}
+
+function readyStateChange() {
+ window.parent.frameReadyStateChange(document.readyState);
+}
+
+function DOMContentLoaded() {
+ window.parent.frameDOMContentLoaded(document.readyState);
+}
+
+window.onload=load;
+
+attachCustomEventListener(document, "readystatechange", readyStateChange);
+attachCustomEventListener(document, "DOMContentLoaded", DOMContentLoaded);
+
+</script>
+</head>
+<body>
+</body>
+</html>
+</xsl:template>
+
+</xsl:stylesheet> \ No newline at end of file
diff --git a/dom/html/test/347174transformable.xml b/dom/html/test/347174transformable.xml
new file mode 100644
index 0000000000..68f7bc6dca
--- /dev/null
+++ b/dom/html/test/347174transformable.xml
@@ -0,0 +1,3 @@
+<?xml version='1.0'?>
+<?xml-stylesheet type="text/xsl" href="347174transform.xsl"?>
+<doc>This is a sample document.</doc>
diff --git a/dom/html/test/allowMedia.sjs b/dom/html/test/allowMedia.sjs
new file mode 100644
index 0000000000..f29619cd89
--- /dev/null
+++ b/dom/html/test/allowMedia.sjs
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+ resp.setHeader("Cache-Control", "no-cache", false);
+ resp.setHeader("Content-Type", "text/plain", false);
+
+ let stateKey = "allowMediaState";
+ let state = getState(stateKey);
+ setState(stateKey, req.queryString ? "FAIL" : "");
+ resp.write(state || "PASS");
+}
diff --git a/dom/html/test/browser.toml b/dom/html/test/browser.toml
new file mode 100644
index 0000000000..13775f607b
--- /dev/null
+++ b/dom/html/test/browser.toml
@@ -0,0 +1,46 @@
+[DEFAULT]
+support-files = [
+ "bug592641_img.jpg",
+ "dummy_page.html",
+ "image.png",
+ "submission_flush.html",
+ "post_action_page.html",
+ "form_data_file.bin",
+ "form_data_file.txt",
+ "form_submit_server.sjs",
+ "head.js",
+]
+
+["browser_DOMDocElementInserted.js"]
+skip-if = ["bits == 64 && (os == 'mac' || os == 'linux')"] #Bug 1646862
+
+["browser_ImageDocument_svg_zoom.js"]
+
+["browser_bug436200.js"]
+support-files = ["bug436200.html"]
+
+["browser_bug592641.js"]
+
+["browser_bug1081537.js"]
+
+["browser_bug1108547.js"]
+support-files = [
+ "file_bug1108547-1.html",
+ "file_bug1108547-2.html",
+ "file_bug1108547-3.html",
+]
+
+["browser_containerLoadingContent.js"]
+
+["browser_form_post_from_file_to_http.js"]
+
+["browser_refresh_after_document_write.js"]
+support-files = ["file_refresh_after_document_write.html"]
+
+["browser_submission_flush.js"]
+
+["browser_targetBlankNoOpener.js"]
+support-files = [
+ "empty.html",
+ "image_yellow.png",
+]
diff --git a/dom/html/test/browser_DOMDocElementInserted.js b/dom/html/test/browser_DOMDocElementInserted.js
new file mode 100644
index 0000000000..fb2b2ae63b
--- /dev/null
+++ b/dom/html/test/browser_DOMDocElementInserted.js
@@ -0,0 +1,23 @@
+// Tests that the DOMDocElementInserted event is visible on the frame
+add_task(async function () {
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ let uri = "data:text/html;charset=utf-8,<html/>";
+
+ let eventPromise = ContentTask.spawn(tab.linkedBrowser, null, function () {
+ return new Promise(resolve => {
+ addEventListener(
+ "DOMDocElementInserted",
+ event => resolve(event.target.documentURIObject.spec),
+ {
+ once: true,
+ }
+ );
+ });
+ });
+
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, uri);
+ let loadedURI = await eventPromise;
+ is(loadedURI, uri, "Should have seen the event for the right URI");
+
+ gBrowser.removeTab(tab);
+});
diff --git a/dom/html/test/browser_ImageDocument_svg_zoom.js b/dom/html/test/browser_ImageDocument_svg_zoom.js
new file mode 100644
index 0000000000..f0df2282a3
--- /dev/null
+++ b/dom/html/test/browser_ImageDocument_svg_zoom.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = `data:image/svg+xml,<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="green"/></svg>`;
+
+function test_once() {
+ return BrowserTestUtils.withNewTab(URL, async browser => {
+ return await SpecialPowers.spawn(browser, [], async function () {
+ const rect = content.document.documentElement.getBoundingClientRect();
+ info(
+ `${rect.width}x${rect.height}, ${content.innerWidth}x${content.innerHeight}`
+ );
+ is(
+ Math.round(rect.height),
+ content.innerHeight,
+ "Should fill the viewport and not overflow"
+ );
+ });
+ });
+}
+
+add_task(async function test_with_no_text_zoom() {
+ await test_once();
+});
+
+add_task(async function test_with_text_zoom() {
+ let dpi = window.devicePixelRatio;
+
+ await SpecialPowers.pushPrefEnv({ set: [["ui.textScaleFactor", 200]] });
+ Assert.greater(
+ window.devicePixelRatio,
+ dpi,
+ "DPI should change as a result of the pref flip"
+ );
+
+ return test_once();
+});
diff --git a/dom/html/test/browser_bug1081537.js b/dom/html/test/browser_bug1081537.js
new file mode 100644
index 0000000000..2a079be2f7
--- /dev/null
+++ b/dom/html/test/browser_bug1081537.js
@@ -0,0 +1,11 @@
+// This test is useful because mochitest-browser runs as an addon, so we test
+// addon-scope paths here.
+var ifr;
+function test() {
+ ifr = document.createXULElement("iframe");
+ document.getElementById("main-window").appendChild(ifr);
+ is(ifr.contentDocument.nodePrincipal.origin, "[System Principal]");
+ ifr.contentDocument.open();
+ ok(true, "Didn't throw");
+}
+registerCleanupFunction(() => ifr.remove());
diff --git a/dom/html/test/browser_bug1108547.js b/dom/html/test/browser_bug1108547.js
new file mode 100644
index 0000000000..4949827086
--- /dev/null
+++ b/dom/html/test/browser_bug1108547.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+requestLongerTimeout(2);
+
+function test() {
+ waitForExplicitFinish();
+
+ runPass("file_bug1108547-2.html", function () {
+ runPass("file_bug1108547-3.html", function () {
+ finish();
+ });
+ });
+}
+
+function runPass(getterFile, finishedCallback) {
+ var rootDir = "http://mochi.test:8888/browser/dom/html/test/";
+ var testBrowser;
+ var privateWin;
+
+ function whenDelayedStartupFinished(win, callback) {
+ let topic = "browser-delayed-startup-finished";
+ Services.obs.addObserver(function onStartup(aSubject) {
+ if (win != aSubject) {
+ return;
+ }
+
+ Services.obs.removeObserver(onStartup, topic);
+ executeSoon(callback);
+ }, topic);
+ }
+
+ // First, set the cookie in a normal window.
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ rootDir + "file_bug1108547-1.html"
+ );
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(
+ afterOpenCookieSetter
+ );
+
+ function afterOpenCookieSetter() {
+ gBrowser.removeCurrentTab();
+
+ // Now, open a private window.
+ privateWin = OpenBrowserWindow({ private: true });
+ whenDelayedStartupFinished(privateWin, afterPrivateWindowOpened);
+ }
+
+ function afterPrivateWindowOpened() {
+ // In the private window, open the getter file, and wait for a new tab to be opened.
+ privateWin.gBrowser.selectedTab = BrowserTestUtils.addTab(
+ privateWin.gBrowser,
+ rootDir + getterFile
+ );
+ testBrowser = privateWin.gBrowser.selectedBrowser;
+ privateWin.gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ onNewTabOpened,
+ true
+ );
+ }
+
+ function fetchResult() {
+ return SpecialPowers.spawn(testBrowser, [], function () {
+ return content.document.getElementById("result").textContent;
+ });
+ }
+
+ function onNewTabOpened() {
+ // When the new tab is opened, wait for it to load.
+ privateWin.gBrowser.tabContainer.removeEventListener(
+ "TabOpen",
+ onNewTabOpened,
+ true
+ );
+ BrowserTestUtils.browserLoaded(
+ privateWin.gBrowser.tabs[privateWin.gBrowser.tabs.length - 1]
+ .linkedBrowser
+ )
+ .then(fetchResult)
+ .then(onNewTabLoaded);
+ }
+
+ function onNewTabLoaded(result) {
+ // Now, ensure that the private tab doesn't have access to the cookie set in normal mode.
+ is(result, "", "Shouldn't have access to the cookies");
+
+ // We're done with the private window, close it.
+ privateWin.close();
+
+ // Clear all cookies.
+ Cc["@mozilla.org/cookiemanager;1"]
+ .getService(Ci.nsICookieManager)
+ .removeAll();
+
+ // Open a new private window, this time to set a cookie inside it.
+ privateWin = OpenBrowserWindow({ private: true });
+ whenDelayedStartupFinished(privateWin, afterPrivateWindowOpened2);
+ }
+
+ function afterPrivateWindowOpened2() {
+ // In the private window, open the setter file, and wait for it to load.
+ privateWin.gBrowser.selectedTab = BrowserTestUtils.addTab(
+ privateWin.gBrowser,
+ rootDir + "file_bug1108547-1.html"
+ );
+ BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser).then(
+ afterOpenCookieSetter2
+ );
+ }
+
+ function afterOpenCookieSetter2() {
+ // We're done with the private window now, close it.
+ privateWin.close();
+
+ // Now try to read the cookie in a normal window, and wait for a new tab to be opened.
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ rootDir + getterFile
+ );
+ testBrowser = gBrowser.selectedBrowser;
+ gBrowser.tabContainer.addEventListener("TabOpen", onNewTabOpened2, true);
+ }
+
+ function onNewTabOpened2() {
+ // When the new tab is opened, wait for it to load.
+ gBrowser.tabContainer.removeEventListener("TabOpen", onNewTabOpened2, true);
+ BrowserTestUtils.browserLoaded(
+ gBrowser.tabs[gBrowser.tabs.length - 1].linkedBrowser
+ )
+ .then(fetchResult)
+ .then(onNewTabLoaded2);
+ }
+
+ function onNewTabLoaded2(result) {
+ // Now, ensure that the normal tab doesn't have access to the cookie set in private mode.
+ is(result, "", "Shouldn't have access to the cookies");
+
+ // Remove both of the tabs opened here.
+ gBrowser.removeCurrentTab();
+ gBrowser.removeCurrentTab();
+
+ privateWin = null;
+ testBrowser = null;
+
+ finishedCallback();
+ }
+}
diff --git a/dom/html/test/browser_bug436200.js b/dom/html/test/browser_bug436200.js
new file mode 100644
index 0000000000..7e739c02ad
--- /dev/null
+++ b/dom/html/test/browser_bug436200.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kTestPage = "https://example.org/browser/dom/html/test/bug436200.html";
+
+async function run_test(shouldShowPrompt, msg) {
+ let promptShown = false;
+
+ function tabModalObserver(subject) {
+ promptShown = true;
+ subject.querySelector(".tabmodalprompt-button0").click();
+ }
+ Services.obs.addObserver(tabModalObserver, "tabmodal-dialog-loaded");
+
+ function commonDialogObserver(subject) {
+ let dialog = subject.Dialog;
+ promptShown = true;
+ dialog.ui.button0.click();
+ }
+ Services.obs.addObserver(commonDialogObserver, "common-dialog-loaded");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kTestPage);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let form = content.document.getElementById("test_form");
+ form.submit();
+ });
+ Services.obs.removeObserver(tabModalObserver, "tabmodal-dialog-loaded");
+ Services.obs.removeObserver(commonDialogObserver, "common-dialog-loaded");
+
+ is(promptShown, shouldShowPrompt, msg);
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_prompt() {
+ await run_test(true, "Should show prompt");
+});
+
+add_task(async function test_noprompt() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.warn_submit_secure_to_insecure", false]],
+ });
+ await run_test(false, "Should not show prompt");
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_prompt_modal() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "prompts.modalType.insecureFormSubmit",
+ Services.prompt.MODAL_TYPE_WINDOW,
+ ],
+ ],
+ });
+ await run_test(true, "Should show prompt");
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/dom/html/test/browser_bug592641.js b/dom/html/test/browser_bug592641.js
new file mode 100644
index 0000000000..761af6a568
--- /dev/null
+++ b/dom/html/test/browser_bug592641.js
@@ -0,0 +1,61 @@
+// Test for bug 592641 - Image document doesn't show dimensions of cached images
+
+// Globals
+var testPath = "http://mochi.test:8888/browser/dom/html/test/";
+var ctx = { loadsDone: 0 };
+
+// Entry point from Mochikit
+function test() {
+ waitForExplicitFinish();
+
+ ctx.tab1 = BrowserTestUtils.addTab(gBrowser, testPath + "bug592641_img.jpg");
+ ctx.tab1Browser = gBrowser.getBrowserForTab(ctx.tab1);
+ BrowserTestUtils.browserLoaded(ctx.tab1Browser).then(load1Soon);
+}
+
+function checkTitle(title) {
+ ctx.loadsDone++;
+ ok(
+ /^bug592641_img\.jpg \(JPEG Image, 1500\u00A0\u00D7\u00A01500 pixels\)/.test(
+ title
+ ),
+ "Title should be correct on load #" + ctx.loadsDone + ", was: " + title
+ );
+}
+
+function load1Soon() {
+ // onload is fired in OnStopDecode, so let's use executeSoon() to make sure
+ // that any other OnStopDecode event handlers get the chance to fire first.
+ executeSoon(load1Done);
+}
+
+function load1Done() {
+ // Check the title
+ var title = ctx.tab1Browser.contentTitle;
+ checkTitle(title);
+
+ // Try loading the same image in a new tab to make sure things work in
+ // the cached case.
+ ctx.tab2 = BrowserTestUtils.addTab(gBrowser, testPath + "bug592641_img.jpg");
+ ctx.tab2Browser = gBrowser.getBrowserForTab(ctx.tab2);
+ BrowserTestUtils.browserLoaded(ctx.tab2Browser).then(load2Soon);
+}
+
+function load2Soon() {
+ // onload is fired in OnStopDecode, so let's use executeSoon() to make sure
+ // that any other OnStopDecode event handlers get the chance to fire first.
+ executeSoon(load2Done);
+}
+
+function load2Done() {
+ // Check the title
+ var title = ctx.tab2Browser.contentTitle;
+ checkTitle(title);
+
+ // Clean up
+ gBrowser.removeTab(ctx.tab1);
+ gBrowser.removeTab(ctx.tab2);
+
+ // Test done
+ finish();
+}
diff --git a/dom/html/test/browser_containerLoadingContent.js b/dom/html/test/browser_containerLoadingContent.js
new file mode 100644
index 0000000000..4fb10db614
--- /dev/null
+++ b/dom/html/test/browser_containerLoadingContent.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const DIRPATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ ""
+);
+
+const ORIGIN = "https://example.com";
+const CROSSORIGIN = "https://example.org";
+
+const TABURL = `${ORIGIN}/${DIRPATH}dummy_page.html`;
+
+const IMAGEURL = `${ORIGIN}/${DIRPATH}image.png`;
+const CROSSIMAGEURL = `${CROSSORIGIN}/${DIRPATH}image.png`;
+
+const DOCUMENTURL = `${ORIGIN}/${DIRPATH}dummy_page.html`;
+const CROSSDOCUMENTURL = `${CROSSORIGIN}/${DIRPATH}dummy_page.html`;
+
+function getPids(browser) {
+ return browser.browsingContext.children.map(
+ child => child.currentWindowContext.osPid
+ );
+}
+
+async function runTest(spec, tabUrl, imageurl, crossimageurl, check) {
+ await BrowserTestUtils.withNewTab(tabUrl, async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [spec, imageurl, crossimageurl],
+ async ({ element, attribute }, url1, url2) => {
+ for (let url of [url1, url2]) {
+ const object = content.document.createElement(element);
+ object[attribute] = url;
+ const onloadPromise = new Promise(res => {
+ object.onload = res;
+ });
+ content.document.body.appendChild(object);
+ await onloadPromise;
+ }
+ }
+ );
+
+ await check(browser);
+ });
+}
+
+let iframe = { element: "iframe", attribute: "src" };
+let embed = { element: "embed", attribute: "src" };
+let object = { element: "object", attribute: "data" };
+
+async function checkImage(browser) {
+ let pids = getPids(browser);
+ is(pids.length, 2, "There should be two browsing contexts");
+ ok(pids[0], "The first pid should have a sane value");
+ ok(pids[1], "The second pid should have a sane value");
+ isnot(pids[0], pids[1], "The two pids should be different");
+
+ let images = [];
+ for (let context of browser.browsingContext.children) {
+ images.push(
+ await SpecialPowers.spawn(context, [], async () => {
+ let img = new URL(content.document.querySelector("img").src);
+ is(
+ `${img.protocol}//${img.host}`,
+ `${content.location.protocol}//${content.location.host}`,
+ "Images should be loaded in the same domain as the document"
+ );
+ return img.href;
+ })
+ );
+ }
+ isnot(images[0], images[1], "The images should have different sources");
+}
+
+function checkDocument(browser) {
+ let pids = getPids(browser);
+ is(pids.length, 2, "There should be two browsing contexts");
+ ok(pids[0], "The first pid should have a sane value");
+ ok(pids[1], "The second pid should have a sane value");
+ isnot(pids[0], pids[1], "The two pids should be different");
+}
+
+add_task(async function test_iframeImageDocument() {
+ await runTest(iframe, TABURL, IMAGEURL, CROSSIMAGEURL, checkImage);
+});
+
+add_task(async function test_embedImageDocument() {
+ await runTest(embed, TABURL, IMAGEURL, CROSSIMAGEURL, checkImage);
+});
+
+add_task(async function test_objectImageDocument() {
+ await runTest(object, TABURL, IMAGEURL, CROSSIMAGEURL, checkImage);
+});
+
+add_task(async function test_iframeDocument() {
+ await runTest(iframe, TABURL, DOCUMENTURL, CROSSDOCUMENTURL, checkDocument);
+});
+
+add_task(async function test_embedDocument() {
+ await runTest(embed, TABURL, DOCUMENTURL, CROSSDOCUMENTURL, checkDocument);
+});
+
+add_task(async function test_objectDocument() {
+ await runTest(object, TABURL, DOCUMENTURL, CROSSDOCUMENTURL, checkDocument);
+});
diff --git a/dom/html/test/browser_form_post_from_file_to_http.js b/dom/html/test/browser_form_post_from_file_to_http.js
new file mode 100644
index 0000000000..e62912bdcd
--- /dev/null
+++ b/dom/html/test/browser_form_post_from_file_to_http.js
@@ -0,0 +1,181 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+
+const TEST_HTTP_POST =
+ "http://example.org/browser/dom/html/test/form_submit_server.sjs";
+
+// Test for bug 1351358.
+async function runTest(doNewTab) {
+ // Create file URI and test data file paths.
+ let testFile = getChromeDir(getResolvedURI(gTestPath));
+ testFile.append("dummy_page.html");
+ const fileUriString = Services.io.newFileURI(testFile).spec;
+ let filePaths = [];
+ testFile.leafName = "form_data_file.txt";
+ filePaths.push(testFile.path);
+ testFile.leafName = "form_data_file.bin";
+ filePaths.push(testFile.path);
+
+ // Open file:// page tab in which to run the test.
+ await BrowserTestUtils.withNewTab(
+ fileUriString,
+ async function (fileBrowser) {
+ // Create a form to post to server that writes posted data into body as JSON.
+
+ var promiseLoad;
+ if (doNewTab) {
+ promiseLoad = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ TEST_HTTP_POST,
+ true,
+ false
+ );
+ } else {
+ promiseLoad = BrowserTestUtils.browserLoaded(
+ fileBrowser,
+ false,
+ TEST_HTTP_POST
+ );
+ }
+
+ /* eslint-disable no-shadow */
+ await SpecialPowers.spawn(
+ fileBrowser,
+ [TEST_HTTP_POST, filePaths, doNewTab],
+ (actionUri, filePaths, doNewTab) => {
+ // eslint-disable-next-line mozilla/reject-importGlobalProperties
+ Cu.importGlobalProperties(["File"]);
+
+ let doc = content.document;
+ let form = doc.body.appendChild(doc.createElement("form"));
+ form.action = actionUri;
+ form.method = "POST";
+ form.enctype = "multipart/form-data";
+ if (doNewTab) {
+ form.target = "_blank";
+ }
+
+ let inputText = form.appendChild(doc.createElement("input"));
+ inputText.type = "text";
+ inputText.name = "text";
+ inputText.value = "posted";
+
+ let inputCheckboxOn = form.appendChild(doc.createElement("input"));
+ inputCheckboxOn.type = "checkbox";
+ inputCheckboxOn.name = "checked";
+ inputCheckboxOn.checked = true;
+
+ let inputCheckboxOff = form.appendChild(doc.createElement("input"));
+ inputCheckboxOff.type = "checkbox";
+ inputCheckboxOff.name = "unchecked";
+ inputCheckboxOff.checked = false;
+
+ let inputFile = form.appendChild(doc.createElement("input"));
+ inputFile.type = "file";
+ inputFile.name = "file";
+ let fileList = [];
+ let promises = [];
+ for (let path of filePaths) {
+ promises.push(
+ File.createFromFileName(path).then(file => {
+ fileList.push(file);
+ })
+ );
+ }
+
+ Promise.all(promises).then(() => {
+ inputFile.mozSetFileArray(fileList);
+ form.submit();
+ });
+ }
+ );
+ /* eslint-enable no-shadow */
+
+ var href;
+ var testBrowser;
+ var newTab;
+ if (doNewTab) {
+ newTab = await promiseLoad;
+ testBrowser = newTab.linkedBrowser;
+ href = testBrowser.currentURI.spec;
+ } else {
+ testBrowser = fileBrowser;
+ href = await promiseLoad;
+ }
+ is(
+ href,
+ TEST_HTTP_POST,
+ "Check that the loaded page is the one to which we posted."
+ );
+
+ let binContentType;
+ if (AppConstants.platform == "macosx") {
+ binContentType = "application/macbinary";
+ } else {
+ binContentType = "application/octet-stream";
+ }
+
+ /* eslint-disable no-shadow */
+ await SpecialPowers.spawn(
+ testBrowser,
+ [binContentType],
+ binContentType => {
+ let data = JSON.parse(content.document.body.textContent);
+ is(
+ data[0].headers["Content-Disposition"],
+ 'form-data; name="text"',
+ "Check text input Content-Disposition"
+ );
+ is(data[0].body, "posted", "Check text input body");
+
+ is(
+ data[1].headers["Content-Disposition"],
+ 'form-data; name="checked"',
+ "Check checkbox input Content-Disposition"
+ );
+ is(data[1].body, "on", "Check checkbox input body");
+
+ // Note that unchecked checkbox details are not sent.
+
+ is(
+ data[2].headers["Content-Disposition"],
+ 'form-data; name="file"; filename="form_data_file.txt"',
+ "Check text file input Content-Disposition"
+ );
+ is(
+ data[2].headers["Content-Type"],
+ "text/plain",
+ "Check text file input Content-Type"
+ );
+ is(data[2].body, "1234\n", "Check text file input body");
+
+ is(
+ data[3].headers["Content-Disposition"],
+ 'form-data; name="file"; filename="form_data_file.bin"',
+ "Check binary file input Content-Disposition"
+ );
+ is(
+ data[3].headers["Content-Type"],
+ binContentType,
+ "Check binary file input Content-Type"
+ );
+ is(
+ data[3].body,
+ "\u0001\u0002\u0003\u0004\n",
+ "Check binary file input body"
+ );
+ }
+ );
+ /* eslint-enable no-shadow */
+
+ if (newTab) {
+ BrowserTestUtils.removeTab(newTab);
+ }
+ }
+ );
+}
+
+add_task(async function runWithDocumentChannel() {
+ await runTest(false);
+ await runTest(true);
+});
diff --git a/dom/html/test/browser_refresh_after_document_write.js b/dom/html/test/browser_refresh_after_document_write.js
new file mode 100644
index 0000000000..88e0dbe489
--- /dev/null
+++ b/dom/html/test/browser_refresh_after_document_write.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+Test that after using document.write(...), refreshing the document and calling write again,
+resulting document.URL is identical to the original URL.
+
+This testcase is aimed at preventing bug 619092
+*/
+var testURL =
+ "http://mochi.test:8888/browser/dom/html/test/file_refresh_after_document_write.html";
+let aTab, aBrowser;
+
+function test() {
+ waitForExplicitFinish();
+
+ aTab = BrowserTestUtils.addTab(gBrowser, testURL);
+ aBrowser = gBrowser.getBrowserForTab(aTab);
+ BrowserTestUtils.browserLoaded(aBrowser)
+ .then(() => {
+ is(
+ aBrowser.currentURI.spec,
+ testURL,
+ "Make sure we start at the correct URL"
+ );
+
+ SpecialPowers.spawn(aBrowser, [], () => {
+ // test_btn calls document.write() then reloads the document
+ let test_btn = content.document.getElementById("test_btn");
+
+ docShell.chromeEventHandler.addEventListener(
+ "load",
+ () => {
+ test_btn.click();
+ },
+ { once: true, capture: true }
+ );
+
+ test_btn.click();
+ });
+
+ return BrowserTestUtils.browserLoaded(aBrowser);
+ })
+ .then(() => {
+ return SpecialPowers.spawn(aBrowser, [], () => content.document.URL);
+ })
+ .then(url => {
+ is(url, testURL, "Document URL should be identical after reload");
+ gBrowser.removeTab(aTab);
+ finish();
+ });
+}
diff --git a/dom/html/test/browser_submission_flush.js b/dom/html/test/browser_submission_flush.js
new file mode 100644
index 0000000000..add886c6a3
--- /dev/null
+++ b/dom/html/test/browser_submission_flush.js
@@ -0,0 +1,97 @@
+"use strict";
+// Form submissions triggered by the Javascript 'submit' event listener are
+// deferred until the event listener finishes. However, changes to specific
+// attributes ("action" and "target" attributes) need to cause an immediate
+// flush of any pending submission to prevent the form submission from using the
+// wrong action or target. This test ensures that such flushes happen properly.
+
+const kTestPage =
+ "https://example.org/browser/dom/html/test/submission_flush.html";
+// This is the page pointed to by the form action in the test HTML page.
+const kPostActionPage =
+ "https://example.org/browser/dom/html/test/post_action_page.html";
+
+const kFormId = "test_form";
+const kFrameId = "test_frame";
+const kSubmitButtonId = "submit_button";
+
+// Take in a variety of actions (in the form of setting and unsetting form
+// attributes). Then submit the form in the submit event listener to cause a
+// deferred form submission. Then perform the test actions and ensure that the
+// form used the correct attribute values rather than the changed ones.
+async function runTest(aTestActions) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kTestPage);
+ registerCleanupFunction(() => BrowserTestUtils.removeTab(tab));
+
+ /* eslint-disable no-shadow */
+ let frame_url = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ kFormId, kFrameId, kSubmitButtonId, aTestActions }],
+ async function ({ kFormId, kFrameId, kSubmitButtonId, aTestActions }) {
+ let form = content.document.getElementById(kFormId);
+
+ form.addEventListener(
+ "submit",
+ event => {
+ // Need to trigger the deferred submission by submitting in the submit
+ // event handler. To prevent the form from being submitted twice, the
+ // <form> tag contains the attribute |onsubmit="return false;"| to cancel
+ // the original submission.
+ form.submit();
+
+ if (aTestActions.setattr) {
+ for (let { attr, value } of aTestActions.setattr) {
+ form.setAttribute(attr, value);
+ }
+ }
+ if (aTestActions.unsetattr) {
+ for (let attr of aTestActions.unsetattr) {
+ form.removeAttribute(attr);
+ }
+ }
+ },
+ { capture: true, once: true }
+ );
+
+ // Trigger the above event listener
+ content.document.getElementById(kSubmitButtonId).click();
+
+ // Test that the form was submitted to the correct target (the frame) with
+ // the correct action (kPostActionPage).
+ let frame = content.document.getElementById(kFrameId);
+ await new Promise(resolve => {
+ frame.addEventListener("load", resolve, { once: true });
+ });
+ return frame.contentWindow.location.href;
+ }
+ );
+ /* eslint-enable no-shadow */
+ is(
+ frame_url,
+ kPostActionPage,
+ "Form should have submitted with correct target and action"
+ );
+}
+
+add_task(async function () {
+ info("Changing action should flush pending submissions");
+ await runTest({ setattr: [{ attr: "action", value: "about:blank" }] });
+});
+
+add_task(async function () {
+ info("Changing target should flush pending submissions");
+ await runTest({ setattr: [{ attr: "target", value: "_blank" }] });
+});
+
+add_task(async function () {
+ info("Unsetting action should flush pending submissions");
+ await runTest({ unsetattr: ["action"] });
+});
+
+// On failure, this test will time out rather than failing an assert. When the
+// target attribute is not set, the form will submit the active page, navigating
+// it away and preventing the wait for iframe load from ever finishing.
+add_task(async function () {
+ info("Unsetting target should flush pending submissions");
+ await runTest({ unsetattr: ["target"] });
+});
diff --git a/dom/html/test/browser_targetBlankNoOpener.js b/dom/html/test/browser_targetBlankNoOpener.js
new file mode 100644
index 0000000000..1647df0be2
--- /dev/null
+++ b/dom/html/test/browser_targetBlankNoOpener.js
@@ -0,0 +1,121 @@
+const TEST_URL = "http://mochi.test:8888/browser/dom/html/test/empty.html";
+
+async function checkOpener(browser, elm, name, rel) {
+ let p = BrowserTestUtils.waitForNewTab(gBrowser, null, true, true);
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ url: TEST_URL, name, rel, elm }],
+ async obj => {
+ let element;
+
+ if (obj.elm == "anchor") {
+ element = content.document.createElement("a");
+ content.document.body.appendChild(element);
+ element.appendChild(content.document.createTextNode(obj.name));
+ } else {
+ let img = content.document.createElement("img");
+ img.src = "image_yellow.png";
+ content.document.body.appendChild(img);
+
+ element = content.document.createElement("area");
+ img.appendChild(element);
+
+ element.setAttribute("shape", "rect");
+ element.setAttribute("coords", "0,0,100,100");
+ }
+
+ element.setAttribute("target", "_blank");
+ element.setAttribute("href", obj.url);
+
+ if (obj.rel) {
+ element.setAttribute("rel", obj.rel);
+ }
+
+ element.click();
+ }
+ );
+
+ let newTab = await p;
+ let newBrowser = gBrowser.getBrowserForTab(newTab);
+
+ let hasOpener = await SpecialPowers.spawn(
+ newTab.linkedBrowser,
+ [],
+ _ => !!content.window.opener
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+ return hasOpener;
+}
+
+async function runTests(browser, elm) {
+ info("Creating an " + elm + " with target=_blank rel=opener");
+ ok(
+ !!(await checkOpener(browser, elm, "rel=opener", "opener")),
+ "We want the opener with rel=opener"
+ );
+
+ info("Creating an " + elm + " with target=_blank rel=noopener");
+ ok(
+ !(await checkOpener(browser, elm, "rel=noopener", "noopener")),
+ "We don't want the opener with rel=noopener"
+ );
+
+ info("Creating an " + elm + " with target=_blank");
+ ok(
+ !(await checkOpener(browser, elm, "no rel", null)),
+ "We don't want the opener with no rel is passed"
+ );
+
+ info("Creating an " + elm + " with target=_blank rel='noopener opener'");
+ ok(
+ !(await checkOpener(
+ browser,
+ elm,
+ "rel=noopener+opener",
+ "noopener opener"
+ )),
+ "noopener wins with rel=noopener+opener"
+ );
+
+ info("Creating an " + elm + " with target=_blank rel='noreferrer opener'");
+ ok(
+ !(await checkOpener(browser, elm, "noreferrer wins", "noreferrer opener")),
+ "We don't want the opener with rel=noreferrer+opener"
+ );
+
+ info("Creating an " + elm + " with target=_blank rel='opener noreferrer'");
+ ok(
+ !(await checkOpener(
+ browser,
+ elm,
+ "noreferrer wins again",
+ "noreferrer opener"
+ )),
+ "We don't want the opener with rel=opener+noreferrer"
+ );
+}
+
+add_task(async _ => {
+ await SpecialPowers.flushPrefEnv();
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.block_multiple_popups", false],
+ ["dom.disable_open_during_load", true],
+ ["dom.targetBlankNoOpener.enabled", true],
+ ],
+ });
+
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await runTests(browser, "anchor");
+ await runTests(browser, "area");
+
+ info("Removing the tab");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/html/test/bug100533_iframe.html b/dom/html/test/bug100533_iframe.html
new file mode 100644
index 0000000000..ddf58a15c6
--- /dev/null
+++ b/dom/html/test/bug100533_iframe.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title></title>
+</head>
+<body>
+<form method='get' action='bug100533_load.html' id='b'><input type="submit"/></form>
+</body>
+</html>
diff --git a/dom/html/test/bug100533_load.html b/dom/html/test/bug100533_load.html
new file mode 100644
index 0000000000..99cf26640c
--- /dev/null
+++ b/dom/html/test/bug100533_load.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+<title></title>
+</head>
+
+
+<body onload="parent.submitted();">
+
+<span id="foo"></span>
+
+
+
+</body>
+</html>
diff --git a/dom/html/test/bug1260704_iframe.html b/dom/html/test/bug1260704_iframe.html
new file mode 100644
index 0000000000..695dc7c1ac
--- /dev/null
+++ b/dom/html/test/bug1260704_iframe.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript">
+ var noDefault = (location.search.includes("noDefault=true"));
+ var isMap = (location.search.includes("isMap=true"));
+
+ window.addEventListener("load", () => {
+ let image = document.getElementById("testImage");
+ isMap ? image.setAttribute("ismap", "") : image.removeAttribute("ismap");
+ image.addEventListener("click", event => {
+ if (noDefault) {
+ ok(true, "image element prevents default");
+ event.preventDefault();
+ }
+ });
+
+ window.addEventListener("click", event => {
+ ok(true, "expected prevent default = " + noDefault);
+ ok(true, "actual prevent default = " + event.defaultPrevented);
+ ok(event.defaultPrevented == noDefault, "PreventDefault should work fine");
+ if (noDefault) {
+ window.parent.postMessage("finished", "http://mochi.test:8888");
+ }
+ });
+ window.parent.postMessage("started", "http://mochi.test:8888");
+ });
+ </script>
+</head>
+<body>
+<a href="bug1260704_iframe_empty.html">
+ <img id="testImage" src="file_bug1260704.png" width="100" height="100"/>
+</a>
+</body>
+</html>
diff --git a/dom/html/test/bug1260704_iframe_empty.html b/dom/html/test/bug1260704_iframe_empty.html
new file mode 100644
index 0000000000..e826b1e5e6
--- /dev/null
+++ b/dom/html/test/bug1260704_iframe_empty.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript">
+ window.addEventListener("load", () => {
+ window.parent.postMessage("empty_frame_loaded", "http://mochi.test:8888");
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/html/test/bug1292522_iframe.html b/dom/html/test/bug1292522_iframe.html
new file mode 100644
index 0000000000..99a3369d00
--- /dev/null
+++ b/dom/html/test/bug1292522_iframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html><head><title>iframe</title></head>
+ <body>
+ <p>var testvar = "testiframe"</p>
+ <script>
+ document.domain='example.org';
+ var testvar = "testiframe";
+ </script>
+ </body>
+</html>
diff --git a/dom/html/test/bug1292522_page.html b/dom/html/test/bug1292522_page.html
new file mode 100644
index 0000000000..9570f12d2d
--- /dev/null
+++ b/dom/html/test/bug1292522_page.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test for Bug 1292522</title>
+ <script>
+ var check_var = function() {
+ opener.postMessage(document.getElementsByTagName('iframe')[0].contentWindow.testvar, "http://mochi.test:8888");
+ }
+ </script>
+ </head>
+ <body>
+ <iframe src="http://test2.example.org:80/tests/dom/html/test/bug1292522_iframe.html" onload="document.domain='example.org';check_var();"></iframe>
+ </body>
+</html>
diff --git a/dom/html/test/bug1315146-iframe.html b/dom/html/test/bug1315146-iframe.html
new file mode 100644
index 0000000000..280db53052
--- /dev/null
+++ b/dom/html/test/bug1315146-iframe.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+document.domain = "example.org";
+</script>
diff --git a/dom/html/test/bug1315146-main.html b/dom/html/test/bug1315146-main.html
new file mode 100644
index 0000000000..e9f356dda6
--- /dev/null
+++ b/dom/html/test/bug1315146-main.html
@@ -0,0 +1,15 @@
+<!DOCTYPE HTML>
+<iframe src="http://example.org/tests/dom/html/test/bug1315146-iframe.html"></iframe>
+<input value="test">
+<script>
+document.domain = "example.org";
+onload = function() {
+ let iframe = document.querySelector("iframe");
+ let input = document.querySelector("input");
+ input.selectionStart = input.selectionEnd = 2;
+ document.body.style.overflow = "scroll";
+ iframe.contentDocument.body.offsetWidth;
+ opener.postMessage({start: input.selectionStart,
+ end: input.selectionEnd}, "*");
+}
+</script>
diff --git a/dom/html/test/bug196523-subframe.html b/dom/html/test/bug196523-subframe.html
new file mode 100644
index 0000000000..ac53572a7a
--- /dev/null
+++ b/dom/html/test/bug196523-subframe.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<script>
+ function checkDomain(str, msg) {
+ window.parent.postMessage((str == document.domain) + ";" +msg,
+ "http://mochi.test:8888");
+ }
+
+ function reportException(msg) {
+ window.parent.postMessage(false + ";" + msg, "http://mochi.test:8888");
+ }
+
+ var win1;
+ try {
+ win1 = window.open("", "", "width=100,height=100");
+ var otherDomain1 = win1.document.domain;
+ win1.close();
+ checkDomain(otherDomain1, "Opened document should have our domain");
+ } catch(e) {
+ reportException("Exception getting document.domain: " + e);
+ } finally {
+ win1.close();
+ }
+
+ document.domain = "example.org";
+
+ var win2;
+ try {
+ win2 = window.open("", "", "width=100,height=100");
+ var otherDomain2 = win2.document.domain;
+ checkDomain(otherDomain2, "Opened document should have our domain");
+ win2.close();
+ } catch(e) {
+ reportException("Exception getting document.domain after domain set: " + e);
+ } finally {
+ win2.close();
+ }
+</script>
diff --git a/dom/html/test/bug199692-nested-d2.html b/dom/html/test/bug199692-nested-d2.html
new file mode 100644
index 0000000000..70064efe74
--- /dev/null
+++ b/dom/html/test/bug199692-nested-d2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=199692
+-->
+<head>
+ <title>Nested, nested iframe for bug 199692 tests</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+</head>
+<body>
+ <div id="nest2div" style="border: 2px dotted blue;">nested, depth 2</div>
+</body>
+</html>
+
diff --git a/dom/html/test/bug199692-nested.html b/dom/html/test/bug199692-nested.html
new file mode 100644
index 0000000000..27201a953d
--- /dev/null
+++ b/dom/html/test/bug199692-nested.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=199692
+-->
+<head>
+ <title>Nested iframe for bug 199692 tests</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+</head>
+<body>
+ <div id="nest1div" style="border: 2px dotted green;">nested, depth 1</div>
+ <iframe src="bug199692-nested-d2.html"></iframe>
+</body>
+</html>
+
diff --git a/dom/html/test/bug199692-popup.html b/dom/html/test/bug199692-popup.html
new file mode 100644
index 0000000000..de93ca8599
--- /dev/null
+++ b/dom/html/test/bug199692-popup.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=199692
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Popup in test for Bug 199692</title>
+ <style type="text/css">
+#content * {
+ border: 2px solid black;
+ margin: 2px;
+ clear: both;
+ height: 20px;
+ overflow: hidden;
+}
+
+#txt, #static, #fixed, #absolute, #relative, #hidden, #float, #empty, #static, #relative {
+ width: 200px !important;
+}
+ </style>
+
+</head>
+<!--
+Elements are styled in such a way that they don't overlap visually
+unless they also overlap structurally.
+
+This file is designed to be opened from test_bug199692.html in a popup
+window, to guarantee that the window in which document.elementFromPoint runs
+is large enough to display all the elements being tested.
+-->
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=199692">Mozilla Bug 199692</a>
+
+<div id="content" style="width: 500px; background-color: #ccc;">
+
+<!-- element containing text -->
+<div id="txt" style="height: 30px;">txt</div>
+
+<!-- element not containing text -->
+<div id="empty" style="border: 2px solid blue;"></div>
+
+<!-- element with only whitespace -->
+<p id="whitespace" style="border: 2px solid magenta;"> </p>
+
+<!-- position: static -->
+<span id="static" style="position: static; border-color: green;">static</span>
+
+<!-- floated element -->
+<div id="float" style="border-color: red; float: right;">float</div>
+
+<!-- position: fixed -->
+<span id="fixed" style="position: fixed; top: 500px; left: 100px; border: 3px solid yellow;">fixed</span>
+
+<!-- position: absolute -->
+<span id="absolute" style="position: absolute; top: 550px; left: 150px; border-color: orange;">abs</span>
+
+<!-- position: relative -->
+<div id="relative" style="position: relative; top: 200px; border-color: teal;">rel</div>
+
+<!-- visibility: hidden -->
+<div id="hidden-wrapper" style="border: 1px dashed teal;">
+ <div id="hidden" style="opacity: 0.5; background-color: blue; visibility:hidden;">hidden</div>
+</div>
+
+<!-- iframe (within iframe) -->
+<iframe id="our-iframe" src="bug199692-nested.html" style="height: 100px; overflow: scroll;"></iframe>
+
+<input type="textbox" id="textbox" value="textbox"></input>
+</div>
+
+<!-- interaction with scrolling -->
+<iframe id="scrolled-iframe"
+ src="bug199692-scrolled.html#down"
+ style="position: absolute; top: 345px; left: 325px; height: 200px; width: 200px"></iframe>
+
+<script type="application/javascript">
+
+var SimpleTest = window.opener.SimpleTest;
+function ok() { window.opener.ok.apply(window.opener, arguments); }
+function is() { window.opener.is.apply(window.opener, arguments); }
+function todo() { window.opener.todo.apply(window.opener, arguments); }
+function todo_is() { window.opener.todo_is.apply(window.opener, arguments); }
+function $(id) { return document.getElementById(id); }
+
+/**
+ * Like is, but for tests which don't always succeed or always fail on all
+ * platforms.
+ */
+function random_fail(a, b, m)
+{
+ if (a != b)
+ todo_is(a, b, m);
+ else
+ is(a, b, m);
+}
+
+/* Test for Bug 199692 */
+
+function getCoords(elt)
+{
+ var x = 0, y = 0;
+
+ do
+ {
+ x += elt.offsetLeft;
+ y += elt.offsetTop;
+ } while ((elt = elt.offsetParent));
+
+ return { x, y };
+}
+
+var elts = ["txt", "empty", "whitespace", "static", "fixed", "absolute",
+ "relative", "float", "textbox"];
+
+function testPoints()
+{
+ ok('elementFromPoint' in document, "document.elementFromPoint must exist");
+ ok(typeof document.elementFromPoint === "function", "must be a function");
+
+ var doc = document;
+ doc.pt = doc.elementFromPoint; // for shorter lines
+ is(doc.pt(-1, 0), null, "Negative coordinates (-1, 0) should return null");
+ is(doc.pt(0, -1), null, "Negative coordinates (0, -1) should return null");
+ is(doc.pt(-1, -1), null, "Negative coordinates (-1, -1) should return null");
+
+ var pos;
+ for (var i = 0; i < elts.length; i++)
+ {
+ var id = elts[i];
+ var elt = $(id);
+
+ // The upper left corner of an element (with a moderate offset) will
+ // usually contain text, and the lower right corner usually won't.
+ var pos = getCoords(elt);
+ var x = pos.x, y = pos.y;
+ var w = elt.offsetWidth, h = elt.offsetHeight;
+
+ var d = 5;
+ is(doc.pt(x + d, y + d), elt,
+ "(" + (x + d) + "," + (y + d) + ") IDs should match (upper left " +
+ "corner of " + id + ")");
+ is(doc.pt(x + w - d, y + h - d), elt,
+ "(" + (x + w - d) + "," + (y + h - d) + ") IDs should match (lower " +
+ "right corner of " + id + ")");
+ }
+
+ // content
+ var c = $("content");
+ pos = getCoords(c);
+ x = pos.x + c.offsetWidth / 2;
+ y = pos.y;
+
+ // This fails on some platforms but not others for unknown reasons
+ random_fail(doc.pt(x, y), c, "Point to right of #txt should be #content");
+ is(doc.pt(x, y + 1), c, "Point to right of #txt should be #content");
+ random_fail(doc.pt(x + 1, y), c, "Point to right of #txt should be #content");
+ is(doc.pt(x + 1, y + 1), c, "Point to right of #txt should be #content");
+
+ // hidden
+ c = $("hidden");
+ pos = getCoords(c);
+ x = pos.x;
+ y = pos.y;
+ is(doc.pt(x, y), $("hidden-wrapper"),
+ "Hit testing should bypass hidden elements.");
+
+ // iframe nested
+ var iframe = $("our-iframe");
+ pos = getCoords(iframe);
+ x = pos.x;
+ y = pos.y;
+ is(doc.pt(x + 20, y + 20), $("our-iframe"),
+ "Element from nested iframe returned is from calling document");
+ // iframe, doubly nested
+ is(doc.pt(x + 60, y + 60), $("our-iframe"),
+ "Element from doubly nested iframe returned is from calling document");
+
+ // scrolled iframe tests
+ $("scrolled-iframe").contentWindow.runTests();
+
+ SimpleTest.finish();
+ window.close();
+}
+
+window.onload = testPoints;
+</script>
+</body>
+</html>
+
diff --git a/dom/html/test/bug199692-scrolled.html b/dom/html/test/bug199692-scrolled.html
new file mode 100644
index 0000000000..f13bf7ab12
--- /dev/null
+++ b/dom/html/test/bug199692-scrolled.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=199692
+-->
+<head>
+ <title>Scrolled page for bug 199692 tests</title>
+ <style type="text/css">
+/* Disable default margins/padding/borders so (0, 0) gets a div. */
+* { margin: 0; padding: 0; border: 0; }
+ </style>
+ <script type="application/javascript">
+function $(id) { return document.getElementById(id); }
+
+function runTests()
+{
+ var is = window.parent.is;
+
+ is(document.elementFromPoint(0, 0), $("down"),
+ "document.elementFromPoint not respecting scrolling?");
+ is(document.elementFromPoint(200, 200), null,
+ "should have returned null for a not-visible point");
+ is(document.elementFromPoint(3, -5), null,
+ "should have returned null for a not-visible point");
+}
+ </script>
+</head>
+<!-- This page is loaded in a 200px-square iframe scrolled to #down. -->
+<body>
+<div style="height: 150px; background: lightblue;">first</div>
+<div id="down" style="height: 250px; background: lightgreen;">second</div>
+</body>
+</html>
+
diff --git a/dom/html/test/bug242709_iframe.html b/dom/html/test/bug242709_iframe.html
new file mode 100644
index 0000000000..1155299692
--- /dev/null
+++ b/dom/html/test/bug242709_iframe.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<title></title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script type="text/javascript">
+function submitIframeForm () {
+ document.getElementById('b').submit();
+ document.getElementById('thebutton').disabled = true;
+}
+</script>
+
+</head>
+<body onload="sendMouseEvent({type:'click'}, 'thebutton')">
+
+<form method="get" action="bug242709_load.html" id="b">
+<input type="submit" onclick="submitIframeForm()" id="thebutton">
+</form>
+
+</body>
+</html>
diff --git a/dom/html/test/bug242709_load.html b/dom/html/test/bug242709_load.html
new file mode 100644
index 0000000000..c9be79b241
--- /dev/null
+++ b/dom/html/test/bug242709_load.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title></title>
+</head>
+
+<body onload="parent.submitted();">
+
+<span id="foo"></span>
+
+</body>
+</html>
diff --git a/dom/html/test/bug277724_iframe1.html b/dom/html/test/bug277724_iframe1.html
new file mode 100644
index 0000000000..d0d881b766
--- /dev/null
+++ b/dom/html/test/bug277724_iframe1.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Use an unload handler to prevent bfcache from messing with us -->
+<body onunload="parent.childUnloaded = true;">
+ <select id="select">
+ <option>aaa</option>
+ <option>bbbb</option>
+ </select>
+
+ <textarea id="textarea">
+ </textarea>
+
+ <input type="text" id="text">
+ <input type="password" id="password">
+ <input type="checkbox" id="checkbox">
+ <input type="radio" id="radio">
+ <input type="image" id="image">
+ <input type="submit" id="submit">
+ <input type="reset" id="reset">
+ <input type="button" id="button input">
+ <input type="hidden" id="hidden">
+ <input type="file" id="file">
+
+ <button type="submit" id="submit button"></button>
+ <button type="reset" id="reset button"></button>
+ <button type="button" id="button"></button>
+</body>
+</html>
diff --git a/dom/html/test/bug277724_iframe2.xhtml b/dom/html/test/bug277724_iframe2.xhtml
new file mode 100644
index 0000000000..14423aa06c
--- /dev/null
+++ b/dom/html/test/bug277724_iframe2.xhtml
@@ -0,0 +1,27 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!-- Use an unload handler to prevent bfcache from messing with us -->
+<body onunload="parent.childUnloaded = true;">
+ <select id="select">
+ <option>aaa</option>
+ <option>bbbb</option>
+ </select>
+
+ <textarea id="textarea">
+ </textarea>
+
+ <input type="text" id="text" />
+ <input type="password" id="password" />
+ <input type="checkbox" id="checkbox" />
+ <input type="radio" id="radio" />
+ <input type="image" id="image" />
+ <input type="submit" id="submit" />
+ <input type="reset" id="reset" />
+ <input type="button" id="button input" />
+ <input type="hidden" id="hidden" />
+ <input type="file" id="file" />
+
+ <button type="submit" id="submit button"></button>
+ <button type="reset" id="reset button"></button>
+ <button type="button" id="button"></button>
+</body>
+</html>
diff --git a/dom/html/test/bug277890_iframe.html b/dom/html/test/bug277890_iframe.html
new file mode 100644
index 0000000000..c1cb4ff2e1
--- /dev/null
+++ b/dom/html/test/bug277890_iframe.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<title></title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script type="text/javascript">
+function submitIframeForm () {
+ document.getElementById('b').submit();
+ document.getElementById('thebutton').disabled = true;
+}
+</script>
+
+</head>
+<body onload="sendMouseEvent({type:'click'}, 'thebutton')">
+
+<form method="get" action="bug277890_load.html" id="b">
+<button onclick="submitIframeForm()" id="thebutton">Submit</button>
+</form>
+
+</body>
+</html>
diff --git a/dom/html/test/bug277890_load.html b/dom/html/test/bug277890_load.html
new file mode 100644
index 0000000000..c9be79b241
--- /dev/null
+++ b/dom/html/test/bug277890_load.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title></title>
+</head>
+
+<body onload="parent.submitted();">
+
+<span id="foo"></span>
+
+</body>
+</html>
diff --git a/dom/html/test/bug340800_iframe.txt b/dom/html/test/bug340800_iframe.txt
new file mode 100644
index 0000000000..369dfe7441
--- /dev/null
+++ b/dom/html/test/bug340800_iframe.txt
@@ -0,0 +1,4 @@
+Line 1.
+Line 2.
+Line 3.
+Line 4.
diff --git a/dom/html/test/bug369370-popup.png b/dom/html/test/bug369370-popup.png
new file mode 100644
index 0000000000..9063d12648
--- /dev/null
+++ b/dom/html/test/bug369370-popup.png
Binary files differ
diff --git a/dom/html/test/bug372098-link-target.html b/dom/html/test/bug372098-link-target.html
new file mode 100644
index 0000000000..b22b8e020e
--- /dev/null
+++ b/dom/html/test/bug372098-link-target.html
@@ -0,0 +1,7 @@
+<html>
+<script type="text/javascript">
+
+parent.callback(location.search.substr(1));
+
+</script>
+</html>
diff --git a/dom/html/test/bug436200.html b/dom/html/test/bug436200.html
new file mode 100644
index 0000000000..1ef7e73b5e
--- /dev/null
+++ b/dom/html/test/bug436200.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Secure to Insecure Test</title>
+ </head>
+ <body>
+ <form id="test_form" action="http://example.org/browser/dom/html/test/bug436200.html">
+ <button type="submit" id="submit_button">Submit</button>
+ </form>
+ </body>
+</html>
diff --git a/dom/html/test/bug441930_iframe.html b/dom/html/test/bug441930_iframe.html
new file mode 100644
index 0000000000..532cd5c36a
--- /dev/null
+++ b/dom/html/test/bug441930_iframe.html
@@ -0,0 +1,27 @@
+<html>
+<body>
+ The content of this <code>textarea</code> should not disappear on page reload:<br />
+ <textarea>This text should not disappear on page reload!</textarea>
+ <script>
+ var ta = document.getElementsByTagName("textarea").item(0);
+ if (!parent.reloaded) {
+ parent.reloaded = true;
+ ta.disabled = true;
+ location.reload();
+ } else {
+ // Primary regression test:
+ parent.isnot(ta.value, "",
+ "Content of dynamically disabled textarea disappeared on page reload.");
+
+ // Bonus regression test: changing the textarea's defaultValue after
+ // reloading should also update the textarea's value.
+ var newDefaultValue = "new default value";
+ ta.defaultValue = newDefaultValue;
+ parent.is(ta.value, newDefaultValue,
+ "Changing the defaultValue attribute of a textarea fails to update its value attribute.");
+
+ parent.SimpleTest.finish();
+ }
+ </script>
+</body>
+</html>
diff --git a/dom/html/test/bug445004-inner.html b/dom/html/test/bug445004-inner.html
new file mode 100644
index 0000000000..b946520ea6
--- /dev/null
+++ b/dom/html/test/bug445004-inner.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <base href="http://test1.example.org/tests/dom/html/test/bug445004-inner.html">
+ <script src="bug445004-inner.js"></script>
+ </head>
+ <body>
+ <iframe name="w" id="w" width="100" height="100"></iframe>
+ <iframe name="x" id="x" width="100" height="100"></iframe>
+ <iframe name="y" id="y" width="100" height="100"></iframe>
+ <iframe name="z" id="z" width="100" height="100"></iframe>
+ <img src="test1.example.org.png">
+ </body>
+</html>
diff --git a/dom/html/test/bug445004-inner.js b/dom/html/test/bug445004-inner.js
new file mode 100644
index 0000000000..2d751da454
--- /dev/null
+++ b/dom/html/test/bug445004-inner.js
@@ -0,0 +1,27 @@
+document.domain = "example.org";
+function $(str) {
+ return document.getElementById(str);
+}
+function hookLoad(str) {
+ $(str).onload = function () {
+ window.parent.parent.postMessage("end", "*");
+ };
+ window.parent.parent.postMessage("start", "*");
+}
+window.onload = function () {
+ hookLoad("w");
+ $("w").contentWindow.location.href = "test1.example.org.png";
+ hookLoad("x");
+ var doc = $("x").contentDocument;
+ doc.write('<img src="test1.example.org.png">');
+ doc.close();
+};
+function doIt() {
+ hookLoad("y");
+ $("y").contentWindow.location.href = "example.org.png";
+ hookLoad("z");
+ var doc = $("z").contentDocument;
+ doc.write('<img src="example.org.png">');
+ doc.close();
+}
+window.addEventListener("message", doIt);
diff --git a/dom/html/test/bug445004-outer-abs.html b/dom/html/test/bug445004-outer-abs.html
new file mode 100644
index 0000000000..8a93ef2b73
--- /dev/null
+++ b/dom/html/test/bug445004-outer-abs.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <base href="http://example.org/tests/dom/html/test/bug445004-outer.html">
+ <script>document.domain = "example.org"</script>
+ </head>
+ <body>
+ <iframe width="500" height="200" src="http://test1.example.org/tests/dom/html/test/bug445004-inner.html"
+ onload="window.frames[0].doIt()"></iframe>
+ </body>
+</html>
diff --git a/dom/html/test/bug445004-outer-rel.html b/dom/html/test/bug445004-outer-rel.html
new file mode 100644
index 0000000000..0967338899
--- /dev/null
+++ b/dom/html/test/bug445004-outer-rel.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <base href="http://example.org/tests/dom/html/test/bug445004-outer.html">
+ <script>document.domain = "example.org"</script>
+ </head>
+ <body>
+ <iframe width="500" height="200" src="bug445004-inner.html"
+ onload="window.frames[0].doIt()"></iframe>
+ </body>
+</html>
diff --git a/dom/html/test/bug445004-outer-write.html b/dom/html/test/bug445004-outer-write.html
new file mode 100644
index 0000000000..be6e37b6d7
--- /dev/null
+++ b/dom/html/test/bug445004-outer-write.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <base href="http://example.org/tests/dom/html/test/bug445004-outer.html">
+ <script>document.domain = "example.org"</script>
+ </head>
+ <body>
+ <iframe width="500" height="200" src="javascript:&quot;<!DOCTYPE html> <html> <script> function $(str) { return document.getElementById(str); } function hookLoad(str) { $(str).onload = function() { window.parent.parent.postMessage('end', '*'); }; window.parent.parent.postMessage('start', '*'); } window.onload = function() { hookLoad(\&quot;w\&quot;); $(\&quot;w\&quot;).contentWindow.location.href = \&quot;example.org.png\&quot;; hookLoad(\&quot;x\&quot;); var doc = $(\&quot;x\&quot;).contentDocument; doc.write('<img src=\&quot;example.org.png\&quot;>'); doc.close(); }; function doIt() { hookLoad(\&quot;y\&quot;); $(\&quot;y\&quot;).contentWindow.location.href = \&quot;example.org.png\&quot;; hookLoad(\&quot;z\&quot;); var doc = $(\&quot;z\&quot;).contentDocument; doc.write('<img src=\&quot;example.org.png\&quot;>'); doc.close(); } </script> <body> <iframe name=\&quot;w\&quot; id=\&quot;w\&quot; width=\&quot;100\&quot; height=\&quot;100\&quot;></iframe> <iframe name=\&quot;x\&quot; id=\&quot;x\&quot; width=\&quot;100\&quot; height=\&quot;100\&quot;></iframe> <iframe name=\&quot;y\&quot; id=\&quot;y\&quot; width=\&quot;100\&quot; height=\&quot;100\&quot;></iframe> <iframe name=\&quot;z\&quot; id=\&quot;z\&quot; width=\&quot;100\&quot; height=\&quot;100\&quot;></iframe><img src=\&quot;example.org.png\&quot;> </body> </html>&quot; "
+ onload="window.frames[0].doIt();"></iframe>
+ </body>
+</html>
diff --git a/dom/html/test/bug446483-iframe.html b/dom/html/test/bug446483-iframe.html
new file mode 100644
index 0000000000..fe5a6cf9f7
--- /dev/null
+++ b/dom/html/test/bug446483-iframe.html
@@ -0,0 +1,10 @@
+<script>
+function doe(){
+window.focus();
+window.getSelection().collapse(document.body, 0);
+}
+setTimeout(doe,50);
+
+setTimeout(function() {window.location.reload()}, 200);
+</script>
+<span contenteditable="true"></span>
diff --git a/dom/html/test/bug448564-echo.sjs b/dom/html/test/bug448564-echo.sjs
new file mode 100644
index 0000000000..1eee116fd7
--- /dev/null
+++ b/dom/html/test/bug448564-echo.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ response.write(request.queryString);
+}
diff --git a/dom/html/test/bug448564-iframe-1.html b/dom/html/test/bug448564-iframe-1.html
new file mode 100644
index 0000000000..4f3e79e5d2
--- /dev/null
+++ b/dom/html/test/bug448564-iframe-1.html
@@ -0,0 +1,16 @@
+<html>
+<body>
+
+ <table>
+ <form action="bug448564-echo.sjs" method="GET">
+ <tr><td><input name="a" value="aval"></td></tr>
+ <input type="hidden" name="b" value="bval">
+ <input name="c" value="cval">
+ <tr><td><input name="d" value="dval" type="submit"></td></tr>
+ </form>
+ </table>
+
+ <script src="bug448564-submit.js"></script>
+
+</body>
+</html>
diff --git a/dom/html/test/bug448564-iframe-2.html b/dom/html/test/bug448564-iframe-2.html
new file mode 100644
index 0000000000..dba19b37e2
--- /dev/null
+++ b/dom/html/test/bug448564-iframe-2.html
@@ -0,0 +1,16 @@
+<html>
+<body>
+
+ <form action="bug448564-echo.sjs" method="GET">
+ <table>
+ <tr><td><input name="a" value="aval"></td></tr>
+ <input type="hidden" name="b" value="bval">
+ <input name="c" value="cval">
+ <tr><td><input name="d" value="dval" type="submit"></td></tr>
+ </table>
+ </form>
+
+ <script src="bug448564-submit.js"></script>
+
+</body>
+</html>
diff --git a/dom/html/test/bug448564-iframe-3.html b/dom/html/test/bug448564-iframe-3.html
new file mode 100644
index 0000000000..64288ebb15
--- /dev/null
+++ b/dom/html/test/bug448564-iframe-3.html
@@ -0,0 +1,16 @@
+<html>
+<body>
+
+ <table>
+ <span><form action="bug448564-echo.sjs" method="GET">
+ <tr><td><input name="a" value="aval"></td></tr>
+ <input type="hidden" name="b" value="bval">
+ <input name="c" value="cval">
+ <tr><td><input name="d" value="dval" type="submit"></td></tr>
+ </form></span>
+ </table>
+
+ <script src="bug448564-submit.js"></script>
+
+</body>
+</html>
diff --git a/dom/html/test/bug448564-submit.js b/dom/html/test/bug448564-submit.js
new file mode 100644
index 0000000000..a650487d65
--- /dev/null
+++ b/dom/html/test/bug448564-submit.js
@@ -0,0 +1,6 @@
+var inputs = document.getElementsByTagName("input");
+for (var input, i = 0; (input = inputs[i]); ++i) {
+ if ("submit" == input.type) {
+ input.click();
+ }
+}
diff --git a/dom/html/test/bug499092.html b/dom/html/test/bug499092.html
new file mode 100644
index 0000000000..0476fa4e76
--- /dev/null
+++ b/dom/html/test/bug499092.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<script>
+var title = document.createElementNS("http://www.w3.org/1999/xhtml", "aa:title");
+title.textContent = "HTML OK";
+document.documentElement.firstChild.appendChild(title);
+</script>
diff --git a/dom/html/test/bug499092.xml b/dom/html/test/bug499092.xml
new file mode 100644
index 0000000000..eedd2c77b3
--- /dev/null
+++ b/dom/html/test/bug499092.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<doc xmlns:aa="http://www.w3.org/1999/xhtml">
+<aa:title>XML OK</aa:title>
+</doc>
diff --git a/dom/html/test/bug514856_iframe.html b/dom/html/test/bug514856_iframe.html
new file mode 100644
index 0000000000..2abf9e91e2
--- /dev/null
+++ b/dom/html/test/bug514856_iframe.html
@@ -0,0 +1,21 @@
+<html>
+ <head>
+ <style>
+ html, body, a, img {
+ padding: 0px;
+ margin: 0px;
+ border: 0px;
+ }
+ img {
+ width: 100%;
+ height: 100%;
+ }
+ </style>
+ </head>
+ <body>
+ <a href="bug514856_iframe.html">
+ <img ismap="ismap"
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0vr4MkhoXe0rZigAAAABJRU5ErkJggg==">
+ </a>
+ </body>
+</html>
diff --git a/dom/html/test/bug592641_img.jpg b/dom/html/test/bug592641_img.jpg
new file mode 100644
index 0000000000..c9103b8b0e
--- /dev/null
+++ b/dom/html/test/bug592641_img.jpg
Binary files differ
diff --git a/dom/html/test/bug649134/file_bug649134-1.sjs b/dom/html/test/bug649134/file_bug649134-1.sjs
new file mode 100644
index 0000000000..fed0a9d693
--- /dev/null
+++ b/dom/html/test/bug649134/file_bug649134-1.sjs
@@ -0,0 +1,12 @@
+function handleRequest(request, response) {
+ response.seizePower();
+ var r =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/html\r\n" +
+ 'Link: < \014>; rel="stylesheet"\r\n' +
+ "\r\n" +
+ "<!-- selector {} body {display:none;} --><body>PASS</body>\r\n";
+ response.bodyOutputStream.write(r, r.length);
+ response.bodyOutputStream.flush();
+ response.finish();
+}
diff --git a/dom/html/test/bug649134/file_bug649134-2.sjs b/dom/html/test/bug649134/file_bug649134-2.sjs
new file mode 100644
index 0000000000..3cbacf7184
--- /dev/null
+++ b/dom/html/test/bug649134/file_bug649134-2.sjs
@@ -0,0 +1,12 @@
+function handleRequest(request, response) {
+ response.seizePower();
+ var r =
+ "HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/html\r\n" +
+ 'Link: < \014>; rel="stylesheet",\r\n' +
+ "\r\n" +
+ "<!-- selector {} body {display:none;} --><body>PASS</body>\r\n";
+ response.bodyOutputStream.write(r, r.length);
+ response.bodyOutputStream.flush();
+ response.finish();
+}
diff --git a/dom/html/test/bug649134/index.html b/dom/html/test/bug649134/index.html
new file mode 100644
index 0000000000..2f3973704e
--- /dev/null
+++ b/dom/html/test/bug649134/index.html
@@ -0,0 +1,3 @@
+body {
+ display:none;
+}
diff --git a/dom/html/test/chrome.toml b/dom/html/test/chrome.toml
new file mode 100644
index 0000000000..ac226b51c2
--- /dev/null
+++ b/dom/html/test/chrome.toml
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files = [
+ "file_anchor_ping.html",
+ "image.png",
+]
+
+["test_anchor_ping.html"]
+skip-if = ["os == 'android'"]
+
+["test_bug1414077.html"]
+
+["test_external_protocol_iframe.html"]
diff --git a/dom/html/test/dialog/mochitest.toml b/dom/html/test/dialog/mochitest.toml
new file mode 100644
index 0000000000..18f1f551a7
--- /dev/null
+++ b/dom/html/test/dialog/mochitest.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+["test_bug1648877_dialog_fullscreen_denied.html"]
+
diff --git a/dom/html/test/dialog/test_bug1648877_dialog_fullscreen_denied.html b/dom/html/test/dialog/test_bug1648877_dialog_fullscreen_denied.html
new file mode 100644
index 0000000000..906c7dd53e
--- /dev/null
+++ b/dom/html/test/dialog/test_bug1648877_dialog_fullscreen_denied.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1648877
+-->
+<head>
+ <title>Test for Bug 1648877</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1648877">Requesting
+ fullscreen a dialog element should be denied</a>
+<p id="display"></p>
+<dialog>
+</dialog>
+<div style="width: 30px; height:30px" </div>
+
+<pre id="test">
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ document.addEventListener("fullscreenchange", () => {
+ ok(false, "Should never receive " +
+ "a fullscreenchange event in the main window.");
+ });
+
+ document.addEventListener('fullscreenerror', (event) => {
+ ok(!document.fullscreenElement,
+ "Should not grant request if the element is dialog");
+ SimpleTest.finish();
+ });
+
+ const div = document.querySelector("div");
+
+ div.addEventListener("click", function() {
+ const dialog = document.querySelector("dialog");
+ dialog.requestFullscreen();
+ });
+
+ synthesizeMouseAtCenter(div, {});
+}
+
+SimpleTest.waitForFocus(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/dummy_page.html b/dom/html/test/dummy_page.html
new file mode 100644
index 0000000000..fd238954c6
--- /dev/null
+++ b/dom/html/test/dummy_page.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<title>Dummy test page</title>
+<meta charset="utf-8"/>
+</head>
+<body>
+<p>Dummy test page</p>
+</body>
+</html>
diff --git a/dom/html/test/empty.html b/dom/html/test/empty.html
new file mode 100644
index 0000000000..0dc101b533
--- /dev/null
+++ b/dom/html/test/empty.html
@@ -0,0 +1 @@
+<html><body></body></html>
diff --git a/dom/html/test/file.webm b/dom/html/test/file.webm
new file mode 100644
index 0000000000..7bc738b8b4
--- /dev/null
+++ b/dom/html/test/file.webm
Binary files differ
diff --git a/dom/html/test/file_anchor_ping.html b/dom/html/test/file_anchor_ping.html
new file mode 100644
index 0000000000..3b9717263f
--- /dev/null
+++ b/dom/html/test/file_anchor_ping.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>file_anchor_ping.html</title>
+ </head>
+ <body onload="document.body.firstElementChild.click()">
+ <a href="/">click me</a>
+ <script>
+ document.body.firstElementChild.ping = window.location.search.slice(1);
+ </script>
+ </body>
+</html>
diff --git a/dom/html/test/file_broadcast_load.html b/dom/html/test/file_broadcast_load.html
new file mode 100644
index 0000000000..ffae9c6536
--- /dev/null
+++ b/dom/html/test/file_broadcast_load.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<h1>file_broadcast_load.html</h1>
+<script>
+let channel = new BroadcastChannel("test");
+channel.onmessage = function(e) {
+ console.log("file_broadcast_load.html got message:", e.data);
+ if (e.data == "close") {
+ window.close();
+ }
+};
+
+addEventListener("load", function() {
+ console.log("file_broadcast_load.html loaded");
+ channel.postMessage("load");
+});
+</script>
diff --git a/dom/html/test/file_bug1108547-1.html b/dom/html/test/file_bug1108547-1.html
new file mode 100644
index 0000000000..efc0eae494
--- /dev/null
+++ b/dom/html/test/file_bug1108547-1.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+document.cookie = "foo=bar";
+</script>
diff --git a/dom/html/test/file_bug1108547-2.html b/dom/html/test/file_bug1108547-2.html
new file mode 100644
index 0000000000..f5d8c5f964
--- /dev/null
+++ b/dom/html/test/file_bug1108547-2.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<body onload="document.querySelector('form').submit();">
+<form action="javascript:opener.document.getElementById('result').textContent = document.cookie;" target="_blank" rel="opener">
+</form>
+<div id="result">not tested yet</div>
+</body>
diff --git a/dom/html/test/file_bug1108547-3.html b/dom/html/test/file_bug1108547-3.html
new file mode 100644
index 0000000000..e6a8ba3fa2
--- /dev/null
+++ b/dom/html/test/file_bug1108547-3.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<body onload="document.querySelector('a').click();">
+<a href="javascript:opener.document.getElementById('result').textContent = document.cookie;" target="_blank" rel="opener">test</a>
+<div id="result">not tested yet</div>
+</body>
diff --git a/dom/html/test/file_bug1166138_1x.png b/dom/html/test/file_bug1166138_1x.png
new file mode 100644
index 0000000000..df421453c2
--- /dev/null
+++ b/dom/html/test/file_bug1166138_1x.png
Binary files differ
diff --git a/dom/html/test/file_bug1166138_2x.png b/dom/html/test/file_bug1166138_2x.png
new file mode 100644
index 0000000000..6f76d44387
--- /dev/null
+++ b/dom/html/test/file_bug1166138_2x.png
Binary files differ
diff --git a/dom/html/test/file_bug1166138_def.png b/dom/html/test/file_bug1166138_def.png
new file mode 100644
index 0000000000..144a2f0b93
--- /dev/null
+++ b/dom/html/test/file_bug1166138_def.png
Binary files differ
diff --git a/dom/html/test/file_bug1260704.png b/dom/html/test/file_bug1260704.png
new file mode 100644
index 0000000000..df421453c2
--- /dev/null
+++ b/dom/html/test/file_bug1260704.png
Binary files differ
diff --git a/dom/html/test/file_bug209275_1.html b/dom/html/test/file_bug209275_1.html
new file mode 100644
index 0000000000..3f7233876b
--- /dev/null
+++ b/dom/html/test/file_bug209275_1.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <base href="http://example.org" />
+</head>
+<body onload="load();">
+Initial state
+
+<script>
+function load() {
+ // Nuke and rebuild the page.
+ document.removeChild(document.documentElement);
+ var html = document.createElement("html");
+ var body = document.createElement("body");
+ html.appendChild(body);
+ var link = document.createElement("a");
+ link.href = "#";
+ link.id = "link";
+ body.appendChild(link);
+ document.appendChild(html);
+
+ // Tell our parent to have a look at us.
+ parent.gGen.next();
+}
+</script>
+
+</body>
+</html>
diff --git a/dom/html/test/file_bug209275_2.html b/dom/html/test/file_bug209275_2.html
new file mode 100644
index 0000000000..36e9ff4672
--- /dev/null
+++ b/dom/html/test/file_bug209275_2.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <base href="http://example.com" />
+</head>
+<body onload="load();">
+Page 2 initial state
+
+<script>
+function load() {
+ // Nuke and rebuild the page.
+ document.removeChild(document.documentElement);
+ html = document.createElement("html");
+ html.innerHTML = "<body><a href='/' id='link'>B</a></body>"
+ document.appendChild(html);
+
+ // Tell our parent to have a look at us
+ parent.gGen.next();
+}
+</script>
+
+</body>
+</html>
diff --git a/dom/html/test/file_bug209275_3.html b/dom/html/test/file_bug209275_3.html
new file mode 100644
index 0000000000..2544115901
--- /dev/null
+++ b/dom/html/test/file_bug209275_3.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <base href="http://example.org" />
+</head>
+<body onload="load();">
+Initial state
+
+<script>
+function load() {
+ // Nuke and rebuild the page. If document.open() clears the <base> properly,
+ // our new <base> will take precedence and the test will pass.
+ document.open();
+ document.write("<html><base href='http://mochi.test:8888' /><body>" +
+ "<a id='link' href='/'>A</a></body></html>");
+
+ // Tell our parent to have a look at us.
+ parent.gGen.next();
+}
+</script>
+
+</body>
+</html>
diff --git a/dom/html/test/file_bug297761.html b/dom/html/test/file_bug297761.html
new file mode 100644
index 0000000000..5e861a00fd
--- /dev/null
+++ b/dom/html/test/file_bug297761.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <base href="http://www.mozilla.org/">
+ </head>
+ <body>
+ <form action="">
+ <input type='submit' formaction="">
+ <button type='submit' formaction=""></button>
+ <input id='i' type='image' formaction="">
+ </form>
+ </body>
+</html>
diff --git a/dom/html/test/file_bug417760.png b/dom/html/test/file_bug417760.png
new file mode 100644
index 0000000000..743292dc6f
--- /dev/null
+++ b/dom/html/test/file_bug417760.png
Binary files differ
diff --git a/dom/html/test/file_bug871161-1.html b/dom/html/test/file_bug871161-1.html
new file mode 100644
index 0000000000..16015f0c4e
--- /dev/null
+++ b/dom/html/test/file_bug871161-1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=windows-1251>
+<title>Page with non-default charset</title>
+<script>
+function run() {
+ document.forms[0].submit();
+}
+</script>
+</head>
+<body onload="run();">
+<form method=post action="http://example.org/tests/dom/html/test/file_bug871161-2.html"></form>
+</body>
+</html>
+
diff --git a/dom/html/test/file_bug871161-2.html b/dom/html/test/file_bug871161-2.html
new file mode 100644
index 0000000000..18cf825b2d
--- /dev/null
+++ b/dom/html/test/file_bug871161-2.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Page without declared charset</title>
+<script>
+function done() {
+ window.opener.postMessage(document.characterSet, "*");
+}
+</script>
+</head>
+<body onload="done();">
+</body>
+</html>
+
diff --git a/dom/html/test/file_bug893537.html b/dom/html/test/file_bug893537.html
new file mode 100644
index 0000000000..1dcb454ff1
--- /dev/null
+++ b/dom/html/test/file_bug893537.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=893537
+-->
+<body>
+<iframe id="iframe" src="data:text/html;charset=US-ASCII,Goodbye World" srcdoc="Hello World"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_cookiemanager.js b/dom/html/test/file_cookiemanager.js
new file mode 100644
index 0000000000..08c9d72898
--- /dev/null
+++ b/dom/html/test/file_cookiemanager.js
@@ -0,0 +1,20 @@
+/* eslint-env mozilla/chrome-script */
+
+addMessageListener("getCookieFromManager", ({ host, path }) => {
+ let cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ let values = [];
+ path = path.substring(0, path.lastIndexOf("/"));
+ for (let cookie of cm.cookies) {
+ if (!cookie) {
+ break;
+ }
+ if (host != cookie.host || path != cookie.path) {
+ continue;
+ }
+ values.push(cookie.name + "=" + cookie.value);
+ }
+
+ sendAsyncMessage("getCookieFromManager:return", {
+ cookie: values.join("; "),
+ });
+});
diff --git a/dom/html/test/file_formSubmission_img.jpg b/dom/html/test/file_formSubmission_img.jpg
new file mode 100644
index 0000000000..dcd99b9670
--- /dev/null
+++ b/dom/html/test/file_formSubmission_img.jpg
Binary files differ
diff --git a/dom/html/test/file_formSubmission_text.txt b/dom/html/test/file_formSubmission_text.txt
new file mode 100644
index 0000000000..a496efee84
--- /dev/null
+++ b/dom/html/test/file_formSubmission_text.txt
@@ -0,0 +1 @@
+This is a text file
diff --git a/dom/html/test/file_iframe_sandbox_a_if1.html b/dom/html/test/file_iframe_sandbox_a_if1.html
new file mode 100644
index 0000000000..b60d52ca00
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if1.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ I am sandboxed without any permissions
+ <iframe id="if_2a" src="file_iframe_sandbox_a_if2.html" height="10" width="10"></iframe>
+ <iframe id="if_2b" sandbox="allow-scripts" src="file_iframe_sandbox_a_if2.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_a_if10.html b/dom/html/test/file_iframe_sandbox_a_if10.html
new file mode 100644
index 0000000000..14306eb613
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if10.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<frameset>
+ <frame src="file_iframe_sandbox_a_if11.html">
+ <frame src="file_iframe_sandbox_a_if16.html">
+</frameset>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_a_if11.html b/dom/html/test/file_iframe_sandbox_a_if11.html
new file mode 100644
index 0000000000..8eee71df1d
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if11.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script>
+ function doStuff() {
+ try {
+ window.parent.parent.ok_wrapper(false, "a frame inside a sandboxed iframe should NOT be same origin with the iframe's parent");
+ }
+ catch (e) {
+ window.parent.parent.postMessage({ok: true, desc: "a frame inside a sandboxed iframe is not same origin with the iframe's parent"}, "*");
+ }
+ }
+ </script>
+</head>
+<frameset>
+ <frame onload='doStuff()' src="file_iframe_sandbox_a_if12.html">
+</frameset>
+I'm a &lt;frame&gt; inside an iframe which is sandboxed with 'allow-scripts allow-forms'
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if12.html b/dom/html/test/file_iframe_sandbox_a_if12.html
new file mode 100644
index 0000000000..d49d4e5625
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if12.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+function doStuff() {
+ try {
+ window.parent.parent.parent.ok_wrapper(false, "a frame inside a frame inside a sandboxed iframe should NOT be same origin with the iframe's parent");
+ }
+ catch (e) {
+ dump("caught some e if12\n");
+ window.parent.parent.parent.postMessage({ok: true, desc: "a frame inside a frame inside a sandboxed iframe is not same origin with the iframe's parent"}, "*");
+ }
+}
+</script>
+<body onload='doStuff()'>
+ I'm a &lt;frame&gt; inside a &lt;frame&gt; inside an iframe which is sandboxed with 'allow-scripts allow-forms'
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if13.html b/dom/html/test/file_iframe_sandbox_a_if13.html
new file mode 100644
index 0000000000..8737a7682e
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if13.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 886262</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+ <object data="file_iframe_sandbox_a_if14.html"></object>
+</body>
+
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_a_if14.html b/dom/html/test/file_iframe_sandbox_a_if14.html
new file mode 100644
index 0000000000..b588f7ec50
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if14.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 886262</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ window.addEventListener("message", receiveMessage);
+
+ function receiveMessage(event)
+ {
+ window.parent.parent.postMessage({ok: event.data.ok, desc: "objects containing " + event.data.desc}, "*");
+ }
+
+ function doStuff() {
+ try {
+ window.parent.parent.ok_wrapper(false, "an object inside a sandboxed iframe should NOT be same origin with the iframe's parent");
+ }
+ catch (e) {
+ window.parent.parent.postMessage({ok: true, desc: "an object inside a sandboxed iframe is not same origin with the iframe's parent"}, "*");
+ }
+ }
+</script>
+
+<body onload='doStuff()'>
+I'm a &lt;object&gt; inside an iframe which is sandboxed with 'allow-scripts allow-forms'
+
+ <object data="file_iframe_sandbox_a_if15.html"></object>
+</body>
+
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if15.html b/dom/html/test/file_iframe_sandbox_a_if15.html
new file mode 100644
index 0000000000..9c5a003d7c
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if15.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 886262</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+function doStuff() {
+ try {
+ window.parent.parent.parent.ok_wrapper(false, "an object inside a frame or object inside a sandboxed iframe should NOT be same origin with the iframe's parent");
+ }
+ catch (e) {
+ window.parent.parent.parent.postMessage({ok: true, desc: "an object inside a frame or object inside a sandboxed iframe is not same origin with the iframe's parent"}, "*");
+ }
+
+ // Check that sandboxed forms browsing context flag NOT set by attempting to submit a form.
+ document.getElementById('a_form').submit();
+}
+</script>
+
+<body onload='doStuff()'>
+ I'm a &lt;object&gt; inside a &lt;frame&gt; or &lt;object&gt; inside an iframe which is sandboxed with 'allow-scripts allow-forms'
+
+ <form method="get" action="file_iframe_sandbox_form_pass.html" id="a_form">
+ First name: <input type="text" name="firstname">
+ Last name: <input type="text" name="lastname">
+ <input type="submit" id="a_button">
+ </form>
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if16.html b/dom/html/test/file_iframe_sandbox_a_if16.html
new file mode 100644
index 0000000000..141d3c2b06
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if16.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 886262</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ window.addEventListener("message", receiveMessage);
+
+ function receiveMessage(event)
+ {
+ window.parent.parent.postMessage({ok: event.data.ok, desc: "objects containing " + event.data.desc}, "*");
+ }
+</script>
+
+<body>
+I'm a &lt;frame&gt; inside an iframe which is sandboxed with 'allow-scripts allow-forms'
+
+ <object data="file_iframe_sandbox_a_if15.html"></object>
+</body>
+
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if17.html b/dom/html/test/file_iframe_sandbox_a_if17.html
new file mode 100644
index 0000000000..a736924bf5
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if17.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 886262</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ function doTest() {
+ var if_18_19 = document.getElementById('if_18_19');
+ if_18_19.sandbox = "allow-scripts allow-same-origin";
+ if_18_19.contentWindow.postMessage("go", "*");
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed but with "allow-scripts". I change the sandbox flags on if_18_19 to
+ "allow-scripts allow-same-origin" then get it to re-navigate itself to
+ file_iframe_sandbox_a_if18.html, which attemps to call a function in my parent.
+ This should fail since my sandbox flags should be copied to it when the sandbox
+ flags are changed.
+
+ <iframe sandbox="allow-scripts" id="if_18_19" src="file_iframe_sandbox_a_if19.html" height="10" width="10"></iframe>
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if18.html b/dom/html/test/file_iframe_sandbox_a_if18.html
new file mode 100644
index 0000000000..bbe90970d4
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if18.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 886262</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ function doTest() {
+ try {
+ window.parent.parent.ok_wrapper(false, "an iframe in an iframe SHOULD copy its parent's sandbox flags when its sandbox flags are changed");
+ }
+ catch (e) {
+ window.parent.parent.postMessage({ok: true, desc: "an iframe in an iframe copies its parent's sandbox flags when its sandbox flags are changed"}, "*");
+ }
+ }
+</script>
+
+<body onload="doTest()">
+ I'm an iframe whose sandbox flags have been changed to include allow-same-origin.
+ I should not be able to call a function in my parent's parent because my parent's
+ iframe does not have allow-same-origin set.
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if19.html b/dom/html/test/file_iframe_sandbox_a_if19.html
new file mode 100644
index 0000000000..e4d3d68887
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if19.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 886262</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<script>
+ window.addEventListener("message", function(e){
+ window.open("file_iframe_sandbox_a_if18.html", "_self");
+ });
+</script>
+
+<body>
+ I'm just here to navigate to file_iframe_sandbox_a_if18.html after my owning
+ iframe has had allow-same-origin added.
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if2.html b/dom/html/test/file_iframe_sandbox_a_if2.html
new file mode 100644
index 0000000000..72bde69e41
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if2.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="text/javascript">
+function doStuff() {
+ // should NOT be able to execute scripts
+ window.parent.parent.postMessage({ok: false, desc: "a document within an iframe sandboxed with sandbox='' should NOT be able to execute scripts"}, "*");
+}
+</script>
+
+<body onLoad="doStuff()">
+ I am NOT sandboxed or am sandboxed with "allow-scripts" but am contained within an iframe sandboxed with sandbox = ""
+ or am sandboxed with sandbox='' inside an iframe sandboxed with "allow-scripts"
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if3.html b/dom/html/test/file_iframe_sandbox_a_if3.html
new file mode 100644
index 0000000000..899c2f1093
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if3.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<script type="text/javascript">
+ function ok_wrapper(condition, msg) {
+ window.parent.ok_wrapper(condition, msg);
+ }
+</script>
+
+<body>
+ I am sandboxed but with "allow-scripts"
+
+ <iframe id='if_4' src='file_iframe_sandbox_a_if4.html' height="10" width="10"></iframe>
+ <iframe id='if_7' src='file_iframe_sandbox_a_if7.html' height="10" width="10"></iframe>
+ <iframe id='if_2' sandbox='' src='file_iframe_sandbox_a_if2.html' height="10" width="10"></iframe>
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if4.html b/dom/html/test/file_iframe_sandbox_a_if4.html
new file mode 100644
index 0000000000..a216fb572a
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if4.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<script type="text/javascript">
+function doStuff() {
+ try {
+ window.parent.ok_wrapper(false, "a document contained within a sandboxed document without 'allow-same-origin' should NOT be same domain with its parent");
+ } catch(e) {
+ window.parent.parent.postMessage({type: "ok", ok: true, desc: "a document contained within a sandboxed document without 'allow-same-origin' should NOT be same domain with its parent"}, "*");
+ }
+
+ try {
+ window.parent.parent.ok_wrapper(false, "a document contained within a sandboxed document without 'allow-same-origin' should NOT be same domain with the top level");
+ } catch(e) {
+ window.parent.parent.postMessage({type: "ok", ok: true, desc: "a document contained within a sandboxed document without 'allow-same-origin' should NOT be same domain with the top level"}, "*");
+ }
+}
+</script>
+
+<body onLoad="doStuff()">
+ I am not sandboxed but contained within a sandboxed document with 'allow-scripts'
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if5.html b/dom/html/test/file_iframe_sandbox_a_if5.html
new file mode 100644
index 0000000000..c1081c5039
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if5.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<script type="text/javascript">
+ function ok_wrapper(result, desc) {
+ window.parent.ok_wrapper(result, desc);
+ }
+</script>
+
+<body>
+ I am sandboxed but with "allow-scripts allow-same-origin"
+
+ <iframe sandbox='allow-scripts allow-same-origin' id='if_6' src='file_iframe_sandbox_a_if6.html' height="10" width="10"></iframe>
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if6.html b/dom/html/test/file_iframe_sandbox_a_if6.html
new file mode 100644
index 0000000000..62a7114316
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if6.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<script type="text/javascript">
+function doStuff() {
+ window.parent.ok_wrapper(true, "a document sandboxed with 'allow-same-origin' and contained within a sandboxed document with 'allow-same-origin' should be same domain with its parent");
+ window.parent.parent.ok_wrapper(true, "a document sandboxed with 'allow-same-origin' contained within a sandboxed document with 'allow-same-origin' should be same domain with the top level");
+}
+</script>
+
+<body onLoad="doStuff()">
+ I am sandboxed with 'allow-scripts allow-same-origin' and contained within a sandboxed document with 'allow-scripts allow-same-origin'
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if7.html b/dom/html/test/file_iframe_sandbox_a_if7.html
new file mode 100644
index 0000000000..6480eebdba
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if7.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="text/javascript">
+function doStuff() {
+ // should be able to execute scripts
+ window.parent.parent.postMessage({ok: true, desc: "a document contained within an iframe contained within an iframe sandboxed with 'allow-scripts' should be able to execute scripts"}, "*");
+}
+</script>
+
+<body onLoad="doStuff()">
+ I am NOT sandboxed but am contained within an iframe contained within an iframe sandboxed with sandbox = "allow-scripts"
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if8.html b/dom/html/test/file_iframe_sandbox_a_if8.html
new file mode 100644
index 0000000000..87748f542a
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if8.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script>
+function doSubload() {
+ var if_9 = document.getElementById('if_9');
+ if_9.src = 'file_iframe_sandbox_a_if9.html';
+}
+
+window.doSubload = doSubload;
+
+</script>
+<body>
+ I am sandboxed but with "allow-scripts allow-same-origin". After my initial load, "allow-same-origin" is removed
+ and then I load file_iframe_sandbox_a_if9.html, which attemps to call a function in window.top. This should
+ succeed since the new sandbox flags shouldn't have taken affect on me until I'm reloaded.
+
+ <iframe id='if_9' src='about:blank' height="10" width="10"></iframe>
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_a_if9.html b/dom/html/test/file_iframe_sandbox_a_if9.html
new file mode 100644
index 0000000000..da2bcf1fa3
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_a_if9.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+function doStuff() {
+ window.parent.parent.ok_wrapper(true, "a subloaded document should inherit the flags of the document, not of the docshell/sandbox attribute");
+}
+</script>
+<body onload='doStuff()'>
+ I'm a subloaded document of file_iframe_sandbox_a_if8.html. I should be able to call a function in window.top
+ because I should be same-origin with it.
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_b_if1.html b/dom/html/test/file_iframe_sandbox_b_if1.html
new file mode 100644
index 0000000000..a65cbec6b9
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_b_if1.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ I am sandboxed without any permissions
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_b_if2.html b/dom/html/test/file_iframe_sandbox_b_if2.html
new file mode 100644
index 0000000000..08e7453574
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_b_if2.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+ function ok(condition, msg) {
+ window.parent.ok_wrapper(condition, msg);
+ }
+
+ function testXHR() {
+ var xhr = new XMLHttpRequest();
+
+ xhr.open("GET", "file_iframe_sandbox_b_if1.html");
+
+ xhr.onreadystatechange = function (oEvent) {
+ var result = false;
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ result = true;
+ }
+ ok(result, "XHR should work normally in an iframe sandboxed with 'allow-same-origin'");
+ }
+ }
+
+ xhr.send(null);
+ }
+
+ function doStuff() {
+ ok(true, "documents sandboxed with 'allow-same-origin' should be able to access their parent");
+
+ // should be able to access document.cookie since we have 'allow-same-origin'
+ ok(document.cookie == "", "a document sandboxed with allow-same-origin should be able to access document.cookie");
+
+ // should be able to access localStorage since we have 'allow-same-origin'
+ ok(window.localStorage, "a document sandboxed with allow-same-origin should be able to access localStorage");
+
+ // should be able to access sessionStorage since we have 'allow-same-origin'
+ ok(window.sessionStorage, "a document sandboxed with allow-same-origin should be able to access sessionStorage");
+
+ testXHR();
+ }
+</script>
+<body onLoad="doStuff()">
+ I am sandboxed but with "allow-same-origin" and "allow-scripts"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_b_if3.html b/dom/html/test/file_iframe_sandbox_b_if3.html
new file mode 100644
index 0000000000..350e2ac472
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_b_if3.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+ function ok(result, message) {
+ window.parent.postMessage({ok: result, desc: message}, "*");
+ }
+
+ function testXHR() {
+ // Standard URL should be blocked as we have a unique origin.
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "file_iframe_sandbox_b_if1.html");
+ xhr.onreadystatechange = function (oEvent) {
+ var result = false;
+ if (xhr.readyState == 4) {
+ if (xhr.status == 0) {
+ result = true;
+ }
+ ok(result, "XHR should be blocked in an iframe sandboxed WITHOUT 'allow-same-origin'");
+ }
+ }
+ xhr.send(null);
+
+ // Blob URL should work as it will have our unique origin.
+ var blobXhr = new XMLHttpRequest();
+ var blobUrl = URL.createObjectURL(new Blob(["wibble"], {type: "text/plain"}));
+ blobXhr.open("GET", blobUrl);
+ blobXhr.onreadystatechange = function () {
+ if (this.readyState == 4) {
+ ok(this.status == 200 && this.response == "wibble", "XHR for a blob URL created in this document should NOT be blocked in an iframe sandboxed WITHOUT 'allow-same-origin'");
+ }
+ }
+ try {
+ blobXhr.send();
+ } catch(e) {
+ ok(false, "failed to send XHR for blob URL: error: " + e);
+ }
+
+ // Data URL should work as it inherits the loader's origin.
+ var dataXhr = new XMLHttpRequest();
+ dataXhr.open("GET", "data:text/html,wibble");
+ dataXhr.onreadystatechange = function () {
+ if (this.readyState == 4) {
+ ok(this.status == 200 && this.response == "wibble", "XHR for a data URL should NOT be blocked in an iframe sandboxed WITHOUT 'allow-same-origin'");
+ }
+ }
+ try {
+ dataXhr.send();
+ } catch(e) {
+ ok(false, "failed to send XHR for data URL: error: " + e);
+ }
+ }
+
+ function doStuff() {
+ try {
+ window.parent.ok(false, "documents sandboxed without 'allow-same-origin' should NOT be able to access their parent");
+ } catch (error) {
+ ok(true, "documents sandboxed without 'allow-same-origin' should NOT be able to access their parent");
+ }
+
+ // should NOT be able to access document.cookie
+ try {
+ var foo = document.cookie;
+ } catch(error) {
+ ok(true, "a document sandboxed without allow-same-origin should NOT be able to access document.cookie");
+ }
+
+ // should NOT be able to access localStorage
+ try {
+ var foo = window.localStorage;
+ } catch(error) {
+ ok(true, "a document sandboxed without allow-same-origin should NOT be able to access localStorage");
+ }
+
+ // should NOT be able to access sessionStorage
+ try {
+ var foo = window.sessionStorage;
+ } catch(error) {
+ ok(true, "a document sandboxed without allow-same-origin should NOT be able to access sessionStorage");
+ }
+
+ testXHR();
+ }
+</script>
+<body onLoad="doStuff()">
+ I am sandboxed but with "allow-scripts"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if1.html b/dom/html/test/file_iframe_sandbox_c_if1.html
new file mode 100644
index 0000000000..c2fbf136ae
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_c_if1.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts");
+
+ document.getElementById('a_form').submit();
+
+ // trigger the javascript: url test
+ sendMouseEvent({type:'click'}, 'a_link');
+ }
+</script>
+<script src='file_iframe_sandbox_pass.js'></script>
+<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'>
+ I am sandboxed but with "allow-scripts"
+
+ <form method="get" action="file_iframe_sandbox_form_fail.html" id="a_form">
+ First name: <input type="text" name="firstname">
+ Last name: <input type="text" name="lastname">
+ <input type="submit" onclick="doSubmit()" id="a_button">
+ </form>
+
+ <a href = 'javascript:ok(true, "documents sandboxed with allow-scripts should be able to run script from javascript: URLs");' id='a_link'>click me</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if2.html b/dom/html/test/file_iframe_sandbox_c_if2.html
new file mode 100644
index 0000000000..1ea8a90ca3
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_c_if2.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(false, "documents sandboxed without allow-scripts should NOT be able to run inline scripts");
+ }
+</script>
+<script src='file_iframe_sandbox_fail.js'></script>
+<body onLoad='window.parent.postmessage({ok: false, desc: "documents sandboxed without allow-scripts should NOT be able to run script from event handlers"}, "*");doStuff();'>
+ I am sandboxed with no permissions
+ <img src="about:blank" onerror='ok(false, "documents sandboxed without allow-scripts should NOT be able to run script from event handlers");')>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if3.html b/dom/html/test/file_iframe_sandbox_c_if3.html
new file mode 100644
index 0000000000..fdf98d93d4
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_c_if3.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+</head>
+<script type="text/javascript">
+ function doStuff() {
+ dump("*** c_if3 has loaded\n");
+ // try and submit the form - this should succeed
+ document.getElementById('a_form').submit();
+ }
+</script>
+<body onLoad="doStuff()">
+ I am sandboxed but with "allow-scripts allow-forms"
+
+ <form method="get" action="file_iframe_sandbox_form_pass.html" id="a_form">
+ First name: <input type="text" name="firstname">
+ Last name: <input type="text" name="lastname">
+ <input type="submit" onclick="doSubmit()" id="a_button">
+ </form>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if4.html b/dom/html/test/file_iframe_sandbox_c_if4.html
new file mode 100644
index 0000000000..ee2438f28a
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_c_if4.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.ok_wrapper(result, desc);
+ }
+
+ function doStuff() {
+ // try to open a new window via target="_blank", target="BC341604", and window.open()
+ // the window we try to open closes itself once it opens
+ sendMouseEvent({type:'click'}, 'target_blank');
+ sendMouseEvent({type:'click'}, 'target_BC341604');
+
+ var threw = false;
+ try {
+ window.open("about:blank");
+ } catch (error) {
+ threw = true;
+ }
+
+ ok(threw, "window.open threw a JS exception and was not allowed");
+ }
+</script>
+<body onLoad="doStuff()">
+ I am sandboxed but with "allow-scripts allow-same-origin"
+
+ <a href="file_iframe_sandbox_open_window_fail.html" target="_blank" id="target_blank" rel="opener">open window</a>
+ <a href="file_iframe_sandbox_open_window_fail.html" target="BC341604" id="target_BC341604">open window</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if5.html b/dom/html/test/file_iframe_sandbox_c_if5.html
new file mode 100644
index 0000000000..bd368de425
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_c_if5.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.ok_wrapper(result, desc);
+ }
+</script>
+<body onLoad="doStuff()">
+ I am sandboxed but with "allow-same-origin"
+
+ <a href = 'javascript:ok(false, "documents sandboxed without allow-scripts should not be able to run script with javascript: URLs");' id='a_link'>click me</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if6.html b/dom/html/test/file_iframe_sandbox_c_if6.html
new file mode 100644
index 0000000000..e5ecf3051e
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_c_if6.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.ok_wrapper(result, desc);
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "a document sandboxed with allow-same-origin and allow-scripts should be same origin with its parent and able to run scripts " +
+ "regardless of what kind of whitespace was used in its sandbox attribute");
+ }
+</script>
+<body onLoad="doStuff()">
+ I am sandboxed but with "allow-same-origin" and "allow-scripts"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if7.html b/dom/html/test/file_iframe_sandbox_c_if7.html
new file mode 100644
index 0000000000..b9a55def6f
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_c_if7.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ try {
+ var thing = indexedDB.open("sandbox");
+ ok(false, "documents sandboxed without allow-same-origin should NOT be able to access indexedDB");
+ }
+
+ catch(e) {
+ ok(true, "documents sandboxed without allow-same-origin should NOT be able to access indexedDB");
+ }
+ }
+</script>
+<body onLoad='doStuff();'>
+ I am sandboxed but with "allow-scripts"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if8.html b/dom/html/test/file_iframe_sandbox_c_if8.html
new file mode 100644
index 0000000000..d8b8948466
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_c_if8.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ var thing = indexedDB.open("sandbox");
+
+ thing.onerror = function(event) {
+ ok(false, "documents sandboxed with allow-same-origin SHOULD be able to access indexedDB");
+ };
+ thing.onsuccess = function(event) {
+ ok(true, "documents sandboxed with allow-same-origin SHOULD be able to access indexedDB");
+ };
+ }
+</script>
+<body onLoad='doStuff();'>
+ I am sandboxed but with "allow-scripts allow-same-origin"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if9.html b/dom/html/test/file_iframe_sandbox_c_if9.html
new file mode 100644
index 0000000000..0c88a677cb
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_c_if9.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 671389</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ I am
+ <ul>
+ <li>sandboxed but with "allow-forms", "allow-pointer-lock", "allow-popups", "allow-same-origin", "allow-scripts", and "allow-top-navigation", </li>
+ <li>sandboxed but with "allow-same-origin", "allow-scripts", </li>
+ <li>sandboxed, or </li>
+ <li>not sandboxed.</li>
+ </ul>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_close.html b/dom/html/test/file_iframe_sandbox_close.html
new file mode 100644
index 0000000000..3b87534978
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_close.html
@@ -0,0 +1,3 @@
+<script>
+ self.close();
+</script>
diff --git a/dom/html/test/file_iframe_sandbox_d_if1.html b/dom/html/test/file_iframe_sandbox_d_if1.html
new file mode 100644
index 0000000000..744594e813
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if1.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+function doTest() {
+ sendMouseEvent({type:'click'}, 'anchor');
+}
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+
+ <a href="file_iframe_sandbox_navigation_pass.html?Test 1:%20" target="_self" id='anchor'>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if10.html b/dom/html/test/file_iframe_sandbox_d_if10.html
new file mode 100644
index 0000000000..41fb46b586
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if10.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+function doTest() {
+ window.parent.postMessage({type: "if_10"}, "*");
+}
+</script>
+<body onload='doTest()'>
+ I am sandboxed with 'allow-scripts'
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if11.html b/dom/html/test/file_iframe_sandbox_d_if11.html
new file mode 100644
index 0000000000..63880587f5
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if11.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+
+function navigateAway() {
+ document.getElementById("anchor").click();
+}
+
+function doTest() {
+ try {
+ // this should fail the first time, but work the second
+ window.parent.ok_wrapper(true, "a document that was loaded, navigated to another document, had 'allow-same-origin' added and then was" +
+ " navigated back should be same-origin with its parent");
+ } catch (e) {
+ navigateAway();
+ }
+}
+
+</script>
+<body onload='doTest()'>
+ I am sandboxed with 'allow-scripts'
+ <a href='file_iframe_sandbox_d_if12.html' id='anchor'>CLICK ME</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if12.html b/dom/html/test/file_iframe_sandbox_d_if12.html
new file mode 100644
index 0000000000..0d7936512e
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if12.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+function doTest() {
+ window.parent.postMessage({test:'if_11'}, "*");
+}
+</script>
+<body onload='doTest()'>
+ I am sandboxed with 'allow-scripts'
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if13.html b/dom/html/test/file_iframe_sandbox_d_if13.html
new file mode 100644
index 0000000000..aad330c33c
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if13.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+ // this message is part of if_11's test
+ if (event.data.test == 'if_11') {
+ doIf11TestPart2();
+ }
+}
+
+function ok_wrapper(result, msg) {
+ window.opener.postMessage({ok: result, desc: msg}, "*");
+ window.close();
+}
+
+function doIf11TestPart2() {
+ var if_11 = document.getElementById('if_11');
+ if_11.sandbox = 'allow-scripts allow-same-origin';
+ // window.history is no longer cross-origin accessible in gecko.
+ SpecialPowers.wrap(if_11).contentWindow.history.back();
+}
+</script>
+<body>
+ <iframe sandbox='allow-scripts' id="if_11" src="file_iframe_sandbox_d_if11.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if14.html b/dom/html/test/file_iframe_sandbox_d_if14.html
new file mode 100644
index 0000000000..237a9d704f
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if14.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 838692</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="text/javascript">
+ var test20Context = "Test 20: Navigate another window (not opened by us): ";
+
+ function doTest() {
+ // Try to navigate auxiliary browsing context (window) not opened by us.
+ // We should not be able to do this as we are sandboxed.
+ sendMouseEvent({type:'click'}, 'navigate_window');
+ window.parent.postMessage({type: "attempted"}, "*");
+
+ // Try to navigate auxiliary browsing context (window) not opened by us, using window.open().
+ // We should not be able to do this as we are sandboxed.
+ try {
+ window.open("file_iframe_sandbox_window_navigation_fail.html?" + escape(test20Context), "window_to_navigate2");
+ window.parent.postMessage({type: "attempted"}, "*");
+ } catch(error) {
+ window.parent.postMessage({ok: true, desc: test20Context + "as expected, error thrown during window.open(..., \"window_to_navigate2\")"}, "*");
+ }
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed but with "allow-scripts allow-same-origin allow-top-navigation".
+
+ <a href="file_iframe_sandbox_window_navigation_fail.html?Test 14: Navigate another window (not opened by us):%20" target="window_to_navigate" id="navigate_window">navigate window</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if15.html b/dom/html/test/file_iframe_sandbox_d_if15.html
new file mode 100644
index 0000000000..6c969c8fe1
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if15.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+ I am an unsandboxed iframe.
+
+ <iframe sandbox="allow-same-origin allow-scripts" id="if_16" src="file_iframe_sandbox_d_if16.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if16.html b/dom/html/test/file_iframe_sandbox_d_if16.html
new file mode 100644
index 0000000000..e50dd97ea0
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if16.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<script type="application/javascript">
+function doTest() {
+ window.parent.parent.postMessage({type: "attempted"}, "*");
+ sendMouseEvent({type:'click'}, 'anchor');
+}
+</script>
+
+<body onload="doTest()">
+ I am sandboxed with 'allow-same-origin allow-scripts'
+
+ <a href="file_iframe_sandbox_navigation_fail.html?Test 16: Navigate parent/ancestor by name:%20" target='if_parent' id='anchor'>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if17.html b/dom/html/test/file_iframe_sandbox_d_if17.html
new file mode 100644
index 0000000000..047a08137d
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if17.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="application/javascript">
+ var testContext = "Test 17: navigate _self with window.open(): ";
+
+ function doTest() {
+ try {
+ window.open("file_iframe_sandbox_navigation_pass.html?" + escape(testContext), "_self");
+ } catch(error) {
+ window.parent.postMessage({ok: false, desc: testContext + "error thrown during window.open(..., \"_self\")"}, "*");
+ }
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if18.html b/dom/html/test/file_iframe_sandbox_d_if18.html
new file mode 100644
index 0000000000..fdcb4198f4
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if18.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<script type="application/javascript">
+ window.addEventListener("message", receiveMessage);
+
+ function receiveMessage(event) {
+ window.parent.postMessage(event.data, "*");
+ }
+
+ var testContext = "Test 18: navigate child with window.open(): ";
+
+ function doTest() {
+ try {
+ window.open("file_iframe_sandbox_navigation_pass.html?" + escape(testContext), "foo");
+ } catch(error) {
+ window.parent.postMessage({ok: false, desc: testContext + " error thrown during window.open(..., \"foo\")"}, "*");
+ }
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+
+ <iframe name="foo" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if19.html b/dom/html/test/file_iframe_sandbox_d_if19.html
new file mode 100644
index 0000000000..d766d26492
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if19.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ I am sandboxed with 'allow-scripts'
+
+ <iframe sandbox="allow-scripts" id="if_20" src="file_iframe_sandbox_d_if20.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if2.html b/dom/html/test/file_iframe_sandbox_d_if2.html
new file mode 100644
index 0000000000..b45cb975ca
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if2.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+// needed to forward the message to the main test page
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+ window.parent.postMessage(event.data, "*");
+}
+
+function doTest() {
+ sendMouseEvent({type:'click'}, 'anchor');
+}
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+
+ <iframe name="foo" src="file_iframe_sandbox_navigation_start.html" height="10" width="10"></iframe>
+
+ <a href="file_iframe_sandbox_navigation_pass.html?Test 2:%20" target='foo' id='anchor'>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if20.html b/dom/html/test/file_iframe_sandbox_d_if20.html
new file mode 100644
index 0000000000..005c4bc823
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if20.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="application/javascript">
+ var testContext = "Test 19: navigate _parent with window.open(): ";
+
+ function doTest() {
+ try {
+ window.open("file_iframe_sandbox_navigation_fail.html?" + escape(testContext), "_parent");
+ window.parent.parent.postMessage({type: "attempted"}, "*");
+ } catch(error) {
+ window.parent.parent.postMessage({ok: true, desc: testContext + "as expected, error thrown during window.open(..., \"_parent\")"}, "*");
+ }
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if21.html b/dom/html/test/file_iframe_sandbox_d_if21.html
new file mode 100644
index 0000000000..6d0ab232e0
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if21.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+ I am an unsandboxed iframe.
+
+ <iframe sandbox="allow-same-origin allow-scripts" id="if_22" src="file_iframe_sandbox_d_if22.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if22.html b/dom/html/test/file_iframe_sandbox_d_if22.html
new file mode 100644
index 0000000000..bd27157926
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if22.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="application/javascript">
+ var testContext = "Test 21: navigate parent by name with window.open(): ";
+
+ function doTest() {
+ try {
+ window.open("file_iframe_sandbox_navigation_fail.html?" + escape(testContext), "if_parent2");
+ window.parent.parent.postMessage({type: "attempted"}, "*");
+ } catch(error) {
+ window.parent.parent.postMessage({ok: true, desc: testContext + "as expected, error thrown during window.open(..., \"if_parent2\")"}, "*");
+ }
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed with 'allow-same-origin allow-scripts'
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if23.html b/dom/html/test/file_iframe_sandbox_d_if23.html
new file mode 100644
index 0000000000..e755511e37
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if23.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="application/javascript">
+ var test27Context = "Test 27: navigate opened window by name with anchor: ";
+ var test28Context = "Test 28: navigate opened window by name with window.open(): ";
+
+ var windowsToClose = new Array();
+
+ function closeWindows() {
+ for (var i = 0; i < windowsToClose.length; i++) {
+ windowsToClose[i].close();
+ }
+ }
+
+ // Add message listener to forward messages on to parent
+ window.addEventListener("message", receiveMessage);
+
+ function receiveMessage(event) {
+ switch (event.data.type) {
+ case "closeWindows":
+ closeWindows();
+ break;
+ default:
+ window.parent.postMessage(event.data, "*");
+ }
+ }
+
+ function doTest() {
+ try {
+ windowsToClose.push(window.open("about:blank", "test27window"));
+ var test27Anchor = document.getElementById("test27Anchor");
+ test27Anchor.href = "file_iframe_sandbox_window_navigation_pass.html?" + escape(test27Context);
+ sendMouseEvent({type:"click"}, "test27Anchor");
+ window.parent.postMessage({type: "attempted"}, "*");
+ } catch(error) {
+ window.parent.postMessage({ok: false, desc: test27Context + "error thrown during window.open(): " + error}, "*");
+ }
+
+ try {
+ windowsToClose.push(window.open("about:blank", "test28window"));
+ window.open("file_iframe_sandbox_window_navigation_pass.html?" + escape(test28Context), "test28window");
+ window.parent.postMessage({type: "attempted"}, "*");
+ } catch(error) {
+ window.parent.postMessage({ok: false, desc: test28Context + "error thrown during window.open(): " + error}, "*");
+ }
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts allow-popups'
+
+ <a id="test27Anchor" target="test27window">Test 27 anchor</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if3.html b/dom/html/test/file_iframe_sandbox_d_if3.html
new file mode 100644
index 0000000000..cd2d53bce9
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if3.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ I am sandboxed with 'allow-scripts'
+
+ <iframe sandbox="allow-scripts" id="if_4" src="file_iframe_sandbox_d_if4.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if4.html b/dom/html/test/file_iframe_sandbox_d_if4.html
new file mode 100644
index 0000000000..c11a414551
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if4.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+function doTest() {
+ window.parent.parent.postMessage({type: "attempted"}, "*");
+ sendMouseEvent({type:'click'}, 'anchor');
+}
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+
+ <a href="file_iframe_sandbox_navigation_fail.html" target='_parent' id='anchor'>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if5.html b/dom/html/test/file_iframe_sandbox_d_if5.html
new file mode 100644
index 0000000000..d8fe4289af
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if5.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+function doTest() {
+ window.parent.postMessage({type: "attempted"}, "*");
+ sendMouseEvent({type:'click'}, 'anchor');
+}
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts allow-same-origin'
+
+ <a href="file_iframe_sandbox_navigation_fail.html?Test 4: Navigate sibling iframe by name:%20" target='if_sibling' id='anchor'>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if6.html b/dom/html/test/file_iframe_sandbox_d_if6.html
new file mode 100644
index 0000000000..9bb48cbb20
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if6.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+function doTest() {
+ sendMouseEvent({type:'click'}, 'anchor');
+}
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+
+ <a href="file_iframe_sandbox_d_if7.html" target='_self' id='anchor'>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if7.html b/dom/html/test/file_iframe_sandbox_d_if7.html
new file mode 100644
index 0000000000..5023ee0294
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if7.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+function doTest() {
+ try {
+ window.parent.ok_wrapper(false, "a sandboxed document when navigated should still NOT be same-origin with its parent");
+ } catch(error) {
+ window.parent.postMessage({ok: true, desc: "sandboxed document's attempt to access parent after navigation blocked, as not same-origin."}, "*");
+ }
+}
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if8.html b/dom/html/test/file_iframe_sandbox_d_if8.html
new file mode 100644
index 0000000000..2b4398ef00
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if8.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="application/javascript">
+ function doTest() {
+ window.parent.modify_if_8();
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts' and 'allow-same-origin' the first time I am loaded, and with 'allow-scripts' the second time
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_d_if9.html b/dom/html/test/file_iframe_sandbox_d_if9.html
new file mode 100644
index 0000000000..ee641904fc
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_d_if9.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+function doTest() {
+ window.parent.modify_if_9();
+}
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts' and 'allow-same-origin' the first time I am loaded, and with 'allow-same-origin' the second time
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_e_if1.html b/dom/html/test/file_iframe_sandbox_e_if1.html
new file mode 100644
index 0000000000..e3882dfb28
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if1.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+
+<script>
+ function doTest() {
+ var testContext = location.search == "" ? "?Test 10: Navigate _top:%20" : location.search;
+ document.getElementById("if_6").src = "file_iframe_sandbox_e_if6.html" + testContext;
+ }
+</script>
+
+<body onload="doTest()">
+ <iframe sandbox='allow-scripts' id='if_6' height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if10.html b/dom/html/test/file_iframe_sandbox_e_if10.html
new file mode 100644
index 0000000000..2484b8f342
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if10.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ function doTest() {
+ var testContext = "?Test 23: Nested navigate _top with window.open():%20";
+ document.getElementById("if_9").src = "file_iframe_sandbox_e_if9.html" + testContext;
+ }
+</script>
+
+<body onload="doTest()">
+ <iframe sandbox='allow-scripts allow-top-navigation' id='if_9' height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if11.html b/dom/html/test/file_iframe_sandbox_e_if11.html
new file mode 100644
index 0000000000..106c4c629b
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if11.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+ function doTest() {
+ var testContext = location.search.substring(1);
+ try {
+ window.open("file_iframe_sandbox_top_navigation_pass.html?" + testContext, "_top");
+ } catch(error) {
+ window.top.opener.postMessage({ok: false, desc: unescape(testContext) + "error thrown during window.open(..., \"_top\")"}, "*");
+ window.top.close();
+ }
+ }
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts and allow-top-navigation'
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if12.html b/dom/html/test/file_iframe_sandbox_e_if12.html
new file mode 100644
index 0000000000..0b1b87e09b
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if12.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ function doTest() {
+ var testContext = location.search == "" ? "?Test 24: Navigate _top with window.open():%20" : location.search;
+ document.getElementById("if_14").src = "file_iframe_sandbox_e_if14.html" + testContext;
+ }
+</script>
+
+<body onload="doTest()">
+ <iframe sandbox='allow-scripts' id='if_14' height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if13.html b/dom/html/test/file_iframe_sandbox_e_if13.html
new file mode 100644
index 0000000000..f5cf912f67
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if13.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ function doTest() {
+ var testContext = "?Test 25: Nested navigate _top with window.open():%20";
+ document.getElementById("if_12").src = "file_iframe_sandbox_e_if12.html" + testContext;
+ }
+</script>
+
+<body onload="doTest()">
+ <iframe sandbox='allow-scripts allow-top-navigation' id='if_12' height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if14.html b/dom/html/test/file_iframe_sandbox_e_if14.html
new file mode 100644
index 0000000000..76d9787020
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if14.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+ function doTest() {
+ var testContext = location.search.substring(1);
+ try {
+ var topsOpener = window.top.opener;
+ window.open("file_iframe_sandbox_top_navigation_fail.html?" + testContext, "_top");
+ topsOpener.postMessage({ok: false, desc: unescape(testContext) + "top navigation should NOT be allowed by a document sandboxed without 'allow-top-navigation.'"}, "*");
+ } catch(error) {
+ window.top.opener.postMessage({ok: true, desc: unescape(testContext) + "as expected error thrown during window.open(..., \"_top\")"}, "*");
+ window.top.close();
+ }
+ }
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if15.html b/dom/html/test/file_iframe_sandbox_e_if15.html
new file mode 100644
index 0000000000..bf4138e1d6
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if15.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ // Set our name, to allow an attempt to navigate us by name.
+ window.name = "e_if15";
+</script>
+
+<body>
+ <iframe sandbox='allow-scripts' id='if_16' src="file_iframe_sandbox_e_if16.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if16.html b/dom/html/test/file_iframe_sandbox_e_if16.html
new file mode 100644
index 0000000000..06c8bf8714
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if16.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ var testContext = "Test 26: navigate top by name with window.open(): ";
+
+ function doTest() {
+ try {
+ var topsOpener = window.top.opener;
+ window.open("file_iframe_sandbox_top_navigation_fail.html?" + escape(testContext), "e_if15");
+ topsOpener.postMessage({ok: false, desc: unescape(testContext) + "top navigation should NOT be allowed by a document sandboxed without 'allow-top-navigation.'"}, "*");
+ } catch(error) {
+ window.top.opener.postMessage({ok: true, desc: testContext + "as expected, error thrown during window.open(..., \"e_if15\")"}, "*");
+ window.top.close();
+ }
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed but with "allow-scripts"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if2.html b/dom/html/test/file_iframe_sandbox_e_if2.html
new file mode 100644
index 0000000000..739dbacbd5
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if2.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+ <iframe sandbox='allow-scripts allow-top-navigation allow-same-origin' id='if_1' src="file_iframe_sandbox_e_if1.html?Test 11: Nested navigate _top:%20" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if3.html b/dom/html/test/file_iframe_sandbox_e_if3.html
new file mode 100644
index 0000000000..ce010e6893
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if3.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <iframe sandbox='allow-scripts allow-top-navigation' id='if_5' src="file_iframe_sandbox_e_if5.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if4.html b/dom/html/test/file_iframe_sandbox_e_if4.html
new file mode 100644
index 0000000000..740a33a94d
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if4.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <iframe sandbox='allow-scripts allow-top-navigation' id='if_3' src="file_iframe_sandbox_e_if3.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if5.html b/dom/html/test/file_iframe_sandbox_e_if5.html
new file mode 100644
index 0000000000..e550df45e5
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if5.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+function navigateAway() {
+ document.getElementById("anchor").click();
+}
+</script>
+<body onload="navigateAway()">
+ I am sandboxed with 'allow-scripts and allow-top-navigation'
+
+ <a href="file_iframe_sandbox_top_navigation_pass.html" target='_top' id='anchor'>Click me</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if6.html b/dom/html/test/file_iframe_sandbox_e_if6.html
new file mode 100644
index 0000000000..399c3c202b
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if6.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+function doTest() {
+ document.getElementById('anchor').href = "file_iframe_sandbox_top_navigation_fail.html" + location.search;
+ window.top.opener.postMessage({type: "attempted"}, "*");
+ sendMouseEvent({type:'click'}, 'anchor');
+}
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts'
+
+ <a target='_top' id='anchor'>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if7.html b/dom/html/test/file_iframe_sandbox_e_if7.html
new file mode 100644
index 0000000000..9d60ed2dbc
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if7.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ // Set our name, to allow an attempt to navigate us by name.
+ window.name = "e_if7";
+</script>
+
+<body>
+ <iframe sandbox='allow-scripts' id='if_8' src="file_iframe_sandbox_e_if8.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if8.html b/dom/html/test/file_iframe_sandbox_e_if8.html
new file mode 100644
index 0000000000..97699abba9
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if8.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<script>
+ function doTest() {
+ // Try to navigate top using its name (e_if7). We should not be able to do this as allow-top-navigation is not specified.
+ window.top.opener.postMessage({type: "attempted"}, "*");
+ sendMouseEvent({type:'click'}, 'navigate_top');
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed but with "allow-scripts"
+
+ <a href="file_iframe_sandbox_top_navigation_fail.html?Test 15: Navigate top by name:%20" target="e_if7" id="navigate_top">navigate top</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if9.html b/dom/html/test/file_iframe_sandbox_e_if9.html
new file mode 100644
index 0000000000..f18a16dba6
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_e_if9.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ function doTest() {
+ var testContext = location.search == "" ? "?Test 22: Navigate _top with window.open():%20" : location.search;
+ document.getElementById("if_11").src = "file_iframe_sandbox_e_if11.html" + testContext;
+ }
+</script>
+
+<body onload="doTest()">
+ <iframe sandbox='allow-scripts allow-top-navigation' id='if_11' height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_fail.js b/dom/html/test/file_iframe_sandbox_fail.js
new file mode 100644
index 0000000000..1f1290d046
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_fail.js
@@ -0,0 +1,4 @@
+ok(
+ false,
+ "documents sandboxed with allow-scripts should NOT be able to run <script src=...>"
+);
diff --git a/dom/html/test/file_iframe_sandbox_form_fail.html b/dom/html/test/file_iframe_sandbox_form_fail.html
new file mode 100644
index 0000000000..6976ced8ad
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_form_fail.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body onLoad="doStuff()">
+ I should NOT be loaded by a form submit from a sandbox without 'allow-forms'
+</body>
+</html>
+
+<script>
+ function doStuff() {
+ window.parent.postMessage({ok: false, desc: "documents sandboxed without allow-forms should NOT be able to submit forms"}, "*");
+ }
+</script> \ No newline at end of file
diff --git a/dom/html/test/file_iframe_sandbox_form_pass.html b/dom/html/test/file_iframe_sandbox_form_pass.html
new file mode 100644
index 0000000000..1ba8853fa5
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_form_pass.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+</head>
+
+<body onLoad="doStuff()">
+ I should be loaded by a form submit from a sandbox with 'allow-forms'
+</body>
+</html>
+
+<script>
+ function doStuff() {
+ window.parent.postMessage({ok: true, desc: "documents sandboxed with allow-forms should be able to submit forms"}, "*");
+ }
+</script> \ No newline at end of file
diff --git a/dom/html/test/file_iframe_sandbox_g_if1.html b/dom/html/test/file_iframe_sandbox_g_if1.html
new file mode 100644
index 0000000000..67604f1f64
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_g_if1.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ // test data URI
+
+ // self.onmessage = function(event) {
+ // self.postMessage('make it so');
+ // };
+ var data_url = "data:text/plain;charset=utf-8;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbihldmVudCkgeyAgDQogICAgc2VsZi5wb3N0TWVzc2FnZSgnbWFrZSBpdCBzbycpOyAgDQp9Ow==";
+ var worker_data = new Worker(data_url);
+ worker_data.addEventListener('message', function(event) {
+ ok(true, "a worker in a sandboxed document should be able to be loaded from a data: URI");
+ });
+
+ worker_data.postMessage("engage!");
+
+ // test a blob URI we created (will have the same null principal
+ // as us
+ var b = new Blob(["onmessage = function(event) { self.postMessage('make it so');};"]);
+
+ var blobURL = URL.createObjectURL(b);
+
+ var worker_blob = new Worker(blobURL);
+
+ worker_blob.addEventListener('message', function(event) {
+ ok(true, "a worker in a sandboxed document should be able to be loaded from a blob URI " +
+ "created by that sandboxed document");
+ });
+
+ worker_blob.postMessage("engage!");
+
+ // test loading with relative url - this should fail since we are
+ // sandboxed and have a null principal
+ var worker_js = new Worker('file_iframe_sandbox_worker.js');
+ worker_js.onerror = function(error) {
+ ok(true, "a worker in a sandboxed document should tell the load error via error event");
+ }
+
+ worker_js.addEventListener('message', function(event) {
+ ok(false, "a worker in a sandboxed document should not be able to load from a relative URI");
+ });
+
+ worker_js.postMessage('engage');
+ }
+</script>
+<body onload='doStuff();'>
+ I am sandboxed but with "allow-scripts"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_h_if1.html b/dom/html/test/file_iframe_sandbox_h_if1.html
new file mode 100644
index 0000000000..7c5cada2dc
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_h_if1.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 766282</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.ok_wrapper(result, desc);
+ }
+
+ function doStuff() {
+ // Try to open a new window via target="_blank", target="BC766282" and window.open().
+ // The window we try to open closes itself once it opens.
+ sendMouseEvent({type:'click'}, 'target_blank');
+ sendMouseEvent({type:'click'}, 'target_BC766282');
+
+ try {
+ window.open("file_iframe_sandbox_open_window_pass.html");
+ } catch(e) {
+ ok(false, "Test 3: iframes sandboxed with allow-popups, should be able to open windows");
+ }
+ }
+</script>
+<body onLoad="doStuff()">
+ I am sandboxed but with "allow-popups allow-scripts allow-same-origin"
+
+ <a href="file_iframe_sandbox_open_window_pass.html" target="_blank" rel="opener" id="target_blank">open window</a>
+ <a href="file_iframe_sandbox_open_window_pass.html?BC766282" target="BC766282" id="target_BC766282">open window</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_k_if1.html b/dom/html/test/file_iframe_sandbox_k_if1.html
new file mode 100644
index 0000000000..f6f1238085
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_k_if1.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="text/javascript">
+ var windowsToClose = new Array();
+
+ function closeWindows() {
+ for (var i = 0; i < windowsToClose.length; i++) {
+ windowsToClose[i].close();
+ }
+ window.open("file_iframe_sandbox_close.html", "blank_if2");
+ window.open("file_iframe_sandbox_close.html", "BC766282_if2");
+ }
+
+ // Add message listener to forward messages on to parent
+ window.addEventListener("message", receiveMessage);
+
+ function receiveMessage(event) {
+ switch (event.data.type) {
+ case "closeWindows":
+ closeWindows();
+ break;
+ }
+ }
+
+ function doStuff() {
+ // Open a new window via target="_blank", target="BC766282_if2" and window.open().
+ sendMouseEvent({type:'click'}, 'target_blank_if2');
+ sendMouseEvent({type:'click'}, 'target_BC766282_if2');
+
+ windowsToClose.push(window.open("file_iframe_sandbox_k_if2.html"));
+ }
+</script>
+<body onLoad="doStuff()">
+ I am navigated to from file_iframe_sandbox_k_if8.html.
+ This was opened in an iframe with "allow-scripts allow-popups allow-same-origin".
+ However allow-same-origin was removed from the iframe before navigating to me,
+ so I should only have "allow-scripts allow-popups" in force.
+ <a href="file_iframe_sandbox_k_if2.html" target="_blank" id="target_blank_if2" rel="opener">open window</a>
+ <a href="file_iframe_sandbox_k_if2.html" target="BC766282_if2" id="target_BC766282_if2">open window</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_k_if2.html b/dom/html/test/file_iframe_sandbox_k_if2.html
new file mode 100644
index 0000000000..dce42aef54
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_k_if2.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="text/javascript">
+ if (window.name == "") {
+ window.name = "blank_if2";
+ }
+
+ function ok(result, message) {
+ window.opener.parent.postMessage({type: "ok", ok: result, desc: message}, "*");
+ }
+
+ function doStuff() {
+ // Check that sandboxed forms browsing context flag copied by attempting to submit a form.
+ document.getElementById('a_form').submit();
+ window.opener.parent.postMessage({type: "attempted"}, "*");
+
+ // Check that sandboxed origin browsing context flag copied by attempting to access cookies.
+ try {
+ var foo = document.cookie;
+ ok(false, "Sandboxed origin browsing context flag NOT copied to new auxiliary browsing context.");
+ } catch(error) {
+ ok(true, "Sandboxed origin browsing context flag copied to new auxiliary browsing context.");
+ }
+
+ // Check that sandboxed top-level navigation browsing context flag copied.
+ // if_3 tries to navigate this document.
+ var if_3 = document.getElementById('if_3');
+ if_3.src = "file_iframe_sandbox_k_if3.html";
+ }
+</script>
+
+<body onLoad="doStuff()">
+ I am not sandboxed directly, but opened from a sandboxed document with 'allow-scripts allow-popups'
+
+ <form method="get" action="file_iframe_sandbox_window_form_fail.html" id="a_form">
+ First name: <input type="text" name="firstname">
+ Last name: <input type="text" name="lastname">
+ <input type="submit" id="a_button">
+ </form>
+
+ <iframe id="if_3" src="about:blank" height="10" width="10"></iframe>
+
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_k_if3.html b/dom/html/test/file_iframe_sandbox_k_if3.html
new file mode 100644
index 0000000000..a2619dd006
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_k_if3.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<script type="application/javascript">
+ function doTest() {
+ sendMouseEvent({type:'click'}, 'anchor');
+ window.parent.opener.parent.postMessage({type: "attempted"}, "*");
+ }
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts allow-popups'
+
+ <a href="file_iframe_sandbox_window_top_navigation_fail.html" target='_top' id='anchor'>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_k_if4.html b/dom/html/test/file_iframe_sandbox_k_if4.html
new file mode 100644
index 0000000000..3d030158dc
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_k_if4.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="text/javascript">
+ function doStuff() {
+ // Open a new window via target="_blank", target="BC766282_if5" and window.open().
+ sendMouseEvent({type:'click'}, 'target_blank_if5');
+ sendMouseEvent({type:'click'}, 'target_BC766282_if5');
+
+ window.open("file_iframe_sandbox_k_if5.html");
+
+ // Open a new window via target="_blank", target="BC766282_if7" and window.open().
+ sendMouseEvent({type:'click'}, 'target_blank_if7');
+ sendMouseEvent({type:'click'}, 'target_BC766282_if7');
+
+ window.open("file_iframe_sandbox_k_if7.html");
+ }
+</script>
+
+<body onLoad="doStuff()">
+ I am sandboxed with "allow-scripts allow-popups allow-same-origin allow-forms allow-top-navigation".
+ <a href="file_iframe_sandbox_k_if5.html" target="_blank" id="target_blank_if5" rel="opener">open window</a>
+ <a href="file_iframe_sandbox_k_if5.html" target="BC766282_if5" id="target_BC766282_if5">open window</a>
+
+ <a href="file_iframe_sandbox_k_if7.html" target="_blank" id="target_blank_if7" rel="opener">open window</a>
+ <a href="file_iframe_sandbox_k_if7.html" target="BC766282_if7" id="target_BC766282_if7">open window</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_k_if5.html b/dom/html/test/file_iframe_sandbox_k_if5.html
new file mode 100644
index 0000000000..8deb65852f
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_k_if5.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="text/javascript">
+ function doStuff() {
+ // Check that sandboxed origin browsing context flag NOT set by attempting to access cookies.
+ try {
+ var foo = document.cookie;
+ window.opener.parent.ok_wrapper(true, "Sandboxed origin browsing context flag NOT set on new auxiliary browsing context.");
+ } catch(error) {
+ window.opener.parent.ok_wrapper(false, "Sandboxed origin browsing context flag set on new auxiliary browsing context.");
+ }
+
+ // Check that sandboxed top-level navigation browsing context flag NOT set.
+ // if_6 tries to navigate this document.
+ var if_6 = document.getElementById('if_6');
+ if_6.src = "file_iframe_sandbox_k_if6.html";
+ }
+</script>
+
+<body onLoad="doStuff()">
+ I am not sandboxed directly, but opened from a sandboxed document with at least
+ 'allow-scripts allow-popups allow-same-origin allow-top-navigation'
+
+ <iframe id="if_6" src="about:blank" height="10" width="10"></iframe>
+
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_k_if6.html b/dom/html/test/file_iframe_sandbox_k_if6.html
new file mode 100644
index 0000000000..53ed080e3e
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_k_if6.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<script type="application/javascript">
+ function doTest() {
+ sendMouseEvent({type:'click'}, 'anchor');
+ }
+</script>
+
+<body onload="doTest()">
+ I am sandboxed with at least 'allow-scripts allow-popups allow-top-navigation'
+
+ <a href="file_iframe_sandbox_window_top_navigation_pass.html" target='_top' id='anchor'>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_k_if7.html b/dom/html/test/file_iframe_sandbox_k_if7.html
new file mode 100644
index 0000000000..269e31eb5b
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_k_if7.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="text/javascript">
+ function doStuff() {
+ // Check that sandboxed forms browsing context flag NOT set by attempting to submit a form.
+ document.getElementById('a_form').submit();
+ }
+</script>
+
+<body onLoad="doStuff()">
+ I am not sandboxed directly, but opened from a sandboxed document with at least
+ 'allow-scripts allow-popups allow-forms allow-same-origin'
+
+ <form method="get" action="file_iframe_sandbox_window_form_pass.html" id="a_form">
+ First name: <input type="text" name="firstname">
+ Last name: <input type="text" name="lastname">
+ <input type="submit" id="a_button">
+ </form>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_k_if8.html b/dom/html/test/file_iframe_sandbox_k_if8.html
new file mode 100644
index 0000000000..e4aad97f3b
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_k_if8.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="text/javascript">
+ function doSubOpens() {
+ // Open a new window via target="_blank", target="BC766282_if9" and window.open().
+ sendMouseEvent({type:'click'}, 'target_blank_if9');
+ sendMouseEvent({type:'click'}, 'target_BC766282_if9');
+
+ window.open("file_iframe_sandbox_k_if9.html");
+
+ sendMouseEvent({type:'click'}, 'target_if1');
+ }
+
+ window.doSubOpens = doSubOpens;
+</script>
+
+<body>
+ I am sandboxed but with "allow-scripts allow-popups allow-same-origin".
+ After my initial load, "allow-same-origin" is removed and then I open file_iframe_sandbox_k_if9.html
+ in 3 different ways, which attemps to call a function in my parent.
+ This should succeed since the new sandbox flags shouldn't have taken affect on me until I'm reloaded.
+ <a href="file_iframe_sandbox_k_if9.html" target="_blank" id="target_blank_if9" rel="opener">open window</a>
+ <a href="file_iframe_sandbox_k_if9.html" target="BC766282_if9" id="target_BC766282_if9">open window</a>
+
+ Now navigate to file_iframe_sandbox_k_if1.html to do tests for a sandbox opening a window
+ when only "allow-scripts allow-popups" are specified.
+ <a href="file_iframe_sandbox_k_if1.html" id="target_if1">navigate to if1</a>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_k_if9.html b/dom/html/test/file_iframe_sandbox_k_if9.html
new file mode 100644
index 0000000000..56e8db3f9a
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_k_if9.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ function doStuff() {
+ window.opener.parent.ok_wrapper(true, "A window opened from within a sandboxed document should inherit the flags of the document, not of the docshell/sandbox attribute.");
+ self.close();
+ }
+</script>
+
+<body onload='doStuff()'>
+ I'm a window opened from the sandboxed document of file_iframe_sandbox_k_if8.html.
+ I should be able to call ok_wrapper in main test page directly because I should be same-origin with it.
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_navigation_fail.html b/dom/html/test/file_iframe_sandbox_navigation_fail.html
new file mode 100644
index 0000000000..bae5276bd1
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_navigation_fail.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onLoad="doStuff()">
+FAIL
+</body>
+<script>
+ function doStuff() {
+ var testContext = unescape(location.search.substring(1));
+ window.parent.postMessage({ok: false, desc: testContext + "this navigation should NOT be allowed by a sandboxed document", addToAttempted: false}, "*");
+ }
+</script>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_navigation_pass.html b/dom/html/test/file_iframe_sandbox_navigation_pass.html
new file mode 100644
index 0000000000..e07248247b
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_navigation_pass.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+function doStuff() {
+ var testContext = unescape(location.search.substring(1));
+ window.parent.postMessage({ok: true, desc: testContext + "this navigation should be allowed by a sandboxed document"}, "*");
+}
+</script>
+<body onLoad="doStuff()">
+PASS
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_navigation_start.html b/dom/html/test/file_iframe_sandbox_navigation_start.html
new file mode 100644
index 0000000000..fa56425177
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_navigation_start.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+I am just a normal HTML document, probably contained in a sandboxed iframe
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_open_window_fail.html b/dom/html/test/file_iframe_sandbox_open_window_fail.html
new file mode 100644
index 0000000000..64e0d36180
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_open_window_fail.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body onLoad="doStuff()">
+ I should NOT be opened by a sandboxed iframe via any method
+</body>
+</html>
+
+<script>
+ function doStuff() {
+ window.opener.ok(false, "sandboxed documents should NOT be able to open windows");
+ self.close();
+ }
+</script>
diff --git a/dom/html/test/file_iframe_sandbox_open_window_pass.html b/dom/html/test/file_iframe_sandbox_open_window_pass.html
new file mode 100644
index 0000000000..ac45c7fd32
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_open_window_pass.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body onLoad="doStuff()">
+ I should be opened by a sandboxed iframe via any method when "allow-popups" is specified.
+</body>
+</html>
+
+<script>
+ function doStuff() {
+ // Check that the browsing context's (window's) name is as expected.
+ var expectedName = location.search.substring(1);
+ if (expectedName == window.name) {
+ window.opener.ok(true, "sandboxed documents should be able to open windows when \"allow-popups\" is specified");
+ } else {
+ window.opener.ok(false, "window opened with \"allow-popups\", but expected name was " + expectedName + " and actual was " + window.name);
+ }
+ self.close();
+ }
+</script>
diff --git a/dom/html/test/file_iframe_sandbox_pass.js b/dom/html/test/file_iframe_sandbox_pass.js
new file mode 100644
index 0000000000..15b3e7d3ff
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_pass.js
@@ -0,0 +1,4 @@
+ok(
+ true,
+ "documents sandboxed with allow-scripts should be able to run <script src=...>"
+);
diff --git a/dom/html/test/file_iframe_sandbox_redirect.html b/dom/html/test/file_iframe_sandbox_redirect.html
new file mode 100644
index 0000000000..62419d7f46
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_redirect.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body>redirect</body>
diff --git a/dom/html/test/file_iframe_sandbox_redirect.html^headers^ b/dom/html/test/file_iframe_sandbox_redirect.html^headers^
new file mode 100644
index 0000000000..71b739c42a
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_redirect.html^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: file_iframe_sandbox_redirect_target.html
diff --git a/dom/html/test/file_iframe_sandbox_redirect_target.html b/dom/html/test/file_iframe_sandbox_redirect_target.html
new file mode 100644
index 0000000000..c134ac0ffd
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_redirect_target.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<head>
+ <script>
+ onmessage = function(event) {
+ parent.postMessage(event.data + " redirect target", "*");
+ }
+ </script>
+</head>
+<body>I have been redirected</body>
diff --git a/dom/html/test/file_iframe_sandbox_refresh.html b/dom/html/test/file_iframe_sandbox_refresh.html
new file mode 100644
index 0000000000..1fad80c428
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_refresh.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<body>refresh</body>
diff --git a/dom/html/test/file_iframe_sandbox_refresh.html^headers^ b/dom/html/test/file_iframe_sandbox_refresh.html^headers^
new file mode 100644
index 0000000000..a7cc383b4f
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_refresh.html^headers^
@@ -0,0 +1 @@
+Refresh: 0 url=data:text/html,Refreshed
diff --git a/dom/html/test/file_iframe_sandbox_srcdoc_allow_scripts.html b/dom/html/test/file_iframe_sandbox_srcdoc_allow_scripts.html
new file mode 100644
index 0000000000..7d585be04f
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_srcdoc_allow_scripts.html
@@ -0,0 +1 @@
+<script>parent.parent.ok_wrapper(true, "an object inside an iframe sandboxed with allow-scripts allow-same-origin should be able to run scripts and call functions in the parent of the iframe")</script>
diff --git a/dom/html/test/file_iframe_sandbox_srcdoc_no_allow_scripts.html b/dom/html/test/file_iframe_sandbox_srcdoc_no_allow_scripts.html
new file mode 100644
index 0000000000..b6faf83cc9
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_srcdoc_no_allow_scripts.html
@@ -0,0 +1 @@
+<script>parent.parent.ok_wrapper(false, 'an object inside an iframe sandboxed with only allow-same-origin should not be able to run scripts')</script>
diff --git a/dom/html/test/file_iframe_sandbox_top_navigation_fail.html b/dom/html/test/file_iframe_sandbox_top_navigation_fail.html
new file mode 100644
index 0000000000..dad6b2c006
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_top_navigation_fail.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+function doStuff() {
+ var testContext = unescape(location.search.substring(1));
+ window.opener.postMessage({ok: false, desc: testContext + "top navigation should NOT be allowed by a document sandboxed without 'allow-top-navigation'", addToAttempted: false}, "*");
+ window.close();
+}
+</script>
+<body onLoad="doStuff()">
+FAIL\
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_top_navigation_pass.html b/dom/html/test/file_iframe_sandbox_top_navigation_pass.html
new file mode 100644
index 0000000000..712240ecb2
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_top_navigation_pass.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+function doStuff() {
+ var testContext = unescape(location.search.substring(1));
+ var bc = new BroadcastChannel("test_iframe_sandbox_navigation");
+ bc.postMessage({ok: true, desc: testContext + "top navigation should be allowed by a document sandboxed with 'allow-top-navigation'"});
+ bc.close();
+ window.close();
+}
+</script>
+<body onLoad="doStuff()">
+PASS
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_window_form_fail.html b/dom/html/test/file_iframe_sandbox_window_form_fail.html
new file mode 100644
index 0000000000..2d678b3ac9
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_window_form_fail.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body onLoad="doStuff()">
+ I should NOT be loaded by a form submit from a window opened from a sandbox without 'allow-forms'.
+</body>
+</html>
+
+<script>
+ function doStuff() {
+ window.opener.parent.postMessage({ok: false, desc: "documents sandboxed without allow-forms should NOT be able to submit forms"}, "*");
+
+ self.close();
+ }
+</script>
diff --git a/dom/html/test/file_iframe_sandbox_window_form_pass.html b/dom/html/test/file_iframe_sandbox_window_form_pass.html
new file mode 100644
index 0000000000..dd2656c1ec
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_window_form_pass.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ function doStuff() {
+ window.opener.parent.ok_wrapper(true, "Sandboxed forms browsing context flag NOT set on new auxiliary browsing context.");
+
+ self.close();
+ }
+</script>
+
+<body onLoad="doStuff()">
+ I should be loaded by a form submit from a window opened from a sandbox with 'allow-forms allow-same-origin'.
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_window_navigation_fail.html b/dom/html/test/file_iframe_sandbox_window_navigation_fail.html
new file mode 100644
index 0000000000..f8e3c83ce8
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_window_navigation_fail.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 838692</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+function doStuff() {
+ var testContext = unescape(location.search.substring(1));
+ window.opener.postMessage({ok: false, desc: testContext + "a sandboxed document should not be able to navigate a window it hasn't opened.", addToAttempted: false}, "*");
+ window.close();
+}
+</script>
+
+<body onLoad="doStuff()">
+FAIL
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_window_navigation_pass.html b/dom/html/test/file_iframe_sandbox_window_navigation_pass.html
new file mode 100644
index 0000000000..a1bff9eb83
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_window_navigation_pass.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+function doStuff() {
+ var testContext = unescape(location.search.substring(1));
+ window.opener.postMessage({type: "ok", ok: true, desc: testContext + "a permitted sandboxed document should be able to navigate a window it has opened.", addToAttempted: false}, "*");
+ window.close();
+}
+</script>
+
+<body onLoad="doStuff()">
+PASS
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_window_top_navigation_fail.html b/dom/html/test/file_iframe_sandbox_window_top_navigation_fail.html
new file mode 100644
index 0000000000..af50476045
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_window_top_navigation_fail.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script>
+ function doStuff() {
+ window.opener.parent.postMessage({ok: false, desc: "Sandboxed top-level navigation browsing context flag NOT copied to new auxiliary browsing context."}, "*");
+
+ // Check that when no browsing context returned by "target='_top'", a new browsing context isn't opened by mistake.
+ try {
+ window.opener.parent.opener.parent.postMessage({ok: false, desc: "An attempt at top navigation without 'allow-top-navigation' should not have opened a new browsing context."}, "*");
+ } catch (error) {
+ }
+
+ self.close();
+ }
+</script>
+<body onLoad="doStuff()">
+FAIL
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_window_top_navigation_pass.html b/dom/html/test/file_iframe_sandbox_window_top_navigation_pass.html
new file mode 100644
index 0000000000..d3637fb04e
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_window_top_navigation_pass.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 766282</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script>
+ function doStuff() {
+ window.opener.parent.ok_wrapper(true, "Sandboxed top-level navigation browsing context flag NOT copied to new auxiliary browsing context.");
+
+ self.close();
+ }
+</script>
+
+<body onLoad="doStuff()">
+ I am navigated to from a window opened from a sandbox with allow-top-navigation.
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_worker.js b/dom/html/test/file_iframe_sandbox_worker.js
new file mode 100644
index 0000000000..3cb9f650dc
--- /dev/null
+++ b/dom/html/test/file_iframe_sandbox_worker.js
@@ -0,0 +1,3 @@
+self.onmessage = function (event) {
+ self.postMessage("make it so");
+};
diff --git a/dom/html/test/file_refresh_after_document_write.html b/dom/html/test/file_refresh_after_document_write.html
new file mode 100644
index 0000000000..ebf3272e08
--- /dev/null
+++ b/dom/html/test/file_refresh_after_document_write.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+<title></title>
+</head>
+<script>
+function write_and_refresh(){
+ document.write("This could be anything");
+ location.reload();
+}
+</script>
+<body>
+<button id='test_btn' onclick='write_and_refresh()'>
+</body>
+
+</html>
diff --git a/dom/html/test/file_script_module.html b/dom/html/test/file_script_module.html
new file mode 100644
index 0000000000..78c4992654
--- /dev/null
+++ b/dom/html/test/file_script_module.html
@@ -0,0 +1,42 @@
+<html>
+<body>
+ <script>
+// Helper methods.
+function ok(a, msg) {
+ parent.postMessage({ check: !!a, msg }, "*")
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+function finish() {
+ parent.postMessage({ done: true }, "*");
+}
+ </script>
+
+ <script id="a" nomodule>42</script>
+ <script id="b">42</script>
+ <script>
+// Let's test the behavior of nomodule attribute and noModule getter/setter.
+var a = document.getElementById("a");
+is(a.noModule, true, "HTMLScriptElement with nomodule attribute has noModule set to true");
+a.removeAttribute("nomodule");
+is(a.noModule, false, "HTMLScriptElement without nomodule attribute has noModule set to false");
+a.noModule = true;
+ok(a.hasAttribute('nomodule'), "HTMLScriptElement.noModule = true add the nomodule attribute");
+
+var b = document.getElementById("b");
+is(b.noModule, false, "HTMLScriptElement without nomodule attribute has noModule set to false");
+b.noModule = true;
+ok(b.hasAttribute('nomodule'), "HTMLScriptElement.noModule = true add the nomodule attribute");
+ </script>
+
+ <script>var foo = 42;</script>
+ <script nomodule>foo = 43;</script>
+ <script>
+is(foo, 42, "nomodule HTMLScriptElements should not be executed in modern browsers");
+finish();
+ </script>
+</body>
+</html>
diff --git a/dom/html/test/file_srcdoc-2.html b/dom/html/test/file_srcdoc-2.html
new file mode 100644
index 0000000000..bd75f5e059
--- /dev/null
+++ b/dom/html/test/file_srcdoc-2.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=802895
+-->
+<body>
+<iframe id="iframe" srcdoc="Hello World"></iframe>
+</body>
+
+</html>
diff --git a/dom/html/test/file_srcdoc.html b/dom/html/test/file_srcdoc.html
new file mode 100644
index 0000000000..7f084bc74b
--- /dev/null
+++ b/dom/html/test/file_srcdoc.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=802895
+-->
+<body>
+<iframe id="iframe" srcdoc="Hello World"></iframe>
+
+<iframe id="iframe1" src="about:mozilla"
+ srcdoc="Goodbye World"></iframe>
+<iframe id="iframe2" srcdoc="Peeking test" sandbox=""></iframe>
+<iframe id="iframe3" src="file_srcdoc_iframe3.html"
+ srcdoc="Going"></iframe>
+</body>
+
+</html>
diff --git a/dom/html/test/file_srcdoc_iframe3.html b/dom/html/test/file_srcdoc_iframe3.html
new file mode 100644
index 0000000000..233692734f
--- /dev/null
+++ b/dom/html/test/file_srcdoc_iframe3.html
@@ -0,0 +1 @@
+Gone
diff --git a/dom/html/test/file_window_close_and_open.html b/dom/html/test/file_window_close_and_open.html
new file mode 100644
index 0000000000..ad96e50aac
--- /dev/null
+++ b/dom/html/test/file_window_close_and_open.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<script>
+ console.log("loading file_window_close_and_open.html");
+ addEventListener("load", function() {
+ console.log("got load event!");
+ let link = document.querySelector("a");
+ if (window.location.hash === "#noopener") {
+ link.setAttribute("rel", "noopener");
+ } else if (window.location.hash === "#opener") {
+ link.setAttribute("rel", "opener");
+ }
+ link.click();
+ });
+</script>
+<body>
+ <h1>close and re-open popup</h1>
+ <a href="file_broadcast_load.html" target="_blank" onclick="window.close()">close and open</a>
+</body>
+</html>
diff --git a/dom/html/test/file_window_open_close_inner.html b/dom/html/test/file_window_open_close_inner.html
new file mode 100644
index 0000000000..dbc7e3aba8
--- /dev/null
+++ b/dom/html/test/file_window_open_close_inner.html
@@ -0,0 +1,7 @@
+<html>
+<body>
+<script>
+window.close();
+</script>
+</html>
+</body>
diff --git a/dom/html/test/file_window_open_close_outer.html b/dom/html/test/file_window_open_close_outer.html
new file mode 100644
index 0000000000..682b399e75
--- /dev/null
+++ b/dom/html/test/file_window_open_close_outer.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+<a id="link" href="file_window_open_close_inner.html" target="_blank" rel="opener" onclick="setTimeout(function () { window.close() }, 0)">link</a>
+</html>
+</body>
diff --git a/dom/html/test/formData_test.js b/dom/html/test/formData_test.js
new file mode 100644
index 0000000000..3997aff4d1
--- /dev/null
+++ b/dom/html/test/formData_test.js
@@ -0,0 +1,289 @@
+function testHas() {
+ var f = new FormData();
+ f.append("foo", "bar");
+ f.append("another", "value");
+ ok(f.has("foo"), "has() on existing name should be true.");
+ ok(f.has("another"), "has() on existing name should be true.");
+ ok(!f.has("nonexistent"), "has() on non-existent name should be false.");
+}
+
+function testGet() {
+ var f = new FormData();
+ f.append("foo", "bar");
+ f.append("foo", "bar2");
+ f.append("blob", new Blob(["hey"], { type: "text/plain" }));
+ f.append("file", new File(["hey"], "testname", { type: "text/plain" }));
+
+ is(f.get("foo"), "bar", "get() on existing name should return first value");
+ ok(
+ f.get("blob") instanceof Blob,
+ "get() on existing name should return first value"
+ );
+ is(
+ f.get("blob").type,
+ "text/plain",
+ "get() on existing name should return first value"
+ );
+ ok(
+ f.get("file") instanceof File,
+ "get() on existing name should return first value"
+ );
+ is(
+ f.get("file").name,
+ "testname",
+ "get() on existing name should return first value"
+ );
+
+ is(
+ f.get("nonexistent"),
+ null,
+ "get() on non-existent name should return null."
+ );
+}
+
+function testGetAll() {
+ var f = new FormData();
+ f.append("other", "value");
+ f.append("foo", "bar");
+ f.append("foo", "bar2");
+ f.append("foo", new Blob(["hey"], { type: "text/plain" }));
+
+ var arr = f.getAll("foo");
+ is(arr.length, 3, "getAll() should retrieve all matching entries.");
+ is(arr[0], "bar", "values should match and be in order");
+ is(arr[1], "bar2", "values should match and be in order");
+ ok(arr[2] instanceof Blob, "values should match and be in order");
+
+ is(
+ f.get("nonexistent"),
+ null,
+ "get() on non-existent name should return null."
+ );
+}
+
+function testDelete() {
+ var f = new FormData();
+ f.append("other", "value");
+ f.append("foo", "bar");
+ f.append("foo", "bar2");
+ f.append("foo", new Blob(["hey"], { type: "text/plain" }));
+
+ ok(f.has("foo"), "has() on existing name should be true.");
+ f.delete("foo");
+ ok(!f.has("foo"), "has() on deleted name should be false.");
+ is(f.getAll("foo").length, 0, "all entries should be deleted.");
+
+ is(f.getAll("other").length, 1, "other names should still be there.");
+ f.delete("other");
+ is(f.getAll("other").length, 0, "all entries should be deleted.");
+}
+
+function testSet() {
+ var f = new FormData();
+
+ f.set("other", "value");
+ ok(f.has("other"), "set() on new name should be similar to append()");
+ is(
+ f.getAll("other").length,
+ 1,
+ "set() on new name should be similar to append()"
+ );
+
+ f.append("other", "value2");
+ is(
+ f.getAll("other").length,
+ 2,
+ "append() should not replace existing entries."
+ );
+
+ f.append("foo", "bar");
+ f.append("other", "value3");
+ f.append("other", "value3");
+ f.append("other", "value3");
+ is(
+ f.getAll("other").length,
+ 5,
+ "append() should not replace existing entries."
+ );
+
+ f.set("other", "value4");
+ is(f.getAll("other").length, 1, "set() should replace existing entries.");
+ is(f.getAll("other")[0], "value4", "set() should replace existing entries.");
+}
+
+function testFilename() {
+ var f = new FormData();
+ f.append("blob", new Blob(["hi"]));
+ ok(f.get("blob") instanceof Blob, "We should have a blob back.");
+
+ // If a filename is passed, that should replace the original.
+ f.append("blob2", new Blob(["hi"]), "blob2.txt");
+ is(
+ f.get("blob2").name,
+ "blob2.txt",
+ 'Explicit filename should override "blob".'
+ );
+
+ var file = new File(["hi"], "file1.txt");
+ f.append("file1", file);
+ // If a file is passed, the "create entry" algorithm should not create a new File, but reuse the existing one.
+ is(
+ f.get("file1"),
+ file,
+ "Retrieved File object should be original File object and not a copy."
+ );
+ is(
+ f.get("file1").name,
+ "file1.txt",
+ "File's filename should be original's name if no filename is explicitly passed."
+ );
+
+ file = new File(["hi"], "file2.txt");
+ f.append("file2", file, "fakename.txt");
+ ok(
+ f.get("file2") !== file,
+ "Retrieved File object should be new File object if explicit filename is passed."
+ );
+ is(
+ f.get("file2").name,
+ "fakename.txt",
+ "File's filename should be explicitly passed name."
+ );
+ f.append("file3", new File(["hi"], ""));
+ is(f.get("file3").name, "", "File's filename is returned even if empty.");
+}
+
+function testIterable() {
+ var fd = new FormData();
+ fd.set("1", "2");
+ fd.set("2", "4");
+ fd.set("3", "6");
+ fd.set("4", "8");
+ fd.set("5", "10");
+
+ var key_iter = fd.keys();
+ var value_iter = fd.values();
+ var entries_iter = fd.entries();
+ for (var i = 0; i < 5; ++i) {
+ var v = i + 1;
+ var key = key_iter.next();
+ var value = value_iter.next();
+ var entry = entries_iter.next();
+ is(key.value, v.toString(), "Correct Key iterator: " + v.toString());
+ ok(!key.done, "key.done is false");
+ is(
+ value.value,
+ (v * 2).toString(),
+ "Correct Value iterator: " + (v * 2).toString()
+ );
+ ok(!value.done, "value.done is false");
+ is(
+ entry.value[0],
+ v.toString(),
+ "Correct Entry 0 iterator: " + v.toString()
+ );
+ is(
+ entry.value[1],
+ (v * 2).toString(),
+ "Correct Entry 1 iterator: " + (v * 2).toString()
+ );
+ ok(!entry.done, "entry.done is false");
+ }
+
+ var last = key_iter.next();
+ ok(last.done, "Nothing more to read.");
+ is(last.value, undefined, "Undefined is the last key");
+
+ last = value_iter.next();
+ ok(last.done, "Nothing more to read.");
+ is(last.value, undefined, "Undefined is the last value");
+
+ last = entries_iter.next();
+ ok(last.done, "Nothing more to read.");
+
+ key_iter = fd.keys();
+ key_iter.next();
+ key_iter.next();
+ fd.delete("1");
+ fd.delete("2");
+ fd.delete("3");
+ fd.delete("4");
+ fd.delete("5");
+
+ last = key_iter.next();
+ ok(last.done, "Nothing more to read.");
+ is(last.value, undefined, "Undefined is the last key");
+}
+
+function testSend(doneCb) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "form_submit_server.sjs");
+ xhr.onload = function () {
+ var response = xhr.response;
+
+ for (var entry of response) {
+ is(entry.body, "hey");
+ is(entry.headers["Content-Type"], "text/plain");
+ }
+
+ is(
+ response[0].headers["Content-Disposition"],
+ 'form-data; name="empty"; filename="blob"'
+ );
+
+ is(
+ response[1].headers["Content-Disposition"],
+ 'form-data; name="explicit"; filename="explicit-file-name"'
+ );
+
+ is(
+ response[2].headers["Content-Disposition"],
+ 'form-data; name="explicit-empty"; filename=""'
+ );
+
+ is(
+ response[3].headers["Content-Disposition"],
+ 'form-data; name="file-name"; filename="testname"'
+ );
+
+ is(
+ response[4].headers["Content-Disposition"],
+ 'form-data; name="empty-file-name"; filename=""'
+ );
+
+ is(
+ response[5].headers["Content-Disposition"],
+ 'form-data; name="file-name-overwrite"; filename="overwrite"'
+ );
+
+ doneCb();
+ };
+
+ var file,
+ blob = new Blob(["hey"], { type: "text/plain" });
+
+ var fd = new FormData();
+ fd.append("empty", blob);
+ fd.append("explicit", blob, "explicit-file-name");
+ fd.append("explicit-empty", blob, "");
+ file = new File([blob], "testname", { type: "text/plain" });
+ fd.append("file-name", file);
+ file = new File([blob], "", { type: "text/plain" });
+ fd.append("empty-file-name", file);
+ file = new File([blob], "testname", { type: "text/plain" });
+ fd.append("file-name-overwrite", file, "overwrite");
+ xhr.responseType = "json";
+ xhr.send(fd);
+}
+
+function runTest(doneCb) {
+ testHas();
+ testGet();
+ testGetAll();
+ testDelete();
+ testSet();
+ testFilename();
+ testIterable();
+ // Finally, send an XHR and verify the response matches.
+ testSend(doneCb);
+}
diff --git a/dom/html/test/formData_worker.js b/dom/html/test/formData_worker.js
new file mode 100644
index 0000000000..750522fbfa
--- /dev/null
+++ b/dom/html/test/formData_worker.js
@@ -0,0 +1,23 @@
+function ok(a, msg) {
+ postMessage({ type: "status", status: !!a, msg: a + ": " + msg });
+}
+
+function is(a, b, msg) {
+ postMessage({
+ type: "status",
+ status: a === b,
+ msg: a + " === " + b + ": " + msg,
+ });
+}
+
+function todo(a, msg) {
+ postMessage({ type: "todo", status: !!a, msg: a + ": " + msg });
+}
+
+importScripts("formData_test.js");
+
+onmessage = function () {
+ runTest(function () {
+ postMessage({ type: "finish" });
+ });
+};
diff --git a/dom/html/test/formSubmission_chrome.js b/dom/html/test/formSubmission_chrome.js
new file mode 100644
index 0000000000..da1224d107
--- /dev/null
+++ b/dom/html/test/formSubmission_chrome.js
@@ -0,0 +1,20 @@
+/* eslint-env mozilla/chrome-script */
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["File"]);
+
+addMessageListener("files.open", function (message) {
+ let list = [];
+ let promises = [];
+ for (let path of message) {
+ promises.push(
+ File.createFromFileName(path).then(file => {
+ list.push(file);
+ })
+ );
+ }
+
+ Promise.all(promises).then(() => {
+ sendAsyncMessage("files.opened", list);
+ });
+});
diff --git a/dom/html/test/form_data_file.bin b/dom/html/test/form_data_file.bin
new file mode 100644
index 0000000000..744bde3558
--- /dev/null
+++ b/dom/html/test/form_data_file.bin
@@ -0,0 +1 @@
+
diff --git a/dom/html/test/form_data_file.txt b/dom/html/test/form_data_file.txt
new file mode 100644
index 0000000000..81c545efeb
--- /dev/null
+++ b/dom/html/test/form_data_file.txt
@@ -0,0 +1 @@
+1234
diff --git a/dom/html/test/form_submit_server.sjs b/dom/html/test/form_submit_server.sjs
new file mode 100644
index 0000000000..553809c01f
--- /dev/null
+++ b/dom/html/test/form_submit_server.sjs
@@ -0,0 +1,86 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function utf8decode(s) {
+ return decodeURIComponent(escape(s));
+}
+
+function utf8encode(s) {
+ return unescape(encodeURIComponent(s));
+}
+
+function handleRequest(request, response) {
+ var bodyStream = new BinaryInputStream(request.bodyInputStream);
+ var result = [];
+ var requestBody = "";
+ while ((bodyAvail = bodyStream.available()) > 0) {
+ requestBody += bodyStream.readBytes(bodyAvail);
+ }
+
+ if (request.method == "POST") {
+ var contentTypeParams = {};
+ request
+ .getHeader("Content-Type")
+ .split(/\s*\;\s*/)
+ .forEach(function (s) {
+ if (s.indexOf("=") >= 0) {
+ let [name, value] = s.split("=");
+ contentTypeParams[name] = value;
+ } else {
+ contentTypeParams[""] = s;
+ }
+ });
+
+ if (
+ contentTypeParams[""] == "multipart/form-data" &&
+ request.queryString == ""
+ ) {
+ requestBody
+ .split("--" + contentTypeParams.boundary)
+ .slice(1, -1)
+ .forEach(function (s) {
+ let headers = {};
+ let headerEnd = s.indexOf("\r\n\r\n");
+ s.substr(2, headerEnd - 2)
+ .split("\r\n")
+ .forEach(function (str) {
+ // We're assuming UTF8 for now
+ let [name, value] = str.split(": ");
+ headers[name] = utf8decode(value);
+ });
+
+ let body = s.substring(headerEnd + 4, s.length - 2);
+ if (
+ !headers["Content-Type"] ||
+ headers["Content-Type"] == "text/plain"
+ ) {
+ // We're assuming UTF8 for now
+ body = utf8decode(body);
+ }
+ result.push({ headers, body });
+ });
+ }
+ if (
+ contentTypeParams[""] == "text/plain" &&
+ request.queryString == "plain"
+ ) {
+ result = utf8decode(requestBody);
+ }
+ if (
+ contentTypeParams[""] == "application/x-www-form-urlencoded" &&
+ request.queryString == "url"
+ ) {
+ result = requestBody;
+ }
+ } else if (request.method == "GET") {
+ result = request.queryString;
+ }
+
+ // Send response body
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+ response.write(utf8encode(JSON.stringify(result)));
+}
diff --git a/dom/html/test/forms/FAIL.html b/dom/html/test/forms/FAIL.html
new file mode 100644
index 0000000000..94e1707e85
--- /dev/null
+++ b/dom/html/test/forms/FAIL.html
@@ -0,0 +1 @@
+FAIL
diff --git a/dom/html/test/forms/PASS.html b/dom/html/test/forms/PASS.html
new file mode 100644
index 0000000000..7ef22e9a43
--- /dev/null
+++ b/dom/html/test/forms/PASS.html
@@ -0,0 +1 @@
+PASS
diff --git a/dom/html/test/forms/chrome.toml b/dom/html/test/forms/chrome.toml
new file mode 100644
index 0000000000..0f49518b9b
--- /dev/null
+++ b/dom/html/test/forms/chrome.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files = ["submit_invalid_file.sjs"]
+
+["test_autocompleteinfo.html"]
+
+["test_submit_invalid_file.html"]
diff --git a/dom/html/test/forms/file_double_submit.html b/dom/html/test/forms/file_double_submit.html
new file mode 100644
index 0000000000..44889f86bc
--- /dev/null
+++ b/dom/html/test/forms/file_double_submit.html
@@ -0,0 +1,11 @@
+<form action="PASS.html" method="POST"><input name="foo"></form>
+<button>clicky</button>
+
+<script>
+document.querySelector("button")
+ .addEventListener("click", () => {
+ let f = document.querySelector("form");
+ f.dispatchEvent(new Event("submit"));
+ f.submit();
+ });
+</script>
diff --git a/dom/html/test/forms/file_login_fields.html b/dom/html/test/forms/file_login_fields.html
new file mode 100644
index 0000000000..f23ee0ad6a
--- /dev/null
+++ b/dom/html/test/forms/file_login_fields.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ // Add an unload listener to bypass bfcache.
+ window.addEventListner("unload", _ => _);
+ </script>
+ </head>
+ <body>
+ <input id="un" />
+ <input id="pw1" type="password" />
+ <input id="pw2" />
+ <a id="navigate" href="?navigated">Navigate</a>
+ <a id="back" href="javascript:history.back()">Back</a>
+ </body>
+</html>
diff --git a/dom/html/test/forms/mochitest.toml b/dom/html/test/forms/mochitest.toml
new file mode 100644
index 0000000000..80d6d3530f
--- /dev/null
+++ b/dom/html/test/forms/mochitest.toml
@@ -0,0 +1,229 @@
+[DEFAULT]
+support-files = [
+ "save_restore_radio_groups.sjs",
+ "test_input_number_data.js",
+ "!/dom/html/test/reflect.js",
+ "FAIL.html",
+ "PASS.html",
+]
+prefs = ["formhelper.autozoom.force-disable.test-only=true"]
+
+["test_MozEditableElement_setUserInput.html"]
+
+["test_autocomplete.html"]
+
+["test_bug1039548.html"]
+
+["test_bug1283915.html"]
+
+["test_bug1286509.html"]
+
+["test_button_attributes_reflection.html"]
+
+["test_change_event.html"]
+
+["test_datalist_element.html"]
+
+["test_double_submit.html"]
+support-files = ["file_double_submit.html"]
+
+["test_form_attribute-1.html"]
+
+["test_form_attribute-2.html"]
+
+["test_form_attribute-3.html"]
+
+["test_form_attribute-4.html"]
+
+["test_form_attributes_reflection.html"]
+
+["test_form_named_getter_dynamic.html"]
+
+["test_formaction_attribute.html"]
+
+["test_formnovalidate_attribute.html"]
+
+["test_input_attributes_reflection.html"]
+
+["test_input_color_input_change_events.html"]
+
+["test_input_color_picker_datalist.html"]
+
+["test_input_color_picker_initial.html"]
+
+["test_input_color_picker_popup.html"]
+
+["test_input_color_picker_update.html"]
+
+["test_input_date_bad_input.html"]
+
+["test_input_date_key_events.html"]
+
+["test_input_datetime_calendar_button.html"]
+
+["test_input_datetime_disabled_focus.html"]
+
+["test_input_datetime_focus_blur.html"]
+
+["test_input_datetime_focus_blur_events.html"]
+
+["test_input_datetime_focus_state.html"]
+
+["test_input_datetime_hidden.html"]
+
+["test_input_datetime_input_change_events.html"]
+
+["test_input_datetime_readonly.html"]
+
+["test_input_datetime_reset_default_value_input_change_event.html"]
+
+["test_input_datetime_tabindex.html"]
+
+["test_input_defaultValue.html"]
+
+["test_input_email.html"]
+
+["test_input_event.html"]
+
+["test_input_file_picker.html"]
+
+["test_input_hasBeenTypePassword.html"]
+
+["test_input_hasBeenTypePassword_navigation.html"]
+support-files = ["file_login_fields.html"]
+
+["test_input_list_attribute.html"]
+
+["test_input_number_focus.html"]
+
+["test_input_number_key_events.html"]
+
+["test_input_number_l10n.html"]
+
+["test_input_number_mouse_events.html"]
+# Not run on Firefox for Android where the spin buttons are hidden:
+skip-if = [
+ "os == 'android'",
+ "os == 'mac' && debug", # Bug 1484442
+]
+
+["test_input_number_placeholder_shown.html"]
+
+["test_input_number_rounding.html"]
+
+["test_input_number_validation.html"]
+
+["test_input_password_click_show_password_button.html"]
+
+["test_input_password_show_password_button.html"]
+
+["test_input_radio_indeterminate.html"]
+
+["test_input_radio_radiogroup.html"]
+
+["test_input_radio_required.html"]
+
+["test_input_range_attr_order.html"]
+
+["test_input_range_key_events.html"]
+
+["test_input_range_mouse_and_touch_events.html"]
+
+["test_input_range_rounding.html"]
+
+["test_input_sanitization.html"]
+
+["test_input_setting_value.html"]
+
+["test_input_textarea_set_value_no_scroll.html"]
+
+["test_input_time_key_events.html"]
+
+["test_input_time_sec_millisec_field.html"]
+
+["test_input_types_pref.html"]
+
+["test_input_typing_sanitization.html"]
+
+["test_input_untrusted_key_events.html"]
+
+["test_input_url.html"]
+
+["test_interactive_content_in_label.html"]
+
+["test_interactive_content_in_summary.html"]
+
+["test_label_control_attribute.html"]
+
+["test_label_input_controls.html"]
+
+["test_max_attribute.html"]
+
+["test_maxlength_attribute.html"]
+
+["test_meter_element.html"]
+
+["test_meter_pseudo-classes.html"]
+
+["test_min_attribute.html"]
+
+["test_minlength_attribute.html"]
+
+["test_mozistextfield.html"]
+
+["test_novalidate_attribute.html"]
+
+["test_option_disabled.html"]
+
+["test_option_index_attribute.html"]
+
+["test_option_text.html"]
+
+["test_output_element.html"]
+
+["test_pattern_attribute.html"]
+
+["test_preserving_metadata_between_reloads.html"]
+
+["test_progress_element.html"]
+
+["test_radio_in_label.html"]
+
+["test_radio_radionodelist.html"]
+
+["test_reportValidation_preventDefault.html"]
+
+["test_required_attribute.html"]
+
+["test_restore_form_elements.html"]
+
+["test_save_restore_custom_elements.html"]
+support-files = ["save_restore_custom_elements_sample.html"]
+
+["test_save_restore_radio_groups.html"]
+
+["test_select_change_event.html"]
+skip-if = ["os == 'mac'"]
+
+["test_select_input_change_event.html"]
+skip-if = ["os == 'mac'"]
+
+["test_select_selectedOptions.html"]
+
+["test_select_validation.html"]
+
+["test_set_range_text.html"]
+
+["test_step_attribute.html"]
+
+["test_stepup_stepdown.html"]
+
+["test_textarea_attributes_reflection.html"]
+
+["test_validation.html"]
+
+["test_validation_not_in_doc.html"]
+
+["test_valueasdate_attribute.html"]
+
+["test_valueasnumber_attribute.html"]
diff --git a/dom/html/test/forms/save_restore_custom_elements_sample.html b/dom/html/test/forms/save_restore_custom_elements_sample.html
new file mode 100644
index 0000000000..75dc4c388d
--- /dev/null
+++ b/dom/html/test/forms/save_restore_custom_elements_sample.html
@@ -0,0 +1,43 @@
+<script>
+ class CEBase extends HTMLElement {
+ static formAssociated = true;
+ constructor() {
+ super();
+ this.internals = this.attachInternals();
+ this.state_ = undefined;
+ }
+ formStateRestoreCallback(state, reason) {
+ if (reason == "restore") {
+ this.state_ = state;
+ }
+ }
+ set(state, value) {
+ this.state_ = state;
+ this.value_ = value;
+ this.internals.setFormValue(value, state);
+ }
+ get state() {
+ return this.state_;
+ }
+ get value() {
+ return this.value_;
+ }
+ }
+
+ customElements.define("c-e", class extends CEBase {});
+</script>
+<form>
+ <c-e id="custom0"></c-e>
+ <c-e id="custom1"></c-e>
+ <c-e id="custom2"></c-e>
+ <c-e id="custom3"></c-e>
+ <c-e id="custom4"></c-e>
+ <upgraded-ce id="upgraded0"></upgraded-ce>
+ <upgraded-ce id="upgraded1"></upgraded-ce>
+ <upgraded-ce id="upgraded2"></upgraded-ce>
+ <upgraded-ce id="upgraded3"></upgraded-ce>
+ <upgraded-ce id="upgraded4"></upgraded-ce>
+</form>
+<script>
+ customElements.define("upgraded-ce", class extends CEBase {});
+</script>
diff --git a/dom/html/test/forms/save_restore_radio_groups.sjs b/dom/html/test/forms/save_restore_radio_groups.sjs
new file mode 100644
index 0000000000..b4c9c4401a
--- /dev/null
+++ b/dom/html/test/forms/save_restore_radio_groups.sjs
@@ -0,0 +1,48 @@
+var pages = [
+ "<!DOCTYPE html>" +
+ "<html><body>" +
+ "<form>" +
+ "<input name='a' type='radio' checked><input name='a' type='radio'><input name='a' type='radio'>" +
+ "</form>" +
+ "</body></html>",
+ "<!DOCTYPE html>" +
+ "<html><body>" +
+ "<form>" +
+ "<input name='a' type='radio'><input name='a' type='radio' checked><input name='a' type='radio'>" +
+ "</form>" +
+ "</body></html>",
+];
+
+/**
+ * This SJS is going to send the same page the two first times it will be called
+ * and another page the two following times. After that, the response will have
+ * no content.
+ * The use case is to have two iframes using this SJS and both being reloaded
+ * once.
+ */
+
+function handleRequest(request, response) {
+ var counter = +getState("counter"); // convert to number; +"" === 0
+
+ response.setStatusLine(request.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Cache-Control", "no-cache");
+
+ switch (counter) {
+ case 0:
+ case 1:
+ response.write(pages[0]);
+ break;
+ case 2:
+ case 3:
+ response.write(pages[1]);
+ break;
+ }
+
+ // When we finish the test case we need to reset the counter
+ if (counter == 3) {
+ setState("counter", "0");
+ } else {
+ setState("counter", "" + ++counter);
+ }
+}
diff --git a/dom/html/test/forms/submit_invalid_file.sjs b/dom/html/test/forms/submit_invalid_file.sjs
new file mode 100644
index 0000000000..3b4b576ec6
--- /dev/null
+++ b/dom/html/test/forms/submit_invalid_file.sjs
@@ -0,0 +1,13 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/html");
+ response.setHeader("Cache-Control", "no-cache");
+
+ var result = {};
+ request.bodyInputStream.search("testfile", true, result, {});
+ if (result.value) {
+ response.write("SUCCESS");
+ } else {
+ response.write("FAIL");
+ }
+}
diff --git a/dom/html/test/forms/test_MozEditableElement_setUserInput.html b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
new file mode 100644
index 0000000000..06380776f6
--- /dev/null
+++ b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
@@ -0,0 +1,581 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for MozEditableElement.setUserInput()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content"></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+// eslint-disable-next-line complexity
+SimpleTest.waitForFocus(async () => {
+ const kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input");
+
+ let content = document.getElementById("content");
+ /**
+ * Test structure:
+ * element: the tag name to create.
+ * type: the type attribute value for the element. If unnecessary omit it.
+ * input: the values calling setUserInput() with.
+ * before: used when calling setUserInput() before the element gets focus.
+ * after: used when calling setUserInput() after the element gets focus.
+ * result: the results of calling setUserInput().
+ * before: the element's expected value of calling setUserInput() before the element gets focus.
+ * after: the element's expected value of calling setUserInput() after the element gets focus.
+ * fireBeforeInputEvent: true if "beforeinput" event should be fired. Otherwise, false.
+ * fireInputEvent: true if "input" event should be fired. Otherwise, false.
+ */
+ for (let test of [{element: "input", type: "hidden",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "input", type: "text",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "search",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "tel",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "url",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "email",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "password",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ // "date" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "date",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "month" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "month",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "week" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "week",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "time" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "time",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "datetime-local" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "datetime-local",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ {element: "input", type: "number",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}},
+ {element: "input", type: "range",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "color" does not support setUserInput, but dispatches "input" event...
+ {element: "input", type: "color",
+ input: {before: "#5C5C5C", after: "#FFFFFF"},
+ result: {before: "#5c5c5c", after:"#ffffff", fireBeforeInputEvent: false, fireInputEvent: true}},
+ {element: "input", type: "checkbox",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ {element: "input", type: "radio",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}},
+ // "file" is not supported by setUserInput? But there is a path...
+ {element: "input", type: "file",
+ input: {before: "3", after: "6"},
+ result: {before: "", after:"", fireBeforeInputEvent: false, fireInputEvent: true}},
+ {element: "input", type: "submit",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "input", type: "image",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "input", type: "reset",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "input", type: "button",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}},
+ {element: "textarea",
+ input: {before: "3", after: "6"},
+ result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}]) {
+ let tag =
+ test.type !== undefined ? `<${test.element} type="${test.type}">` :
+ `<${test.element}>`;
+ content.innerHTML =
+ test.element !== "input" ? tag : `${tag}</${test.element}>`;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+
+ let inputEvents = [], beforeInputEvents = [];
+ function onBeforeInput(aEvent) {
+ beforeInputEvents.push(aEvent);
+ }
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ }
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+
+ // Before setting focus, editor of the element may have not been created yet.
+ let previousValue = target.value;
+ SpecialPowers.wrap(target).setUserInput(test.input.before);
+ if (target.value == previousValue && test.result.before != previousValue) {
+ todo_is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`);
+ } else {
+ is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`);
+ }
+ if (target.value == previousValue) {
+ if (test.type === "date" || test.type === "time" || test.type === "datetime-local") {
+ todo_is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ } else {
+ is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ } else {
+ if (!test.result.fireBeforeInputEvent) {
+ is(beforeInputEvents.length, 0,
+ `No "beforeinput" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ } else {
+ is(beforeInputEvents.length, 1,
+ `Only one "beforeinput" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ if (!test.result.fireInputEvent) {
+ // HTML spec defines that "input" elements whose type are "hidden",
+ // "submit", "image", "reset" and "button" shouldn't fire input event
+ // when its value is changed.
+ // XXX Perhaps, we shouldn't support setUserInput() with such types.
+ if (test.type === "hidden" ||
+ test.type === "submit" ||
+ test.type === "image" ||
+ test.type === "reset" ||
+ test.type === "button") {
+ todo_is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ } else {
+ is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ } else {
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ }
+ if (inputEvents.length) {
+ if (SpecialPowers.wrap(target).isInputEventTarget) {
+ if (test.type === "time") {
+ todo(inputEvents[0] instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ } else {
+ if (beforeInputEvents.length && test.result.fireBeforeInputEvent) {
+ is(beforeInputEvents[0].cancelable, kSetUserInputCancelable,
+ `"beforeinput" event for "insertReplacementText" should be cancelable when setUserInput("${test.input.before}") is called before ${tag} gets focus unless it's suppressed by the pref`);
+ is(beforeInputEvents[0].inputType, "insertReplacementText",
+ `inputType of "beforeinput"event should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(beforeInputEvents[0].data, test.input.before,
+ `data of "beforeinput" event should be "${test.input.before}" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(beforeInputEvents[0].dataTransfer, null,
+ `dataTransfer of "beforeinput" event should be null when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(beforeInputEvents[0].getTargetRanges().length, 0,
+ `getTargetRanges() of "beforeinput" event should return empty array when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ ok(inputEvents[0] instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(inputEvents[0].inputType, "insertReplacementText",
+ `inputType of "input" event should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(inputEvents[0].data, test.input.before,
+ `data of "input" event should be "${test.input.before}" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(inputEvents[0].dataTransfer, null,
+ `dataTransfer of "input" event should be null when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ is(inputEvents[0].getTargetRanges().length, 0,
+ `getTargetRanges() of "input" event should return empty array when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ } else {
+ ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
+ `"input" event should be dispatched with Event interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+ }
+ is(inputEvents[0].cancelable, false,
+ `"input" event should be never cancelable (${tag}, before getting focus)`);
+ is(inputEvents[0].bubbles, true,
+ `"input" event should always bubble (${tag}, before getting focus)`);
+ }
+
+ beforeInputEvents = [];
+ inputEvents = [];
+ target.focus();
+ previousValue = target.value;
+ SpecialPowers.wrap(target).setUserInput(test.input.after);
+ if (target.value == previousValue && test.result.after != previousValue) {
+ todo_is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`);
+ } else {
+ is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`);
+ }
+ if (target.value == previousValue) {
+ if (test.type === "date" || test.type === "time" || test.type === "datetime-local") {
+ todo_is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ } else {
+ is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ } else {
+ if (!test.result.fireBeforeInputEvent) {
+ is(beforeInputEvents.length, 0,
+ `No "beforeinput" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ } else {
+ is(beforeInputEvents.length, 1,
+ `Only one "beforeinput" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ if (!test.result.fireInputEvent) {
+ // HTML spec defines that "input" elements whose type are "hidden",
+ // "submit", "image", "reset" and "button" shouldn't fire input event
+ // when its value is changed.
+ // XXX Perhaps, we shouldn't support setUserInput() with such types.
+ if (test.type === "hidden" ||
+ test.type === "submit" ||
+ test.type === "image" ||
+ test.type === "reset" ||
+ test.type === "button") {
+ todo_is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ } else {
+ is(inputEvents.length, 0,
+ `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ } else {
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ }
+ if (inputEvents.length) {
+ if (SpecialPowers.wrap(target).isInputEventTarget) {
+ if (test.type === "time") {
+ todo(inputEvents[0] instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ } else {
+ if (beforeInputEvents.length && test.result.fireBeforeInputEvent) {
+ is(beforeInputEvents[0].cancelable, kSetUserInputCancelable,
+ `"beforeinput" event should be cancelable when setUserInput("${test.input.after}") is called after ${tag} gets focus unless it's suppressed by the pref`);
+ is(beforeInputEvents[0].inputType, "insertReplacementText",
+ `inputType of "beforeinput" event should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(beforeInputEvents[0].data, test.input.after,
+ `data of "beforeinput" should be "${test.input.after}" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(beforeInputEvents[0].dataTransfer, null,
+ `dataTransfer of "beforeinput" should be null when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(beforeInputEvents[0].getTargetRanges().length, 0,
+ `getTargetRanges() of "beforeinput" should return empty array when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ ok(inputEvents[0] instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(inputEvents[0].inputType, "insertReplacementText",
+ `inputType of "input" event should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(inputEvents[0].data, test.input.after,
+ `data of "input" event should be "${test.input.after}" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(inputEvents[0].dataTransfer, null,
+ `dataTransfer of "input" event should be null when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ is(inputEvents[0].getTargetRanges().length, 0,
+ `getTargetRanges() of "input" event should return empty array when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ } else {
+ ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
+ `"input" event should be dispatched with Event interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+ }
+ is(inputEvents[0].cancelable, false,
+ `"input" event should be never cancelable (${tag}, after getting focus)`);
+ is(inputEvents[0].bubbles, true,
+ `"input" event should always bubble (${tag}, after getting focus)`);
+ }
+
+ target.removeEventListener("input", onInput);
+ }
+
+ function testValidationMessage(aType, aInvalidValue, aValidValue) {
+ let tag = `<input type="${aType}">`
+ content.innerHTML = tag;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+
+ let inputEvents = [];
+ let validationMessage = "";
+
+ function reset() {
+ inputEvents = [];
+ validationMessage = "";
+ }
+
+ function onInput(aEvent) {
+ inputEvents.push(aEvent);
+ validationMessage = aEvent.target.validationMessage;
+ }
+ target.addEventListener("input", onInput);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+ isnot(validationMessage, "",
+ `${tag}.validationMessage should not be empty when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+ ok(target.matches(":invalid"),
+ `The target should have invalid pseudo class when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aValidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${aValidValue}") is called before ${tag} gets focus`);
+ is(validationMessage, "",
+ `${tag}.validationMessage should be empty when setUserInput("${aValidValue}") is called before ${tag} gets focus`);
+ ok(!target.matches(":invalid"),
+ `The target shouldn't have invalid pseudo class when setUserInput("${aValidValue}") is called before ${tag} gets focus`);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+ isnot(validationMessage, "",
+ `${tag}.validationMessage should not be empty again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+ ok(target.matches(":invalid"),
+ `The target should have invalid pseudo class again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`);
+
+ target.value = "";
+ target.focus();
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+ isnot(validationMessage, "",
+ `${tag}.validationMessage should not be empty when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+ ok(target.matches(":invalid"),
+ `The target should have invalid pseudo class when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aValidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched when setUserInput("${aValidValue}") is called after ${tag} gets focus`);
+ is(validationMessage, "",
+ `${tag}.validationMessage should be empty when setUserInput("${aValidValue}") is called after ${tag} gets focus`);
+ ok(!target.matches(":invalid"),
+ `The target shouldn't have invalid pseudo class when setUserInput("${aValidValue}") is called after ${tag} gets focus`);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aInvalidValue);
+ is(inputEvents.length, 1,
+ `Only one "input" event should be dispatched again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+ isnot(validationMessage, "",
+ `${tag}.validationMessage should not be empty again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+ ok(target.matches(":invalid"),
+ `The target should have invalid pseudo class again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`);
+
+ target.removeEventListener("input", onInput);
+ }
+ testValidationMessage("email", "f", "foo@example.com");
+
+ function testValueMissing(aType, aValidValue) {
+ let tag = aType === "textarea" ? "<textarea required>" : `<input type="${aType}" required>`;
+ content.innerHTML = `${tag}${aType === "textarea" ? "</textarea>" : ""}`;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+
+ let inputEvents = [], beforeInputEvents = [];
+ function reset() {
+ beforeInputEvents = [];
+ inputEvents = [];
+ }
+
+ function onBeforeInput(aEvent) {
+ aEvent.validity = aEvent.target.checkValidity();
+ beforeInputEvents.push(aEvent);
+ }
+ function onInput(aEvent) {
+ aEvent.validity = aEvent.target.checkValidity();
+ inputEvents.push(aEvent);
+ }
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aValidValue);
+ is(beforeInputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "beforeinput" event (before gets focus)`);
+ if (beforeInputEvents.length) {
+ is(beforeInputEvents[0].validity, false,
+ `The ${tag} should be invalid at "beforeinput" event (before gets focus)`);
+ }
+ is(inputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "input" event (before gets focus)`);
+ if (inputEvents.length) {
+ is(inputEvents[0].validity, true,
+ `The ${tag} should be valid at "input" event (before gets focus)`);
+ }
+
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+
+ content.innerHTML = "";
+ content.scrollTop; // Flush pending layout.
+ content.innerHTML = `${tag}${aType === "textarea" ? "</textarea>" : ""}`;
+ content.scrollTop; // Flush pending layout.
+ target = content.firstChild;
+
+ target.focus();
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+
+ reset();
+ SpecialPowers.wrap(target).setUserInput(aValidValue);
+ is(beforeInputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "beforeinput" event (after gets focus)`);
+ if (beforeInputEvents.length) {
+ is(beforeInputEvents[0].validity, false,
+ `The ${tag} should be invalid at "beforeinput" event (after gets focus)`);
+ }
+ is(inputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "input" event (after gets focus)`);
+ if (inputEvents.length) {
+ is(inputEvents[0].validity, true,
+ `The ${tag} should be valid at "input" event (after gets focus)`);
+ }
+
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+ }
+ testValueMissing("text", "abc");
+ testValueMissing("password", "abc");
+ testValueMissing("textarea", "abc");
+ testValueMissing("email", "foo@example.com");
+ testValueMissing("url", "https://example.com/");
+
+ function testEditorValueAtEachEvent(aType) {
+ let tag = aType === "textarea" ? "<textarea>" : `<input type="${aType}">`
+ let closeTag = aType === "textarea" ? "</textarea>" : "";
+ content.innerHTML = `${tag}${closeTag}`;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+ target.value = "Old Value";
+ let description = `Setting new value of ${tag} before setting focus: `;
+ let onBeforeInput = (aEvent) => {
+ is(target.value, "Old Value",
+ `${description}The value should not have been modified at "beforeinput" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`);
+ };
+ let onInput = (aEvent) => {
+ is(target.value, "New Value",
+ `${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`);
+ };
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+ SpecialPowers.wrap(target).setUserInput("New Value");
+
+ description = `Setting new value of ${tag} after setting focus: `;
+ target.value = "Old Value";
+ target.focus();
+ SpecialPowers.wrap(target).setUserInput("New Value");
+
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+
+ // FYI: This is not realistic situation because we should do nothing
+ // while user composing IME.
+ // TODO: TextControlState should stop returning setting value as the value
+ // while committing composition.
+ description = `Setting new value of ${tag} during composition: `;
+ target.value = "";
+ target.focus();
+ synthesizeCompositionChange({
+ composition: {
+ string: "composition string",
+ clauses: [{length: 18, attr: COMPOSITION_ATTR_RAW_CLAUSE}],
+ },
+ caret: {start: 18, length: 0},
+ });
+ let onCompositionUpdate = (aEvent) => {
+ todo_is(target.value, "composition string",
+ `${description}The value should not have been modified at "compositionupdate" event yet (data: "${aEvent.data}")`);
+ };
+ let onCompositionEnd = (aEvent) => {
+ todo_is(target.value, "composition string",
+ `${description}The value should not have been modified at "compositionupdate" event yet (data: "${aEvent.data}")`);
+ };
+ onBeforeInput = (aEvent) => {
+ if (aEvent.inputType === "insertCompositionText") {
+ todo_is(target.value, "composition string",
+ `${description}The value should not have been modified at "beforeinput" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`);
+ } else {
+ is(target.value, "composition string",
+ `${description}The value should not have been modified at "beforeinput" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`);
+ }
+ };
+ onInput = (aEvent) => {
+ if (aEvent.inputType === "insertCompositionText") {
+ todo_is(target.value, "composition string",
+ `${description}The value should not have been modified at "input" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`);
+ } else {
+ is(target.value, "New Value",
+ `${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`);
+ }
+ };
+ target.addEventListener("compositionupdate", onCompositionUpdate);
+ target.addEventListener("compositionend", onCompositionEnd);
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+ SpecialPowers.wrap(target).setUserInput("New Value");
+ target.removeEventListener("compositionupdate", onCompositionUpdate);
+ target.removeEventListener("compositionend", onCompositionEnd);
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+ }
+ testEditorValueAtEachEvent("text");
+ testEditorValueAtEachEvent("textarea");
+
+ async function testBeforeInputCancelable(aType) {
+ let tag = aType === "textarea" ? "<textarea>" : `<input type="${aType}">`
+ let closeTag = aType === "textarea" ? "</textarea>" : "";
+ for (const kShouldBeCancelable of [true, false]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.input_event.allow_to_cancel_set_user_input", kShouldBeCancelable]],
+ });
+
+ content.innerHTML = `${tag}${closeTag}`;
+ content.scrollTop; // Flush pending layout.
+ let target = content.firstChild;
+ target.value = "Old Value";
+ let description = `Setting new value of ${tag} before setting focus (the pref ${kShouldBeCancelable ? "allows" : "disallows"} to cancel beforeinput): `;
+ let onBeforeInput = (aEvent) => {
+ is(aEvent.cancelable, kShouldBeCancelable,
+ `${description}The "beforeinput" event should be ${kShouldBeCancelable ? "cancelable" : "not be cancelable due to suppressed by the pref"}`);
+ };
+ let onInput = (aEvent) => {
+ is(aEvent.cancelable, false,
+ `${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`);
+ };
+ target.addEventListener("beforeinput", onBeforeInput);
+ target.addEventListener("input", onInput);
+ SpecialPowers.wrap(target).setUserInput("New Value");
+
+ description = `Setting new value of ${tag} after setting focus (the pref ${kShouldBeCancelable ? "allows" : "disallows"} to cancel beforeinput): `;
+ target.value = "Old Value";
+ target.focus();
+ SpecialPowers.wrap(target).setUserInput("New Value");
+
+ target.removeEventListener("beforeinput", onBeforeInput);
+ target.removeEventListener("input", onInput);
+ }
+
+ await SpecialPowers.clearUserPref({
+ clear: [["dom.input_event.allow_to_cancel_set_user_input"]],
+ });
+ }
+ await testBeforeInputCancelable("text");
+ await testBeforeInputCancelable("textarea");
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_autocomplete.html b/dom/html/test/forms/test_autocomplete.html
new file mode 100644
index 0000000000..c98be94eea
--- /dev/null
+++ b/dom/html/test/forms/test_autocomplete.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<html>
+<!--
+Test @autocomplete on <input>/<select>/<textarea>
+-->
+<head>
+ <title>Test for @autocomplete</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+var values = [
+ // @autocomplete content attribute, expected IDL attribute value
+
+ // Missing or empty attribute
+ [undefined, ""],
+ ["", ""],
+
+ // One token
+ ["on", "on"],
+ ["On", "on"],
+ ["off", "off"],
+ ["OFF", "off"],
+ ["name", "name"],
+ [" name ", "name"],
+ ["username", "username"],
+ [" username ", "username"],
+ ["cc-csc", ""],
+ ["one-time-code", ""],
+ ["language", ""],
+ [" language ", ""],
+ ["tel-extension", ""],
+ ["foobar", ""],
+ ["section-blue", ""],
+ [" WEBAUTHN ", "webauthn"],
+
+ // One token + WebAuthn credential type
+ ["on webauthn", ""],
+ ["off webauthn", ""],
+ ["webauthn webauthn", ""],
+ ["username WebAuthn", "username webauthn"],
+ ["current-PASSWORD webauthn", "current-password webauthn"],
+
+ // Two tokens
+ ["on off", ""],
+ ["off on", ""],
+ ["username tel", ""],
+ ["tel username ", ""],
+ [" username tel ", ""],
+ ["tel mobile", ""],
+ ["tel shipping", ""],
+ ["shipping tel", "shipping tel"],
+ ["shipPING tel", "shipping tel"],
+ ["mobile tel", "mobile tel"],
+ [" MoBiLe TeL ", "mobile tel"],
+ ["pager impp", ""],
+ ["fax tel-extension", ""],
+ ["XXX tel", ""],
+ ["XXX username", ""],
+ ["name section-blue", ""],
+ ["scetion-blue cc-name", ""],
+ ["pager language", ""],
+ ["fax url", ""],
+ ["section-blue name", "section-blue name"],
+ ["section-blue tel", "section-blue tel"],
+ ["webauthn username", ""],
+
+ // Two tokens + WebAuthn credential type
+ ["fax url webauthn", ""],
+ ["shipping tel webauthn", "shipping tel webauthn"],
+
+ // Three tokens
+ ["billing invalid tel", ""],
+ ["___ mobile tel", ""],
+ ["mobile foo tel", ""],
+ ["mobile tel foo", ""],
+ ["tel mobile billing", ""],
+ ["billing mobile tel", "billing mobile tel"],
+ [" BILLing MoBiLE tEl ", "billing mobile tel"],
+ ["billing home tel", "billing home tel"],
+ ["home section-blue tel", ""],
+ ["setion-blue work email", ""],
+ ["section-blue home address-level2", ""],
+ ["section-blue shipping name", "section-blue shipping name"],
+ ["section-blue mobile tel", "section-blue mobile tel"],
+ ["shipping webauthn tel", ""],
+
+ // Three tokens + WebAuthn credential type
+ ["invalid mobile tel webauthn", ""],
+ ["section-blue shipping name webauthn", "section-blue shipping name webauthn"],
+
+ // Four tokens
+ ["billing billing mobile tel", ""],
+ ["name section-blue shipping home", ""],
+ ["secti shipping work address-line1", ""],
+ ["section-blue shipping home name", ""],
+ ["section-blue shipping mobile tel", "section-blue shipping mobile tel"],
+ ["section-blue webauthn mobile tel", ""],
+
+ // Four tokens + WebAuthn credential type
+ ["section-blue shipping home name webauthn", ""],
+ ["section-blue shipping mobile tel webauthn", "section-blue shipping mobile tel webauthn"],
+
+ // Five tokens (invalid)
+ ["billing billing billing mobile tel", ""],
+ ["section-blue section-blue billing mobile tel", ""],
+ ["section-blue section-blue billing webauthn tel", ""],
+
+ // Five tokens + WebAuthn credential type (invalid)
+ ["billing billing billing mobile tel webauthn", ""],
+];
+
+var types = [undefined, "hidden", "text", "search"]; // Valid types for all non-multiline hints.
+
+function checkAutocompleteValues(field, type) {
+ for (var test of values) {
+ if (typeof(test[0]) === "undefined")
+ field.removeAttribute("autocomplete");
+ else
+ field.setAttribute("autocomplete", test[0]);
+ is(field.autocomplete, test[1], "Checking @autocomplete for @type=" + type + " of: " + test[0]);
+ is(field.autocomplete, test[1], "Checking cached @autocomplete for @type=" + type + " of: " + test[0]);
+ }
+}
+
+function start() {
+ var inputField = document.getElementById("input-field");
+ for (var type of types) {
+ // Switch the input type
+ if (typeof(type) === "undefined")
+ inputField.removeAttribute("type");
+ else
+ inputField.type = type;
+ checkAutocompleteValues(inputField, type || "");
+ }
+
+ var selectField = document.getElementById("select-field");
+ checkAutocompleteValues(selectField, "select");
+
+ var textarea = document.getElementById("textarea");
+ checkAutocompleteValues(textarea, "textarea");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["dom.forms.autocomplete.formautofill", true]]}, start);
+</script>
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form>
+ <input id="input-field" />
+ <select id="select-field" />
+ <textarea id="textarea"></textarea>
+ </form>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_autocompleteinfo.html b/dom/html/test/forms/test_autocompleteinfo.html
new file mode 100644
index 0000000000..a3357ac8de
--- /dev/null
+++ b/dom/html/test/forms/test_autocompleteinfo.html
@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<html>
+<!--
+Test getAutocompleteInfo() on <input> and <select>
+-->
+<head>
+ <title>Test for getAutocompleteInfo()</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form>
+ <input id="input"/>
+ <select id="select" />
+ </form>
+</div>
+<pre id="test">
+<script>
+"use strict";
+
+var values = [
+ // Missing or empty attribute
+ [undefined, {}, ""],
+ ["", {}, ""],
+
+ // One token
+ ["on", {fieldName: "on" }, "on"],
+ ["On", {fieldName: "on" }, "on"],
+ ["off", {fieldName: "off", canAutomaticallyPersist: false}, "off" ],
+ ["name", {fieldName: "name" }, "name"],
+ [" name ", {fieldName: "name" }, "name"],
+ ["username", {fieldName: "username"}, "username"],
+ [" username ", {fieldName: "username"}, "username"],
+ ["current-password", {fieldName: "current-password", canAutomaticallyPersist: false}, "current-password"],
+ ["new-password", {fieldName: "new-password", canAutomaticallyPersist: false}, "new-password"],
+ ["cc-number", {fieldName: "cc-number", canAutomaticallyPersist: false}, "cc-number"],
+ ["cc-csc", {fieldName: "cc-csc", canAutomaticallyPersist: false}, ""],
+ ["one-time-code", {fieldName: "one-time-code", canAutomaticallyPersist: false}, ""],
+ ["language", {fieldName: "language"}, ""],
+ [" language ", {fieldName: "language"}, ""],
+ ["tel-extension", {fieldName: "tel-extension"}, ""],
+ ["foobar", {}, ""],
+ ["section-blue", {}, ""],
+ [" WEBAUTHN ", {fieldName: "webauthn", credentialType: "webauthn"}, "webauthn"],
+
+ // One token + WebAuthn credential type
+ ["on webauthn", {}, ""],
+ ["off webauthn", {}, ""],
+ ["webauthn webauthn", {}, ""],
+ ["username WebAuthn", {fieldName: "username", credentialType: "webauthn"}, "username webauthn"],
+ ["current-PASSWORD webauthn", {fieldName: "current-password", credentialType: "webauthn", canAutomaticallyPersist: false}, "current-password webauthn"],
+
+ // Two tokens
+ ["on off", {}, ""],
+ ["off on", {}, ""],
+ ["username tel", {}, ""],
+ ["tel username ", {}, ""],
+ [" username tel ", {}, ""],
+ ["tel mobile", {}, ""],
+ ["tel shipping", {}, ""],
+ ["shipping tel", {addressType: "shipping", fieldName: "tel"}, "shipping tel"],
+ ["shipPING tel", {addressType: "shipping", fieldName: "tel"}, "shipping tel"],
+ ["mobile tel", {contactType: "mobile", fieldName: "tel"}, "mobile tel"],
+ [" MoBiLe TeL ", {contactType: "mobile", fieldName: "tel"}, "mobile tel"],
+ ["pager impp", {contactType: "pager", fieldName: "impp"}, ""],
+ ["fax tel-extension", {contactType: "fax", fieldName: "tel-extension"}, ""],
+ ["XXX tel", {}, ""],
+ ["XXX username", {}, ""],
+ ["name section-blue", {}, ""],
+ ["scetion-blue cc-name", {}, ""],
+ ["pager language", {}, ""],
+ ["fax url", {}, ""],
+ ["section-blue name", {section: "section-blue", fieldName: "name"}, "section-blue name"],
+ ["section-blue tel", {section: "section-blue", fieldName: "tel"}, "section-blue tel"],
+ ["webauthn username", {}, ""],
+
+ // Two tokens + WebAuthn credential type
+ ["fax url webauthn", {}, ""],
+ ["shipping tel webauthn", {addressType: "shipping", fieldName: "tel", credentialType: "webauthn"}, "shipping tel webauthn"],
+
+ // Three tokens
+ ["billing invalid tel", {}, ""],
+ ["___ mobile tel", {}, ""],
+ ["mobile foo tel", {}, ""],
+ ["mobile tel foo", {}, ""],
+ ["tel mobile billing", {}, ""],
+ ["billing mobile tel", {addressType: "billing", contactType: "mobile", fieldName: "tel"}, "billing mobile tel"],
+ [" BILLing MoBiLE tEl ", {addressType: "billing", contactType: "mobile", fieldName: "tel"}, "billing mobile tel"],
+ ["billing home tel", {addressType: "billing", contactType: "home", fieldName: "tel"}, "billing home tel"],
+ ["home section-blue tel", {}, ""],
+ ["setion-blue work email", {}, ""],
+ ["section-blue home address-level2", {}, ""],
+ ["section-blue shipping name", {section: "section-blue", addressType: "shipping", fieldName: "name"}, "section-blue shipping name"],
+ ["section-blue mobile tel", {section: "section-blue", contactType: "mobile", fieldName: "tel"}, "section-blue mobile tel"],
+ ["shipping webauthn tel", {}, ""],
+
+ // Three tokens + WebAuthn credential type
+ ["invalid mobile tel webauthn", {}, ""],
+ ["section-blue shipping name webauthn", {section: "section-blue", addressType: "shipping", fieldName: "name", credentialType: "webauthn"}, "section-blue shipping name webauthn"],
+
+ // Four tokens
+ ["billing billing mobile tel", {}, ""],
+ ["name section-blue shipping home", {}, ""],
+ ["secti shipping work address-line1", {}, ""],
+ ["section-blue shipping home name", {}, ""],
+ ["section-blue shipping mobile tel", {section: "section-blue", addressType: "shipping", contactType: "mobile", fieldName: "tel"}, "section-blue shipping mobile tel"],
+ ["section-blue webauthn mobile tel", {}, ""],
+
+ // Four tokens + WebAuthn credential type
+ ["section-blue shipping home name webauthn", {}, ""],
+ ["section-blue shipping mobile tel webauthn", {section: "section-blue", addressType: "shipping", contactType: "mobile", fieldName: "tel", credentialType: "webauthn"}, "section-blue shipping mobile tel webauthn"],
+
+ // Five tokens (invalid)
+ ["billing billing billing mobile tel", {}, ""],
+ ["section-blue section-blue billing mobile tel", {}, ""],
+ ["section-blue section-blue billing webauthn tel", {}, ""],
+
+ // Five tokens + WebAuthn credential type (invalid)
+ ["billing billing billing mobile tel webauthn", {}, ""],
+];
+
+var autocompleteInfoFieldIds = ["input", "select"];
+var autocompleteEnabledTypes = ["hidden", "text", "search", "url", "tel",
+ "email", "password", "date", "time", "number",
+ "range", "color"];
+var autocompleteDisabledTypes = ["reset", "submit", "image", "button", "radio",
+ "checkbox", "file"];
+
+function testInputTypes() {
+ let field = document.getElementById("input");
+
+ for (var type of autocompleteEnabledTypes) {
+ testAutocomplete(field, type, true);
+ }
+
+ for (var type of autocompleteDisabledTypes) {
+ testAutocomplete(field, type, false);
+ }
+
+ // Clear input type attribute.
+ field.removeAttribute("type");
+}
+
+function testAutocompleteInfoValue(aEnabled) {
+ for (var fieldId of autocompleteInfoFieldIds) {
+ let field = document.getElementById(fieldId);
+
+ for (var test of values) {
+ if (typeof(test[0]) === "undefined")
+ field.removeAttribute("autocomplete");
+ else
+ field.setAttribute("autocomplete", test[0]);
+
+ var info = field.getAutocompleteInfo();
+ if (aEnabled) {
+ // We need to consider if getAutocompleteInfo() is valid,
+ // but @autocomplete is invalid case, because @autocomplete
+ // has smaller set of values.
+ is(field.autocomplete, test[2], "Checking @autocomplete of: " + test[0]);
+ }
+
+ is(info.section, "section" in test[1] ? test[1].section : "",
+ "Checking autocompleteInfo.section for " + field + ": " + test[0]);
+ is(info.addressType, "addressType" in test[1] ? test[1].addressType : "",
+ "Checking autocompleteInfo.addressType for " + field + ": " + test[0]);
+ is(info.contactType, "contactType" in test[1] ? test[1].contactType : "",
+ "Checking autocompleteInfo.contactType for " + field + ": " + test[0]);
+ is(info.fieldName, "fieldName" in test[1] ? test[1].fieldName : "",
+ "Checking autocompleteInfo.fieldName for " + field + ": " + test[0]);
+ is(info.credentialType, "credentialType" in test[1] ? test[1].credentialType: "",
+ "Checking autocompleteInfo.credentialType for " + field + ": " + test[0]);
+ is(info.canAutomaticallyPersist, "canAutomaticallyPersist" in test[1] ? test[1].canAutomaticallyPersist : true,
+ "Checking autocompleteInfo.canAutomaticallyPersist for " + field + ": " + test[0]);
+ }
+ }
+}
+
+function testAutocomplete(aField, aType, aEnabled) {
+ aField.type = aType;
+ if (aEnabled) {
+ ok(aField.getAutocompleteInfo() !== null, "getAutocompleteInfo shouldn't return null");
+ } else {
+ is(aField.getAutocompleteInfo(), null, "getAutocompleteInfo should return null");
+ }
+}
+
+// getAutocompleteInfo() should be able to parse all tokens as defined
+// in the spec regardless of whether dom.forms.autocomplete.formautofill pref
+// is on or off.
+add_task(async function testAutocompletePreferenceEnabled() {
+ await SpecialPowers.pushPrefEnv({"set": [["dom.forms.autocomplete.formautofill", true]]}, testInputTypes);
+ testAutocompleteInfoValue(true);
+});
+
+add_task(async function testAutocompletePreferenceDisabled() {
+ await SpecialPowers.pushPrefEnv({"set": [["dom.forms.autocomplete.formautofill", false]]}, testInputTypes);
+ testAutocompleteInfoValue(false);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_bug1039548.html b/dom/html/test/forms/test_bug1039548.html
new file mode 100644
index 0000000000..cea3cd67ef
--- /dev/null
+++ b/dom/html/test/forms/test_bug1039548.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1039548
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1039548</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1039548 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.waitForFocus(test);
+
+ var didTryToSubmit;
+ function test() {
+ var r = document.getElementById("radio");
+ r.focus();
+ didTryToSubmit = false;
+ sendKey("return");
+ ok(!didTryToSubmit, "Shouldn't have tried to submit!");
+
+ var t = document.getElementById("text");
+ t.focus();
+ didTryToSubmit = false;
+ sendKey("return");
+ ok(didTryToSubmit, "Should have tried to submit!");
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1039548">Mozilla Bug 1039548</a>
+<p id="display"></p>
+<div id="content">
+
+ <form onsubmit="didTryToSubmit = true; event.preventDefault();">
+ <input type="radio" id="radio">
+ </form>
+
+ <form onsubmit="didTryToSubmit = true; event.preventDefault();">
+ <input type="text" id="text">
+ </form>
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_bug1283915.html b/dom/html/test/forms/test_bug1283915.html
new file mode 100644
index 0000000000..90bffd4b20
--- /dev/null
+++ b/dom/html/test/forms/test_bug1283915.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1283915
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1283915</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1283915 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ function isCursorAtEnd(field){
+ is(field.selectionStart, field.value.length);
+ is(field.selectionEnd, field.value.length);
+ }
+
+ function test() {
+ var tField = document.getElementById("textField");
+ tField.focus();
+
+ sendString("a");
+ is(tField.value, "a");
+ isCursorAtEnd(tField);
+ document.body.offsetWidth; // frame must be created after type change
+
+ sendString("b");
+ is(tField.value, "ab");
+ isCursorAtEnd(tField);
+
+ sendString("c");
+ is(tField.value, "abc");
+ isCursorAtEnd(tField);
+
+ var nField = document.getElementById("numField");
+ nField.focus();
+
+ sendString("1");
+ is(nField.value, "1");
+ document.body.offsetWidth;
+
+ sendString("2");
+ is(nField.value, "12");
+
+ sendString("3");
+ is(nField.value, "123");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(test);
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1283915">Mozilla Bug 1283915</a>
+<p id="display"></p>
+<input id="textField" type="text" oninput="if (this.type !='password') this.type = 'password';">
+<input id="numField" type="text" oninput="if (this.type !='number') this.type = 'number';">
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_bug1286509.html b/dom/html/test/forms/test_bug1286509.html
new file mode 100644
index 0000000000..638e7fe85c
--- /dev/null
+++ b/dom/html/test/forms/test_bug1286509.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1286509
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1286509</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1286509">Mozilla Bug 1286509</a>
+<p id="display"></p>
+<div id="content">
+ <input type="range" id="test_input" min="0" max="10" value="5">
+</div>
+<pre id="test">
+ <script type="application/javascript">
+ /** Test for Bug 1286509 **/
+ SimpleTest.waitForExplicitFinish();
+ var expectedEventSequence = ['keydown', 'change', 'keyup'];
+ var eventCounts = {};
+ var expectedEventIdx = 0;
+
+ function test() {
+ var range = document.getElementById("test_input");
+ range.focus();
+ expectedEventSequence.forEach((eventName) => {
+ eventCounts[eventName] = 0;
+ range.addEventListener(eventName, (e) => {
+ ++eventCounts[eventName];
+ is(expectedEventSequence[expectedEventIdx], e.type, "Events sequence should be keydown, change, keyup");
+ expectedEventIdx = (expectedEventIdx + 1) % 3;
+ });
+ });
+ synthesizeKey("KEY_ArrowUp");
+ synthesizeKey("KEY_ArrowDown");
+ synthesizeKey("KEY_ArrowLeft");
+ synthesizeKey("KEY_ArrowRight");
+ is(eventCounts.change, 4, "Expect key up/down/left/right should trigger range input to fire change events");
+ SimpleTest.finish();
+ }
+ addLoadEvent(test);
+ </script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_button_attributes_reflection.html b/dom/html/test/forms/test_button_attributes_reflection.html
new file mode 100644
index 0000000000..de2097cb4c
--- /dev/null
+++ b/dom/html/test/forms/test_button_attributes_reflection.html
@@ -0,0 +1,144 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLButtonElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLButtonElement attributes reflection **/
+
+// .autofocus
+reflectBoolean({
+ element: document.createElement("button"),
+ attribute: "autofocus",
+});
+
+// .disabled
+reflectBoolean({
+ element: document.createElement("button"),
+ attribute: "disabled",
+});
+
+// .formAction
+reflectURL({
+ element: document.createElement("button"),
+ attribute: "formAction",
+});
+
+// .formEnctype
+reflectLimitedEnumerated({
+ element: document.createElement("button"),
+ attribute: "formEnctype",
+ validValues: [
+ "application/x-www-form-urlencoded",
+ "multipart/form-data",
+ "text/plain",
+ ],
+ invalidValues: [ "text/html", "", "tulip" ],
+ defaultValue: {
+ invalid: "application/x-www-form-urlencoded",
+ missing: "",
+ }
+});
+
+// .formMethod
+add_task(async function() {
+ reflectLimitedEnumerated({
+ element: document.createElement("button"),
+ attribute: "formMethod",
+ validValues: [ "get", "post", "dialog"],
+ invalidValues: [ "put", "", "tulip" ],
+ defaultValue: {
+ invalid: "get",
+ missing: "",
+ }
+ });
+});
+
+// .formNoValidate
+reflectBoolean({
+ element: document.createElement("button"),
+ attribute: "formNoValidate",
+});
+
+// .formTarget
+reflectString({
+ element: document.createElement("button"),
+ attribute: "formTarget",
+ otherValues: [ "_blank", "_self", "_parent", "_top" ],
+});
+
+// .name
+reflectString({
+ element: document.createElement("button"),
+ attribute: "name",
+ otherValues: [ "isindex", "_charset_" ]
+});
+
+// .type
+reflectLimitedEnumerated({
+ element: document.createElement("button"),
+ attribute: "type",
+ validValues: [ "submit", "reset", "button" ],
+ invalidValues: [ "this-is-probably-a-wrong-type", "", "tulip" ],
+ unsupportedValues: [ "menu" ],
+ defaultValue: "submit",
+});
+
+// .value
+reflectString({
+ element: document.createElement("button"),
+ attribute: "value",
+});
+
+// .willValidate
+ok("willValidate" in document.createElement("button"),
+ "willValidate should be an IDL attribute of the button element");
+is(typeof(document.createElement("button").willValidate), "boolean",
+ "button.willValidate should be a boolean");
+
+// .validity
+ok("validity" in document.createElement("button"),
+ "validity should be an IDL attribute of the button element");
+is(typeof(document.createElement("button").validity), "object",
+ "button.validity should be an object");
+ok(document.createElement("button").validity instanceof ValidityState,
+ "button.validity sohuld be an instance of ValidityState");
+
+// .validationMessage
+ok("validationMessage" in document.createElement("button"),
+ "validationMessage should be an IDL attribute of the button element");
+is(typeof(document.createElement("button").validationMessage), "string",
+ "button.validationMessage should be a string");
+
+// .checkValidity()
+ok("checkValidity" in document.createElement("button"),
+ "checkValidity() should be a method of the button element");
+is(typeof(document.createElement("button").checkValidity), "function",
+ "button.checkValidity should be a function");
+
+// .setCustomValidity()
+ok("setCustomValidity" in document.createElement("button"),
+ "setCustomValidity() should be a method of the button element");
+is(typeof(document.createElement("button").setCustomValidity), "function",
+ "button.setCustomValidity should be a function");
+
+// .labels
+ok("labels" in document.createElement("button"),
+ "button.labels should be an IDL attribute of the button element");
+is(typeof(document.createElement("button").labels), "object",
+ "button.labels should be an object");
+ok(document.createElement("button").labels instanceof NodeList,
+ "button.labels sohuld be an instance of NodeList");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_change_event.html b/dom/html/test/forms/test_change_event.html
new file mode 100644
index 0000000000..8be4554c58
--- /dev/null
+++ b/dom/html/test/forms/test_change_event.html
@@ -0,0 +1,286 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=722599
+-->
+<head>
+<title>Test for Bug 722599</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=722599">Mozilla Bug 722599</a>
+<p id="display"></p>
+<div id="content">
+<input type="file" id="fileInput"></input>
+<textarea id="textarea" onchange="++textareaChange;"></textarea>
+<input type="text" id="input_text" onchange="++textInputChange[0];"></input>
+<input type="email" id="input_email" onchange="++textInputChange[1];"></input>
+<input type="search" id="input_search" onchange="++textInputChange[2];"></input>
+<input type="tel" id="input_tel" onchange="++textInputChange[3];"></input>
+<input type="url" id="input_url" onchange="++textInputChange[4];"></input>
+<input type="password" id="input_password" onchange="++textInputChange[5];"></input>
+
+<!-- "Non-text" inputs-->
+<input type="button" id="input_button" onchange="++NonTextInputChange[0];"></input>
+<input type="submit" id="input_submit" onchange="++NonTextInputChange[1];"></input>
+<input type="image" id="input_image" onchange="++NonTextInputChange[2];"></input>
+<input type="reset" id="input_reset" onchange="++NonTextInputChange[3];"></input>
+<input type="radio" id="input_radio" onchange="++NonTextInputChange[4];"></input>
+<input type="checkbox" id="input_checkbox" onchange="++NonTextInputChange[5];"></input>
+<input type="number" id="input_number" onchange="++numberChange;"></input>
+<input type="range" id="input_range" onchange="++rangeChange;"></input>
+
+<!-- Input text with default value and blurs on focus-->
+<input type="text" id="input_text_value" onchange="++textInputValueChange"
+ onfocus="this.blur();" value="foo"></input>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ /** Test for Bug 722599 **/
+
+ const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+
+ var textareaChange = 0;
+ var fileInputChange = 0;
+ var textInputValueChange = 0;
+
+ var textInputTypes = ["text", "email", "search", "tel", "url", "password"];
+ var textInputChange = [0, 0, 0, 0, 0, 0];
+
+ var NonTextInputTypes = ["button", "submit", "image", "reset", "radio", "checkbox"];
+ var NonTextInputChange = [0, 0, 0, 0, 0, 0];
+
+ var numberChange = 0;
+ var rangeChange = 0;
+
+ var blurTestCalled = false; //Sentinel to prevent infinite loop.
+
+ SimpleTest.waitForExplicitFinish();
+ var MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ function fileInputBlurTest() {
+ var btn = document.getElementById('fileInput');
+ btn.focus()
+ btn.blur();
+ is(fileInputChange, 1, "change event shouldn't be dispatched on blur for file input element(1)");
+ }
+
+ function testUserInput() {
+ //Simulating an OK click and with a file name return.
+ MockFilePicker.useBlobFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ var input = document.getElementById('fileInput');
+ input.focus();
+
+ input.addEventListener("change", function (aEvent) {
+ ++fileInputChange;
+ if (!blurTestCalled) {
+ is(fileInputChange, 1, "change event should have been dispatched on file input.");
+ blurTestCalled = true;
+ fileInputBlurTest();
+ }
+ else {
+ is(fileInputChange, 1, "change event shouldn't be dispatched on blur for file input element (2)");
+ }
+ });
+ input.click();
+ // blur the file input, we can't use blur() because of bug 760283
+ document.getElementById('input_text').focus();
+ setTimeout(testUserInput2, 0);
+ }
+
+ function testUserInput2() {
+ var input = document.getElementById('fileInput');
+ // remove it, otherwise cleanup() opens a native file picker!
+ input.remove();
+ MockFilePicker.cleanup();
+
+ //text, email, search, telephone, url & password input tests
+ for (var i = 0; i < textInputTypes.length; ++i) {
+ input = document.getElementById("input_" + textInputTypes[i]);
+ input.focus();
+ synthesizeKey("KEY_Enter");
+ is(textInputChange[i], 0, "Change event shouldn't be dispatched on " + textInputTypes[i] + " input element");
+
+ sendString("m");
+ synthesizeKey("KEY_Enter");
+ is(textInputChange[i], 1, textInputTypes[i] + " input element should have dispatched change event.");
+ }
+
+ //focus and blur text input
+ input = document.getElementById("input_text");
+ input.focus();
+ sendString("f");
+ input.blur();
+ is(textInputChange[0], 2, "text input element should have dispatched change event (2).");
+
+ // value being set while focused
+ input.focus();
+ input.value = 'foo';
+ input.blur();
+ is(textInputChange[0], 2, "text input element should not have dispatched change event (2).");
+
+ // value being set while focused after being modified manually
+ input.focus();
+ sendString("f");
+ input.value = 'bar';
+ input.blur();
+ is(textInputChange[0], 3, "text input element should have dispatched change event (3).");
+
+ //focus and blur textarea
+ var textarea = document.getElementById("textarea");
+ textarea.focus();
+ sendString("f");
+ textarea.blur();
+ is(textareaChange, 1, "Textarea element should have dispatched change event.");
+
+ // value being set while focused
+ textarea.focus();
+ textarea.value = 'foo';
+ textarea.blur();
+ is(textareaChange, 1, "textarea should not have dispatched change event (1).");
+
+ // value being set while focused after being modified manually
+ textarea.focus();
+ sendString("f");
+ textarea.value = 'bar';
+ textarea.blur();
+ is(textareaChange, 2, "textearea should have dispatched change event (2).");
+
+ //Non-text input tests:
+ for (var i = 0; i < NonTextInputTypes.length; ++i) {
+ //button, submit, image and reset input type tests.
+ if (i < 4) {
+ input = document.getElementById("input_" + NonTextInputTypes[i]);
+ input.focus();
+ input.click();
+ is(NonTextInputChange[i], 0, "Change event shouldn't be dispatched on " + NonTextInputTypes[i] + " input element");
+ input.blur();
+ is(NonTextInputChange[i], 0, "Change event shouldn't be dispatched on " + NonTextInputTypes[i] + " input element(2)");
+ }
+ //for radio and and checkboxes, we require that change event should ONLY be dispatched on setting the value.
+ else {
+ input = document.getElementById("input_" + NonTextInputTypes[i]);
+ input.focus();
+ input.click();
+ is(NonTextInputChange[i], 1, NonTextInputTypes[i] + " input element should have dispatched change event.");
+ input.blur();
+ is(NonTextInputChange[i], 1, "Change event shouldn't be dispatched on " + NonTextInputTypes[i] + " input element");
+
+ // Test that change event is not dispatched if click event is cancelled.
+ function preventDefault(e) {
+ e.preventDefault();
+ }
+ input.addEventListener("click", preventDefault);
+ input.click();
+ is(NonTextInputChange[i], 1, "Change event shouldn't be dispatched if click event is cancelled");
+ input.removeEventListener("click", preventDefault);
+ }
+ }
+
+ // Special case type=number
+ var number = document.getElementById("input_number");
+ number.focus();
+ sendString("a");
+ number.blur();
+ is(numberChange, 0, "Change event shouldn't be dispatched on number input element for key changes that don't change its value");
+ number.value = "";
+ number.focus();
+ sendString("12");
+ is(numberChange, 0, "Change event shouldn't be dispatched on number input element for keyboard input until it loses focus");
+ number.blur();
+ is(numberChange, 1, "Change event should be dispatched on number input element on blur");
+ is(number.value, "12", "Sanity check that number keys were actually handled");
+ if (isDesktop) { // up/down arrow keys not supported on android/b2g
+ number.value = "";
+ number.focus();
+ synthesizeKey("KEY_ArrowUp");
+ synthesizeKey("KEY_ArrowUp");
+ synthesizeKey("KEY_ArrowDown");
+ is(numberChange, 4, "Change event should be dispatched on number input element for up/down arrow keys (a special case)");
+ is(number.value, "1", "Sanity check that number and arrow keys were actually handled");
+ }
+
+ // Special case type=range
+ var range = document.getElementById("input_range");
+ range.focus();
+ sendString("a");
+ range.blur();
+ is(rangeChange, 0, "Change event shouldn't be dispatched on range input element for key changes that don't change its value");
+ range.focus();
+ synthesizeKey("VK_HOME");
+ is(rangeChange, 1, "Change event should be dispatched on range input element for key changes");
+ range.blur();
+ is(rangeChange, 1, "Change event shouldn't be dispatched on range input element on blur");
+ range.focus();
+ var bcr = range.getBoundingClientRect();
+ var centerOfRangeX = bcr.width / 2;
+ var centerOfRangeY = bcr.height / 2;
+ synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, { type: "mousedown" });
+ is(rangeChange, 1, "Change event shouldn't be dispatched on range input element for mousedown");
+ synthesizeMouse(range, centerOfRangeX - 5, centerOfRangeY, { type: "mousemove" });
+ is(rangeChange, 1, "Change event shouldn't be dispatched on range input element during drag of thumb");
+ synthesizeMouse(range, centerOfRangeX, centerOfRangeY, { type: "mouseup" });
+ is(rangeChange, 2, "Change event should be dispatched on range input element at end of drag");
+ range.blur();
+ is(rangeChange, 2, "Change event shouldn't be dispatched on range input element when range loses focus after a drag");
+ synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, {});
+ is(rangeChange, 3, "Change event should be dispatched on range input element for a click that gives the range focus");
+
+ if (isDesktop) { // up/down arrow keys not supported on android/b2g
+ synthesizeKey("KEY_ArrowUp");
+ is(rangeChange, 4, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowUp)");
+ synthesizeKey("KEY_ArrowDown");
+ is(rangeChange, 5, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowDown)");
+ synthesizeKey("KEY_ArrowRight");
+ is(rangeChange, 6, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowRight)");
+ synthesizeKey("KEY_ArrowLeft");
+ is(rangeChange, 7, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowLeft)");
+ synthesizeKey("KEY_ArrowUp", {shiftKey: true});
+ is(rangeChange, 8, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowUp)");
+ synthesizeKey("KEY_ArrowDown", {shiftKey: true});
+ is(rangeChange, 9, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowDown)");
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true});
+ is(rangeChange, 10, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowRight)");
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ is(rangeChange, 11, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowLeft)");
+ synthesizeKey("KEY_PageUp");
+ is(rangeChange, 12, "Change event should be dispatched on range input element for key changes that change its value (KEY_PageUp)");
+ synthesizeKey("KEY_PageDown");
+ is(rangeChange, 13, "Change event should be dispatched on range input element for key changes that change its value (KEY_PageDown");
+ synthesizeKey("KEY_ArrowRight", {shiftKey: true});
+ is(rangeChange, 14, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_PageUp)");
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ is(rangeChange, 15, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_PageDown)");
+ }
+ //Input type change test.
+ input = document.getElementById("input_checkbox");
+ input.type = "text";
+ input.focus();
+ input.click();
+ input.blur();
+ is(NonTextInputChange[5], 1, "Change event shouldn't be dispatched for checkbox ---> text input type change");
+
+ setTimeout(testInputWithDefaultValue, 0);
+ }
+
+ function testInputWithDefaultValue() {
+ // focus and blur an input text should not trigger change event if content hasn't changed.
+ var input = document.getElementById('input_text_value');
+ input.focus();
+ is(textInputValueChange, 0, "change event shouldn't be dispatched on input text with default value");
+
+ SimpleTest.finish();
+ }
+
+ addLoadEvent(testUserInput);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_datalist_element.html b/dom/html/test/forms/test_datalist_element.html
new file mode 100644
index 0000000000..5f05634018
--- /dev/null
+++ b/dom/html/test/forms/test_datalist_element.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for the datalist element</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <datalist>
+ </datalist>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 555840 **/
+
+function checkClassesAndAttributes()
+{
+ var d = document.getElementsByTagName('datalist');
+ is(d.length, 1, "One datalist has been found");
+
+ d = d[0];
+ ok(d instanceof HTMLDataListElement,
+ "The datalist should be instance of HTMLDataListElement");
+
+ ok('options' in d, "datalist has an options IDL attribute");
+
+ ok(d.options, "options IDL attribute is not null");
+ ok(!d.getAttribute('options'), "datalist has no options content attribute");
+
+ ok(d.options instanceof HTMLCollection,
+ "options IDL attribute should be instance of HTMLCollection");
+}
+
+function checkOptions()
+{
+ var testData = [
+ /* [ Child list, Function modifying children, Recognized options ] */
+ [['option'], null, 1],
+ [['option', 'option', 'option', 'option'], null, 4],
+ /* Disabled options are not valid. */
+ [['option'], function(d) { d.childNodes[0].disabled = true; }, 0],
+ [['option', 'option'], function(d) { d.childNodes[0].disabled = true; }, 1],
+ /* Non-option elements are not recognized. */
+ [['input'], null, 0],
+ [['input', 'option'], null, 1],
+ [['input', 'textarea'], null, 0],
+ /* .value and .label are not needed to be valid options. */
+ [['option', 'option'], function(d) { d.childNodes[0].value = 'value'; }, 2],
+ [['option', 'option'], function(d) { d.childNodes[0].label = 'label'; }, 2],
+ [['option', 'option'], function(d) { d.childNodes[0].value = 'value'; d.childNodes[0].label = 'label'; }, 2],
+ [['select'],
+ function(d) {
+ var s = d.childNodes[0];
+ s.appendChild(new Option("foo"));
+ s.appendChild(new Option("bar"));
+ },
+ 2],
+ [['select'],
+ function(d) {
+ var s = d.childNodes[0];
+ s.appendChild(new Option("foo"));
+ s.appendChild(new Option("bar"));
+ var label = document.createElement("label");
+ d.appendChild(label);
+ label.appendChild(new Option("foobar"));
+ },
+ 3],
+ [['select'],
+ function(d) {
+ var s = d.childNodes[0];
+ s.appendChild(new Option("foo"));
+ s.appendChild(new Option("bar"));
+ var label = document.createElement("label");
+ d.appendChild(label);
+ label.appendChild(new Option("foobar"));
+ s.appendChild(new Option())
+ },
+ 4],
+ [[], function(d) { d.appendChild(document.createElementNS("foo", "option")); }, 0]
+ ];
+
+ var d = document.getElementsByTagName('datalist')[0];
+ var cachedOptions = d.options;
+
+ testData.forEach(function(data) {
+ data[0].forEach(function(e) {
+ d.appendChild(document.createElement(e));
+ })
+
+ /* Modify children. */
+ if (data[1]) {
+ data[1](d);
+ }
+
+ is(d.options, cachedOptions, "Should get the same object")
+ is(d.options.length, data[2],
+ "The number of recognized options should be " + data[2])
+
+ for (var i = 0; i < d.options.length; ++i) {
+ is(d.options[i].localName, "option",
+ "Should get an option for d.options[" + i + "]")
+ }
+
+ /* Cleaning-up. */
+ d.textContent = "";
+ })
+}
+
+checkClassesAndAttributes();
+checkOptions();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_double_submit.html b/dom/html/test/forms/test_double_submit.html
new file mode 100644
index 0000000000..d27fb290a4
--- /dev/null
+++ b/dom/html/test/forms/test_double_submit.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for multiple submissions in straightline code</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script>
+
+add_task(async function double_submit() {
+ dump("test start\n");
+ let popup = window.open("file_double_submit.html");
+ await new Promise(resolve => {
+ popup.addEventListener("load", resolve, {once: true})
+ });
+
+ let numCalls = 0;
+ popup.addEventListener("beforeunload", () => {
+ numCalls++;
+ info("beforeunload called " + numCalls + " times");
+ });
+
+ info("clicking button");
+ popup.document.querySelector("button").click();
+
+ is(numCalls, 1, "beforeunload should only fire once");
+ popup.close();
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attribute-1.html b/dom/html/test/forms/test_form_attribute-1.html
new file mode 100644
index 0000000000..6735f514ae
--- /dev/null
+++ b/dom/html/test/forms/test_form_attribute-1.html
@@ -0,0 +1,473 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=588683
+-->
+<head>
+ <title>Test for form attributes 1</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for form attributes 1 **/
+
+/**
+ * All functions take an array of forms in first argument and an array of
+ * elements in second argument.
+ * Then, it returns an array containing an array of form and an array of array
+ * of elements. The array represent the form association with elements like this:
+ * [ [ form1, form2 ], [ [ elmt1ofForm1, elmt2ofForm2 ], [ elmtofForm2 ] ] ]
+ */
+
+/**
+ * test0a and test0b are testing the regular behavior of form ownership.
+ */
+function test0a(aForms, aElements)
+{
+ // <form><element></form>
+ // <form><element></form>
+ aForms[0].appendChild(aElements[0]);
+ aForms[1].appendChild(aElements[1]);
+
+ return [[aForms[0],aForms[1]],[[aElements[0]],[aElements[1]]]];
+}
+
+function test0b(aForms, aElements)
+{
+ // <form><element><form><element></form></form>
+ aForms[0].appendChild(aElements[0]);
+ aForms[0].appendChild(aForms[1]);
+ aForms[1].appendChild(aElements[1]);
+
+ return [[aForms[0],aForms[1]],[[aElements[0]],[aElements[1]]]];
+}
+
+/**
+ * This function test that, when an element is not a descendant of a form
+ * element and has @form set to a valid form id, it's form owner is the form
+ * which has the id.
+ */
+function test1(aForms, aElements)
+{
+ // <form id='f'></form><element id='f'>
+ aForms[0].id = 'f';
+ aElements[0].setAttribute('form', 'f');
+
+ return [[aForms[0]], [[aElements[0]]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id (not it's descendant), it's form
+ * owner is the form which has the id.
+ */
+function test2(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+
+ return [[aForms[0], aForms[1]], [[aElements[0]],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id (not it's descendant), then the
+ * form attribute is removed, it does not have a form owner.
+ */
+function test3(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aElements[0].removeAttribute('form');
+
+ return [[aForms[0], aForms[1]], [[],[aElements[0]]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id (not it's descendant), then the
+ * form's id attribute is removed, it does not have a form owner.
+ */
+function test4(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[0].removeAttribute('id');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to an invalid form id, then it does not have a form
+ * owner.
+ */
+function test5(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='foo'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'foo');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id (not it's descendant), then the
+ * form id attribute is changed to an invalid id, it does not have a form owner.
+ */
+function test6(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aElements[0].setAttribute('form', 'foo');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to an invalid form id, then the form id attribute
+ * is changed to a valid form id, it's form owner is the form which has this id.
+ */
+function test7(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='foo'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'foo');
+ aElements[0].setAttribute('form', 'f');
+
+ return [[aForms[0], aForms[1]], [[aElements[0]],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a list of ids containing one valid form, then
+ * it does not have a form owner.
+ */
+function test8(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f foo'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f foo');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a form id which is valid in a case insensitive
+ * way, then it does not have a form owner.
+ */
+function test9(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='F'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'F');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a form id which is not a valid id, then it's
+ * form owner is it does not have a form owner.
+ */
+function test10(aForms, aElements)
+{
+ // <form id='F'></form><form><element form='f'></form>
+ aForms[0].id = 'F';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a form id which is not a valid id, then it's
+ * form owner is it does not have a form owner.
+ */
+function test11(aForms, aElements)
+{
+ // <form id='foo bar'></form><form><element form='foo bar'></form>
+ aForms[0].id = 'foo bar';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'foo bar');
+
+ return [[aForms[0], aForms[1]], [[aElements[0]],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id and the form id change, then
+ * it does not have a form owner.
+ */
+function test12(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[0].id = 'foo';
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to an invalid form id and the form id change to a
+ * valid one, then it's form owner is the form which has the id.
+ */
+function test13(aForms, aElements)
+{
+ // <form id='foo'></form><form><element form='f'></form>
+ aForms[0].id = 'foo';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[0].id = 'f';
+
+ return [[aForms[0], aForms[1]], [[aElements[0]],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id and a form with the same id is
+ * inserted before in the tree, then it's form owner is the form which has the
+ * id.
+ */
+function test14(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[2].id = 'f';
+
+ document.getElementById('content').insertBefore(aForms[2], aForms[0]);
+
+ return [[aForms[0], aForms[1], aForms[2]], [[],[],[aElements[0]]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id and an element with the same id is
+ * inserted before in the tree, then it does not have a form owner.
+ */
+function test15(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aElements[1].id = 'f';
+
+ document.getElementById('content').insertBefore(aElements[1], aForms[0]);
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form
+ * element and has @form set to a valid form id and the form is removed from
+ * the tree, then it does not have a form owner.
+ */
+function test16(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ aElements[1].id = 'f';
+
+ document.getElementById('content').removeChild(aForms[0]);
+
+ return [[aForms[0], aForms[1]], [[],[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form element
+ * and has @form set to the empty string, it does not have a form owner.
+ */
+function test17(aForms, aElements)
+{
+ // <form><element form=''></form>
+ aForms[0].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', '');
+
+ return [[aForms[0]], [[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form element
+ * and has @form set to the empty string, it does not have a form owner even if
+ * it's parent has its id equals to the empty string.
+ */
+function test18(aForms, aElements)
+{
+ // <form id=''><element form=''></form>
+ aForms[0].id = '';
+ aForms[0].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', '');
+
+ return [[aForms[0]], [[]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form element
+ * and has @form set to a valid form id and the element is being moving inside
+ * it's parent, it's form owner will remain the form with the id.
+ */
+function test19(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'><element></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aForms[1].appendChild(aElements[1]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[1].appendChild(aElements[0]);
+
+ return [[aForms[0],aForms[1]],[[aElements[0]],[aElements[1]]]];
+}
+
+/**
+ * This function test that, when an element is a descendant of a form element
+ * and has @form set to a valid form id and the element is being moving inside
+ * another form, it's form owner will remain the form with the id.
+ */
+function test20(aForms, aElements)
+{
+ // <form id='f'></form><form><element form='f'><element></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aForms[1].appendChild(aElements[1]);
+ aElements[0].setAttribute('form', 'f');
+ aForms[2].appendChild(aElements[0]);
+
+ return [[aForms[0],aForms[1],aForms[2]],[[aElements[0]],[aElements[1]],[]]];
+}
+
+/**
+ * This function test that when removing a form, the elements with a @form set
+ * will be correctly removed from there form owner.
+ */
+function test21(aForms, aElements)
+{
+ // <form id='f'><form><form><element form='f'></form>
+ aForms[0].id = 'f';
+ aForms[1].appendChild(aElements[0]);
+ aElements[0].setAttribute('form', 'f');
+ document.getElementById('content').removeChild(aForms[1]);
+
+ return [[aForms[0]],[[]]];
+}
+
+var functions = [
+ test0a, test0b,
+ test1, test2, test3, test4, test5, test6, test7, test8, test9,
+ test10, test11, test12, test13, test14, test15, test16, test17, test18, test19,
+ test20, test21,
+];
+
+// Global variable to have an easy access to <div id='content'>.
+var content = document.getElementById('content');
+
+// Initializing the needed elements.
+var forms = [
+ document.createElement('form'),
+ document.createElement('form'),
+ document.createElement('form'),
+];
+
+var elementNames = [
+ 'button', 'fieldset', 'input', 'label', 'object', 'output', 'select',
+ 'textarea'
+];
+
+var todoElements = [
+ ['keygen', 'Keygen'],
+];
+
+for (var e of todoElements) {
+ var node = document.createElement(e[0]);
+ var nodeString = HTMLElement.prototype.toString.apply(node);
+ nodeString = nodeString.replace(/Element[\] ].*/, "Element");
+ todo_is(nodeString, "[object HTML" + e[1] + "Element",
+ e[0] + " should not be implemented");
+}
+
+for (var name of elementNames) {
+ var elements = [
+ document.createElement(name),
+ document.createElement(name),
+ ];
+
+ for (var func of functions) {
+ // Clean-up.
+ while (content.firstChild) {
+ content.firstChild.remove();
+ }
+ for (form of forms) {
+ content.appendChild(form);
+ form.removeAttribute('id');
+ }
+ for (e of elements) {
+ content.appendChild(e);
+ e.removeAttribute('form');
+ is(e.form, null, "The element should not have a form owner");
+ }
+
+ // Calling the test.
+ var results = func(forms, elements);
+
+ // Checking the results.
+ var formsList = results[0];
+ for (var i=0; i<formsList.length; ++i) {
+ var elementsList = results[1][i];
+ if (name != 'label' && name != 'meter' && name != 'progress') {
+ is(formsList[i].elements.length, elementsList.length,
+ "The form should contain " + elementsList.length + " elements");
+ }
+ for (var j=0; j<elementsList.length; ++j) {
+ if (name != 'label' && name != 'meter' && name != 'progress') {
+ is(formsList[i].elements[j], elementsList[j],
+ "The form should contain " + elementsList[j]);
+ }
+ if (name != 'label') {
+ is(elementsList[j].form, formsList[i],
+ "The form owner should be the form associated to the list");
+ }
+ }
+ }
+ }
+
+ // Cleaning-up.
+ for (e of elements) {
+ e.remove();
+ e = null;
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attribute-2.html b/dom/html/test/forms/test_form_attribute-2.html
new file mode 100644
index 0000000000..b7fe5daa87
--- /dev/null
+++ b/dom/html/test/forms/test_form_attribute-2.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=588683
+-->
+<head>
+ <title>Test for form attributes 2</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form id='a'>
+ <form id='b'>
+ <input id='i' form='b'>
+ <script>
+ is(document.getElementById('i').form, document.getElementById('b'),
+ "While parsing, the form property should work.");
+ </script>
+ </form>
+ </form>
+ <form id='c'>
+ <form id='d'>
+ <input id='i2' form='c'>
+ <script>
+ is(document.getElementById('i2').form, document.getElementById('c'),
+ "While parsing, the form property should work.");
+ </script>
+ </form>
+ </form>
+ <!-- Let's tests without @form -->
+ <form id='e'>
+ <form id='f'>
+ <input id='i3'>
+ <script>
+ // bug 589073
+ todo_is(document.getElementById('i3').form, document.getElementById('f'),
+ "While parsing, the form property should work.");
+ </script>
+ </form>
+ </form>
+ <form id='g'>
+ <input id='i4'>
+ <script>
+ is(document.getElementById('i4').form, document.getElementById('g'),
+ "While parsing, the form property should work.");
+ </script>
+ </form>
+</div>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attribute-3.html b/dom/html/test/forms/test_form_attribute-3.html
new file mode 100644
index 0000000000..9ceed86716
--- /dev/null
+++ b/dom/html/test/forms/test_form_attribute-3.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=588683
+-->
+<head>
+ <title>Test for form attributes 3</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a>
+<p id="display"></p>
+<div id="content">
+ <form id='f'>
+ <input name='e1'>
+ </form>
+ <form id='f2'>
+ <input name='e2'>
+ <input id='i3' form='f'
+ onfocus="var catched=false;
+ try { e1; } catch(e) { catched=true; }
+ ok(!catched, 'e1 should be in the scope of i3');
+ catched = false;
+ try { e2; } catch(e) { catched=true; }
+ ok(catched, 'e2 should not be in the scope of i3');
+ document.getElementById('i4').focus();"
+ >
+ <input id='i4' form='f2'
+ onfocus="var catched=false;
+ try { e2; } catch(e) { catched=true; }
+ ok(!catched, 'e2 should be in the scope of i4');
+ document.getElementById('i5').focus();"
+ >
+ <input id='i5'
+ onfocus="var catched=false;
+ try { e2; } catch(e) { catched=true; }
+ ok(!catched, 'e2 should be in the scope of i5');
+ document.getElementById('i6').focus();"
+ >
+ </form>
+ <input id='i6' form='f'
+ onfocus="var catched=false;
+ try { e1; } catch(e) { catched=true; }
+ ok(!catched, 'e1 should be in the scope of i6');
+ document.getElementById('i7').focus();"
+ >
+ <input id='i7' form='f2'
+ onfocus="var catched=false;
+ try { e2; } catch(e) { catched=true; }
+ ok(!catched, 'e2 should be in the scope of i7');
+ this.blur();
+ SimpleTest.finish();"
+ >
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for form attributes 3 **/
+
+SimpleTest.waitForExplicitFinish();
+
+document.getElementById('i3').focus();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attribute-4.html b/dom/html/test/forms/test_form_attribute-4.html
new file mode 100644
index 0000000000..f2228cec45
--- /dev/null
+++ b/dom/html/test/forms/test_form_attribute-4.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=588683
+-->
+<head>
+ <title>Test for form attributes 4</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a>
+<p id="display"></p>
+<div id="content" style='display:none;'>
+ <form id='f'>
+ </form>
+ <table id='t'>
+ <form id='f2'>
+ <tr><td><input id='i1'></td></tr>
+ <tr><td><input id='i2' form='f'></td></tr>
+ </form>
+ </table>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for form attributes 4 **/
+
+var table = document.getElementById('t');
+var i1 = document.getElementById('i1');
+var i2 = document.getElementById('i2');
+
+is(i1.form, document.getElementById('f2'),
+ "i1 form should be it's parent");
+is(i2.form, document.getElementById('f'),
+ "i1 form should be the form with the id in @form");
+
+table.removeChild(document.getElementById('f2'));
+is(i1, document.getElementById('i1'),
+ "i1 should still be in the document");
+is(i1.form, null, "i1 should not have any form owner");
+is(i2.form, document.getElementById('f'),
+ "i1 form should be the form with the id in @form");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_attributes_reflection.html b/dom/html/test/forms/test_form_attributes_reflection.html
new file mode 100644
index 0000000000..0d0ef6b870
--- /dev/null
+++ b/dom/html/test/forms/test_form_attributes_reflection.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLFormElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLFormElement attributes reflection **/
+
+// .acceptCharset
+reflectString({
+ element: document.createElement("form"),
+ attribute: { idl: "acceptCharset", content: "accept-charset" },
+ otherValues: [ "ISO-8859-1", "UTF-8" ],
+});
+
+reflectURL({
+ element: document.createElement("form"),
+ attribute: "action",
+});
+
+// .autocomplete
+reflectLimitedEnumerated({
+ element: document.createElement("form"),
+ attribute: "autocomplete",
+ validValues: [ "on", "off" ],
+ invalidValues: [ "", "foo", "tulip", "default" ],
+ defaultValue: "on",
+});
+
+// .enctype
+reflectLimitedEnumerated({
+ element: document.createElement("form"),
+ attribute: "enctype",
+ validValues: [ "application/x-www-form-urlencoded", "multipart/form-data",
+ "text/plain" ],
+ invalidValues: [ "", "foo", "tulip", "multipart/foo" ],
+ defaultValue: "application/x-www-form-urlencoded"
+});
+
+// .encoding
+reflectLimitedEnumerated({
+ element: document.createElement("form"),
+ attribute: { idl: "encoding", content: "enctype" },
+ validValues: [ "application/x-www-form-urlencoded", "multipart/form-data",
+ "text/plain" ],
+ invalidValues: [ "", "foo", "tulip", "multipart/foo" ],
+ defaultValue: "application/x-www-form-urlencoded"
+});
+
+// .method
+reflectLimitedEnumerated({
+ element: document.createElement("form"),
+ attribute: "method",
+ validValues: [ "get", "post" ],
+ invalidValues: [ "", "foo", "tulip" ],
+ defaultValue: "get"
+});
+
+// .name
+reflectString({
+ element: document.createElement("form"),
+ attribute: "name",
+});
+
+// .noValidate
+reflectBoolean({
+ element: document.createElement("form"),
+ attribute: "noValidate",
+});
+
+// .target
+reflectString({
+ element: document.createElement("form"),
+ attribute: "target",
+ otherValues: [ "_blank", "_self", "_parent", "_top" ],
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_form_named_getter_dynamic.html b/dom/html/test/forms/test_form_named_getter_dynamic.html
new file mode 100644
index 0000000000..4a19768453
--- /dev/null
+++ b/dom/html/test/forms/test_form_named_getter_dynamic.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=377413
+-->
+<head>
+ <title>Test for Bug 377413</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <link rel='stylesheet' href='/resources/testharness.css'>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377413">Mozilla Bug 377413</a>
+<p id="log"></p>
+<div id="content">
+ <form>
+ <table>
+ <tbody>
+ </tbody>
+ </table>
+ </form>
+</div>
+
+<script type="text/javascript">
+
+/** Tests for Bug 377413 **/
+var tb = document.getElementsByTagName('tbody')[0];
+
+test(function(){
+ tb.innerHTML = '<tr><td><input name="fooboo"></td></tr>';
+ document.forms[0].fooboo.value = 'testme';
+ document.getElementsByTagName('table')[0].deleteRow(0);
+ assert_equals(document.forms[0].fooboo, undefined);
+}, "no element reference after deleting it with deleteRow()");
+
+test(function(){
+ var b = tb.appendChild(document.createElement('tr')).appendChild(document.createElement('td')).appendChild(document.createElement('button'));
+ b.name = b.value = 'boofoo';
+ assert_equals(document.forms[0].elements[0].value, 'boofoo');
+}, 'element value set correctly');
+
+test(function(){
+ assert_true('boofoo' in document.forms[0]);
+}, 'element name has created property on form');
+
+test(function(){
+ tb.innerHTML = '';
+ assert_false('boofoo' in document.forms[0]);
+}, "no element reference after deleting it by setting innerHTML");
+
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_formaction_attribute.html b/dom/html/test/forms/test_formaction_attribute.html
new file mode 100644
index 0000000000..0dee2f172d
--- /dev/null
+++ b/dom/html/test/forms/test_formaction_attribute.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=566160
+-->
+<head>
+ <title>Test for Bug 566160</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566160">Mozilla Bug 566160</a>
+<p id="display"></p>
+<style>
+ iframe { width: 130px; height: 100px;}
+</style>
+<iframe name='frame1' id='frame1'></iframe>
+<iframe name='frame2' id='frame2'></iframe>
+<iframe name='frame3' id='frame3'></iframe>
+<iframe name='frame3bis' id='frame3bis'></iframe>
+<iframe name='frame4' id='frame4'></iframe>
+<iframe name='frame5' id='frame5'></iframe>
+<iframe name='frame6' id='frame6'></iframe>
+<iframe name='frame7' id='frame7'></iframe>
+<div id="content">
+ <!-- submit controls with formaction that are validated with a CLICK -->
+ <form target="frame1" action="FAIL.html" method="GET">
+ <input name='foo' value='foo'>
+ <input type='submit' id='is' formaction="PASS.html">
+ </form>
+ <form target="frame2" action="FAIL.html" method="GET">
+ <input name='bar' value='bar'>
+ <input type='image' id='ii' formaction="PASS.html">
+ </form>
+ <form target="frame3" action="FAIL.html" method="GET">
+ <input name='tulip' value='tulip'>
+ <button type='submit' id='bs' formaction="PASS.html">submit</button>
+ </form>
+ <form target="frame3bis" action="FAIL.html" method="GET">
+ <input name='tulipbis' value='tulipbis'>
+ <button type='submit' id='bsbis' formaction="PASS.html">submit</button>
+ </form>
+
+ <!-- submit controls with formaction that are validated with ENTER -->
+ <form target="frame4" action="FAIL.html" method="GET">
+ <input name='footulip' value='footulip'>
+ <input type='submit' id='is2' formaction="PASS.html">
+ </form>
+ <form target="frame5" action="FAIL.html" method="GET">
+ <input name='foobar' value='foobar'>
+ <input type='image' id='ii2' formaction="PASS.html">
+ </form>
+ <form target="frame6" action="FAIL.html" method="GET">
+ <input name='tulip2' value='tulip2'>
+ <button type='submit' id='bs2' formaction="PASS.html">submit</button>
+ </form>
+
+ <!-- check that when submitting a from from an element
+ which is not a submit control, @formaction isn't used -->
+ <form target='frame7' action="PASS.html" method="GET">
+ <input id='enter' name='input' value='enter' formaction="FAIL.html">
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 566160 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+const BASE_URI = `${location.origin}/tests/dom/html/test/forms/PASS.html`;
+var gTestResults = {
+ frame1: BASE_URI + "?foo=foo",
+ frame2: BASE_URI + "?bar=bar&x=0&y=0",
+ frame3: BASE_URI + "?tulip=tulip",
+ frame3bis: BASE_URI + "?tulipbis=tulipbis",
+ frame4: BASE_URI + "?footulip=footulip",
+ frame5: BASE_URI + "?foobar=foobar&x=0&y=0",
+ frame6: BASE_URI + "?tulip2=tulip2",
+ frame7: BASE_URI + "?input=enter",
+};
+
+var gPendingLoad = 0; // Has to be set after depending on the frames number.
+
+function runTests()
+{
+ // We add a load event for the frames which will be called when the forms
+ // will be submitted.
+ var frames = [ document.getElementById('frame1'),
+ document.getElementById('frame2'),
+ document.getElementById('frame3'),
+ document.getElementById('frame3bis'),
+ document.getElementById('frame4'),
+ document.getElementById('frame5'),
+ document.getElementById('frame6'),
+ document.getElementById('frame7'),
+ ];
+ gPendingLoad = frames.length;
+
+ for (var i=0; i<frames.length; i++) {
+ frames[i].setAttribute('onload', "frameLoaded(this);");
+ }
+
+ /**
+ * We are going to focus each element before interacting with either for
+ * simulating the ENTER key (synthesizeKey) or a click (synthesizeMouse) or
+ * using .click(). This because it may be needed (ENTER) and because we want
+ * to have the element visible in the iframe.
+ *
+ * Focusing the first element (id='is') is launching the tests.
+ */
+ document.getElementById('is').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('is'), 5, 5, {});
+ document.getElementById('ii').focus();
+ }, {once: true});
+
+ document.getElementById('ii').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('ii'), 5, 5, {});
+ document.getElementById('bs').focus();
+ }, {once: true});
+
+ document.getElementById('bs').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('bs'), 5, 5, {});
+ document.getElementById('bsbis').focus();
+ }, {once: true});
+
+ document.getElementById('bsbis').addEventListener('focus', function(aEvent) {
+ document.getElementById('bsbis').click();
+ document.getElementById('is2').focus();
+ }, {once: true});
+
+ document.getElementById('is2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('ii2').focus();
+ }, {once: true});
+
+ document.getElementById('ii2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('bs2').focus();
+ }, {once: true});
+
+ document.getElementById('bs2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('enter').focus();
+ }, {once: true});
+
+ document.getElementById('enter').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ }, {once: true});
+
+ document.getElementById('is').focus();
+}
+
+function frameLoaded(aFrame) {
+ // Check if formaction/action has the correct behavior.
+ is(aFrame.contentWindow.location.href, gTestResults[aFrame.name],
+ "the action attribute doesn't have the correct behavior");
+
+ if (--gPendingLoad == 0) {
+ SimpleTest.finish();
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_formnovalidate_attribute.html b/dom/html/test/forms/test_formnovalidate_attribute.html
new file mode 100644
index 0000000000..2e3714d2fe
--- /dev/null
+++ b/dom/html/test/forms/test_formnovalidate_attribute.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=589696
+-->
+<head>
+ <title>Test for Bug 589696</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=589696">Mozilla Bug 589696</a>
+<p id="display"></p>
+<iframe style='width:50px; height: 50px;' name='t'></iframe>
+<div id="content">
+ <!-- Next forms should not submit because formnovalidate isn't set on the
+ element used for the submission. -->
+ <form target='t' action='data:text/html,'>
+ <input id='av' required>
+ <input type='submit' formnovalidate>
+ <input id='a' type='submit'>
+ </form>
+ <form target='t' action='data:text/html,'>
+ <input id='bv' type='checkbox' required>
+ <button type='submit' formnovalidate></button>
+ <button id='b' type='submit'></button>
+ </form>
+ <!-- Next form should not submit because formnovalidate only applies for
+ submit controls. -->
+ <form target='t' action='data:text/html,'>
+ <input id='c' required formnovalidate>
+ </form>
+ <!--- Next forms should submit without any validation check. -->
+ <form target='t' action='data:text/html,'>
+ <input id='dv' required>
+ <input id='d' type='submit' formnovalidate>
+ </form>
+ <form target='t' action='data:text/html,'>
+ <input id='ev' type='checkbox' required>
+ <button id='e' type='submit' formnovalidate></button>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 589696 **/
+
+document.getElementById('av').addEventListener("invalid", function(aEvent) {
+ aEvent.target.removeAttribute("invalid", arguments.callee, false);
+ ok(true, "formnovalidate should not apply on if not set on the submit " +
+ "control used for the submission");
+ document.getElementById('b').click();
+});
+
+document.getElementById('bv').addEventListener("invalid", function(aEvent) {
+ aEvent.target.removeAttribute("invalid", arguments.callee, false);
+ ok(true, "formnovalidate should not apply on if not set on the submit " +
+ "control used for the submission");
+ var c = document.getElementById('c');
+ c.focus();
+ synthesizeKey("KEY_Enter");
+});
+
+document.getElementById('c').addEventListener("invalid", function(aEvent) {
+ aEvent.target.removeAttribute("invalid", arguments.callee, false);
+ ok(true, "formnovalidate should only apply on submit controls");
+ document.getElementById('d').click();
+});
+
+document.forms[3].addEventListener("submit", function(aEvent) {
+ aEvent.target.removeAttribute("submit", arguments.callee, false);
+ ok(true, "formnovalidate applies if set on the submit control used for the submission");
+ document.getElementById('e').click();
+});
+
+document.forms[4].addEventListener("submit", function(aEvent) {
+ aEvent.target.removeAttribute("submit", arguments.callee, false);
+ ok(true, "formnovalidate applies if set on the submit control used for the submission");
+ SimpleTest.executeSoon(SimpleTest.finish);
+});
+
+/**
+ * We have to be sure invalid events behave as expected.
+ * They should be sent before the submit event so we can just create a test
+ * failure if we got one when unexpected. All of them should be caught if
+ * sent.
+ * At worst, we got random green which isn't harmful.
+ * If expected, they will be part of the chain reaction.
+ */
+function unexpectedInvalid(aEvent)
+{
+ aEvent.target.removeAttribute("invalid", unexpectedInvalid, false);
+ ok(false, "invalid event should not be sent");
+}
+
+document.getElementById('dv').addEventListener("invalid", unexpectedInvalid);
+document.getElementById('ev').addEventListener("invalid", unexpectedInvalid);
+
+/**
+ * Some submission have to be canceled. In that case, the submit events should
+ * not be sent.
+ * Same behavior as unexpected invalid events.
+ */
+function unexpectedSubmit(aEvent)
+{
+ aEvent.target.removeAttribute("submit", unexpectedSubmit, false);
+ ok(false, "submit event should not be sent");
+}
+
+document.forms[0].addEventListener("submit", unexpectedSubmit);
+document.forms[1].addEventListener("submit", unexpectedSubmit);
+document.forms[2].addEventListener("submit", unexpectedSubmit);
+
+SimpleTest.waitForExplicitFinish();
+
+// This is going to call all the tests (with a chain reaction).
+SimpleTest.waitForFocus(function() {
+ document.getElementById('a').click();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_attributes_reflection.html b/dom/html/test/forms/test_input_attributes_reflection.html
new file mode 100644
index 0000000000..348ea0f80d
--- /dev/null
+++ b/dom/html/test/forms/test_input_attributes_reflection.html
@@ -0,0 +1,271 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLInputElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLInputElement attributes reflection **/
+
+// TODO: maybe make those reflections be tested against all input types.
+
+function testWidthHeight(attr) {
+ var element = document.createElement('input');
+ is(element[attr], 0, attr + ' always returns 0 if not type=image');
+ element.setAttribute(attr, '42');
+ is(element[attr], 0, attr + ' always returns 0 if not type=image');
+ is(element.getAttribute(attr), '42');
+ element[attr] = 0;
+ is(element.getAttribute(attr), '0', 'setting ' + attr + ' changes the content attribute');
+ element[attr] = 12;
+ is(element.getAttribute(attr), '12', 'setting ' + attr + ' changes the content attribute');
+
+ element.removeAttribute(attr);
+ is(element.getAttribute(attr), null);
+
+ element = document.createElement('input');
+ element.type = 'image';
+ element.style.display = "inline";
+ document.getElementById('content').appendChild(element);
+ isnot(element[attr], 0, attr + ' represents the dimension of the element if type=image');
+
+ element.setAttribute(attr, '42');
+ isnot(element[attr], 0, attr + ' represents the dimension of the element if type=image');
+ isnot(element[attr], 42, attr + ' represents the dimension of the element if type=image');
+ is(element.getAttribute(attr), '42');
+ element[attr] = 0;
+ is(element.getAttribute(attr), '0', 'setting ' + attr + ' changes the content attribute');
+ element[attr] = 12;
+ is(element.getAttribute(attr), '12', 'setting ' + attr + ' changes the content attribute');
+
+ element.removeAttribute(attr);
+ is(element.getAttribute(attr), null);
+}
+
+// .accept
+reflectString({
+ element: document.createElement("input"),
+ attribute: "accept",
+ otherValues: [ "audio/*", "video/*", "image/*", "image/png",
+ "application/msword", "appplication/pdf" ],
+});
+
+// .alt
+reflectString({
+ element: document.createElement("input"),
+ attribute: "alt",
+});
+
+// .autocomplete
+reflectLimitedEnumerated({
+ element: document.createElement("input"),
+ attribute: "autocomplete",
+ validValues: [ "on", "off" ],
+ invalidValues: [ "", "default", "foo", "tulip" ],
+});
+
+// .autofocus
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "autofocus",
+});
+
+// .defaultChecked
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: { idl: "defaultChecked", content: "checked" },
+});
+
+// .checked doesn't reflect a content attribute.
+
+// .dirName
+reflectString({
+ element: document.createElement("input"),
+ attribute: "dirName"
+});
+
+// .disabled
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "disabled",
+});
+
+// TODO: form (HTMLFormElement)
+// TODO: files (FileList)
+
+// .formAction
+reflectURL({
+ element: document.createElement("button"),
+ attribute: "formAction",
+});
+
+// .formEnctype
+reflectLimitedEnumerated({
+ element: document.createElement("input"),
+ attribute: "formEnctype",
+ validValues: [ "application/x-www-form-urlencoded", "multipart/form-data",
+ "text/plain" ],
+ invalidValues: [ "", "foo", "tulip", "multipart/foo" ],
+ defaultValue: { invalid: "application/x-www-form-urlencoded", missing: "" }
+});
+
+// .formMethod
+reflectLimitedEnumerated({
+ element: document.createElement("input"),
+ attribute: "formMethod",
+ validValues: [ "get", "post" ],
+ invalidValues: [ "", "foo", "tulip" ],
+ defaultValue: { invalid: "get", missing: "" }
+});
+
+// .formNoValidate
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "formNoValidate",
+});
+
+// .formTarget
+reflectString({
+ element: document.createElement("input"),
+ attribute: "formTarget",
+ otherValues: [ "_blank", "_self", "_parent", "_top" ],
+});
+
+// .height
+testWidthHeight('height');
+
+// .indeterminate doesn't reflect a content attribute.
+
+// TODO: list (HTMLElement)
+
+// .max
+reflectString({
+ element: document.createElement('input'),
+ attribute: 'max',
+});
+
+// .maxLength
+reflectInt({
+ element: document.createElement("input"),
+ attribute: "maxLength",
+ nonNegative: true,
+});
+
+// .min
+reflectString({
+ element: document.createElement('input'),
+ attribute: 'min',
+});
+
+// .multiple
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "multiple",
+});
+
+// .name
+reflectString({
+ element: document.createElement("input"),
+ attribute: "name",
+ otherValues: [ "isindex", "_charset_" ],
+});
+
+// .pattern
+reflectString({
+ element: document.createElement("input"),
+ attribute: "pattern",
+ otherValues: [ "[0-9][A-Z]{3}" ],
+});
+
+// .placeholder
+reflectString({
+ element: document.createElement("input"),
+ attribute: "placeholder",
+ otherValues: [ "foo\nbar", "foo\rbar", "foo\r\nbar" ],
+});
+
+// .readOnly
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "readOnly",
+});
+
+// .required
+reflectBoolean({
+ element: document.createElement("input"),
+ attribute: "required",
+});
+
+// .size
+reflectUnsignedInt({
+ element: document.createElement("input"),
+ attribute: "size",
+ nonZero: true,
+ defaultValue: 20,
+});
+
+// .src (URL)
+reflectURL({
+ element: document.createElement('input'),
+ attribute: 'src',
+});
+
+// .step
+reflectString({
+ element: document.createElement('input'),
+ attribute: 'step',
+});
+
+// .type
+reflectLimitedEnumerated({
+ element: document.createElement("input"),
+ attribute: "type",
+ validValues: [ "hidden", "text", "search", "tel", "url", "email", "password",
+ "checkbox", "radio", "file", "submit", "image", "reset",
+ "button", "date", "time", "number", "range", "color", "month",
+ "week", "datetime-local" ],
+ invalidValues: [ "this-is-probably-a-wrong-type", "", "tulip" ],
+ defaultValue: "text"
+});
+
+// .defaultValue
+reflectString({
+ element: document.createElement("input"),
+ attribute: { idl: "defaultValue", content: "value" },
+ otherValues: [ "foo\nbar", "foo\rbar", "foo\r\nbar" ],
+});
+
+// .value doesn't reflect a content attribute.
+
+// .valueAsDate
+is("valueAsDate" in document.createElement("input"), true,
+ "valueAsDate should be available");
+
+// Deeper check will be done with bug 763305.
+is('valueAsNumber' in document.createElement("input"), true,
+ "valueAsNumber should be available");
+
+// .selectedOption
+todo("selectedOption" in document.createElement("input"),
+ "selectedOption isn't implemented yet");
+
+// .width
+testWidthHeight('width');
+
+// .willValidate doesn't reflect a content attribute.
+// .validity doesn't reflect a content attribute.
+// .validationMessage doesn't reflect a content attribute.
+// .labels doesn't reflect a content attribute.
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_input_change_events.html b/dom/html/test/forms/test_input_color_input_change_events.html
new file mode 100644
index 0000000000..f97d54f66e
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_input_change_events.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885996
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1234567</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test that update() modifies the element value such as done() when it is
+ * not called as a concellation.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var MockColorPicker = SpecialPowers.MockColorPicker;
+
+ var test = runTest();
+
+ SimpleTest.waitForFocus(function() {
+ test.next();
+ });
+
+ function* runTest() {
+ MockColorPicker.init(window);
+ var element = null;
+
+ MockColorPicker.showCallback = function(picker, update) {
+ is(picker.initialColor, element.value);
+
+ var inputEvent = false;
+ var changeEvent = false;
+ element.oninput = function() {
+ inputEvent = true;
+ };
+ element.onchange = function() {
+ changeEvent = true;
+ };
+
+ if (element.dataset.type == 'update') {
+ update('#f00ba4');
+
+ is(inputEvent, true, 'input event should have been received');
+ is(changeEvent, false, 'change event should not have been received');
+
+ inputEvent = changeEvent = false;
+
+ is(element.value, '#f00ba4');
+
+ MockColorPicker.returnColor = '#f00ba7';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'cancel') {
+ MockColorPicker.returnColor = '#bababa';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'done') {
+ MockColorPicker.returnColor = '#098766';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'noop-done') {
+ MockColorPicker.returnColor = element.value;
+ is(element.value, MockColorPicker.returnColor);
+ }
+
+ SimpleTest.executeSoon(function() {
+ if (element.dataset.type == 'cancel') {
+ isnot(element.value, MockColorPicker.returnColor);
+ is(inputEvent, false, 'no input event should have been sent');
+ is(changeEvent, false, 'no change event should have been sent');
+ } else if (element.dataset.type == 'noop-done') {
+ is(element.value, MockColorPicker.returnColor);
+ is(inputEvent, false, 'no input event should have been sent');
+ is(changeEvent, false, 'no change event should have been sent');
+ } else {
+ is(element.value, MockColorPicker.returnColor);
+ is(inputEvent, true, 'input event should have been sent');
+ is(changeEvent, true, 'change event should have been sent');
+ }
+
+ changeEvent = false;
+ element.blur();
+
+ setTimeout(function() {
+ is(changeEvent, false, "change event should not be fired on blur");
+ test.next();
+ });
+ });
+
+ return element.dataset.type == 'cancel' ? "" : MockColorPicker.returnColor;
+ };
+
+ for (var i = 0; i < document.getElementsByTagName('input').length; ++i) {
+ element = document.getElementsByTagName('input')[i];
+ element.focus();
+ synthesizeMouseAtCenter(element, {});
+ yield undefined;
+ };
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a>
+<p id="display"></p>
+<div id="content">
+ <input type='color' data-type='update'>
+ <input type='color' data-type='cancel'>
+ <input type='color' data-type='done'>
+ <input type='color' data-type='noop-done'>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_picker_datalist.html b/dom/html/test/forms/test_input_color_picker_datalist.html
new file mode 100644
index 0000000000..1a268c0701
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_picker_datalist.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ let MockColorPicker = SpecialPowers.MockColorPicker;
+
+ MockColorPicker.init(window);
+
+ MockColorPicker.showCallback = (picker) => {
+ is(picker.defaultColors.length, 2);
+ is(picker.defaultColors[0], "#112233");
+ is(picker.defaultColors[1], "#00ffaa");
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ let input = document.querySelector("input");
+ synthesizeMouseAtCenter(input, {});
+}
+
+SimpleTest.waitForFocus(runTest);
+</script>
+</head>
+<body>
+<input type="color" list="color-list">
+<datalist id="color-list">
+ <option value="#112233"></option>
+ <option value="black"></option> <!-- invalid -->
+ <option value="#000000" disabled></option>
+ <option value="#00FFAA"></option>
+ <option></option>
+</datalist>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_picker_initial.html b/dom/html/test/forms/test_input_color_picker_initial.html
new file mode 100644
index 0000000000..c7467c7520
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_picker_initial.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885996
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1234567</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test that the initial value of the nsIColorPicker is the current value of
+ the <input type='color'> element. **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ var MockColorPicker = SpecialPowers.MockColorPicker;
+
+ var test = runTest();
+
+ SimpleTest.waitForFocus(function() {
+ test.next();
+ });
+
+ function* runTest() {
+ MockColorPicker.init(window);
+ var element = null;
+
+ MockColorPicker.showCallback = function(picker) {
+ is(picker.initialColor, element.value);
+ SimpleTest.executeSoon(function() {
+ test.next();
+ });
+ return "";
+ };
+
+ for (var i = 0; i < document.getElementsByTagName('input').length; ++i) {
+ element = document.getElementsByTagName('input')[i];
+ if (element.parentElement.id === 'dynamic-values') {
+ element.value = '#deadbe';
+ }
+ synthesizeMouseAtCenter(element, {});
+ yield undefined;
+ };
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a>
+<p id="display"></p>
+<div id="content">
+ <div id='valid-values'>
+ <input type='color' value='#ff00ff'>
+ <input type='color' value='#ab3275'>
+ <input type='color' value='#abcdef'>
+ <input type='color' value='#ABCDEF'>
+ </div>
+ <div id='invalid-values'>
+ <input type='color' value='ffffff'>
+ <input type='color' value='#abcdez'>
+ <input type='color' value='#0123456'>
+ </div>
+ <div id='dynamic-values'>
+ <input type='color' value='#ab4594'>
+ <input type='color' value='#984534'>
+ <input type='color' value='#f8b9a0'>
+ </div>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_picker_popup.html b/dom/html/test/forms/test_input_color_picker_popup.html
new file mode 100644
index 0000000000..9fbebf15bc
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_picker_popup.html
@@ -0,0 +1,144 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885996
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1234567</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style> body { font-family: serif } </style>
+ <script type="application/javascript">
+
+ /** Test the behaviour of the <input type='color'> when clicking on it from
+ different ways. **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ var MockColorPicker = SpecialPowers.MockColorPicker;
+
+ var test = runTest();
+ var testData = [
+ { id: 'normal', result: true },
+ { id: 'hidden', result: false },
+ { id: 'normal', type: 'untrusted', result: true },
+ { id: 'normal', type: 'prevent-default-1', result: false },
+ { id: 'normal', type: 'prevent-default-2', result: false },
+ { id: 'normal', type: 'click-method', result: true },
+ { id: 'normal', type: 'show-picker', result: true },
+ { id: 'normal', type: 'right-click', result: false },
+ { id: 'normal', type: 'middle-click', result: false },
+ { id: 'label-1', result: true },
+ { id: 'label-2', result: true },
+ { id: 'label-3', result: true },
+ { id: 'label-4', result: true },
+ { id: 'button-click', result: true },
+ { id: 'button-down', result: true },
+ { id: 'button-up', result: true },
+ { id: 'div-click', result: true },
+ { id: 'div-click-on-demand', result: true },
+ ];
+
+ SimpleTest.waitForFocus(function() {
+ test.next();
+ });
+
+ function* runTest() {
+ let currentTest = null;
+ MockColorPicker.init(window);
+ var element = null;
+
+ MockColorPicker.showCallback = function(picker) {
+ ok(currentTest.result);
+ SimpleTest.executeSoon(function() {
+ test.next();
+ });
+ return "";
+ };
+
+ while (testData.length) {
+ currentTest = testData.shift();
+ element = document.getElementById(currentTest.id);
+
+ // To make sure we can actually click on the element.
+ element.focus();
+
+ switch (currentTest.type) {
+ case 'untrusted':
+ var e = document.createEvent('MouseEvents');
+ e.initEvent('click', true, false);
+ document.getElementById(element.dispatchEvent(e));
+ break;
+ case 'prevent-default-1':
+ element.onclick = function() {
+ return false;
+ };
+ element.click();
+ element.onclick = function() {};
+ break;
+ case 'prevent-default-2':
+ element.onclick = function(event) {
+ event.preventDefault();
+ };
+ element.click();
+ element.onclick = function() {};
+ break;
+ case 'click-method':
+ element.click();
+ break;
+ case 'show-picker':
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ element.showPicker();
+ break;
+ case 'right-click':
+ synthesizeMouseAtCenter(element, { button: 2 });
+ break;
+ case 'middle-click':
+ synthesizeMouseAtCenter(element, { button: 1 });
+ break;
+ default:
+ synthesizeMouseAtCenter(element, {});
+ }
+
+ if (!currentTest.result) {
+ setTimeout(function() {
+ setTimeout(function() {
+ ok(true);
+ SimpleTest.executeSoon(function() {
+ test.next();
+ });
+ });
+ });
+ }
+ yield undefined;
+ };
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a>
+<p id="display"></p>
+<div id="content">
+ <input type='color' id='normal'>
+ <input type='color' id='hidden' hidden>
+ <label id='label-1'>foo<input type='color'></label>
+ <label id='label-2' for='labeled-2'>foo</label><input id='labeled-2' type='color'></label>
+ <label id='label-3'>foo<input type='color'></label>
+ <label id='label-4' for='labeled-4'>foo</label><input id='labeled-4' type='color'></label>
+ <input id='by-button' type='color'>
+ <button id='button-click' onclick="document.getElementById('by-button').click();">click</button>
+ <button id='button-down' onclick="document.getElementById('by-button').click();">click</button>
+ <button id='button-up' onclick="document.getElementById('by-button').click();">click</button>
+ <div id='div-click' onclick="document.getElementById('by-button').click();">click</div>
+ <div id='div-click-on-demand' onclick="var i=document.createElement('input'); i.type='color'; i.click();">click</div>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_color_picker_update.html b/dom/html/test/forms/test_input_color_picker_update.html
new file mode 100644
index 0000000000..5c22b667e1
--- /dev/null
+++ b/dom/html/test/forms/test_input_color_picker_update.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885996
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1234567</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style> body { font-family: serif } </style>
+ <script type="application/javascript">
+
+ /** Test that update() modifies the element value such as done() when it is
+ * not called as a concellation.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var MockColorPicker = SpecialPowers.MockColorPicker;
+
+ var test = runTest();
+
+ SimpleTest.waitForFocus(function() {
+ test.next();
+ });
+
+ function* runTest() {
+ MockColorPicker.init(window);
+ var element = null;
+
+ MockColorPicker.showCallback = function(picker, update) {
+ is(picker.initialColor, element.value);
+
+ if (element.dataset.type == 'update') {
+ update('#f00ba4');
+ is(element.value, '#f00ba4');
+
+ MockColorPicker.returnColor = '#f00ba7';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'cancel') {
+ MockColorPicker.returnColor = '#bababa';
+ isnot(element.value, MockColorPicker.returnColor);
+ } else if (element.dataset.type == 'done') {
+ MockColorPicker.returnColor = '#098766';
+ isnot(element.value, MockColorPicker.returnColor);
+ }
+
+ SimpleTest.executeSoon(function() {
+ if (element.dataset.type == 'cancel') {
+ isnot(element.value, MockColorPicker.returnColor);
+ } else {
+ is(element.value, MockColorPicker.returnColor);
+ }
+
+ test.next();
+ });
+
+ return element.dataset.type == 'cancel' ? "" : MockColorPicker.returnColor;
+ };
+
+ for (var i = 0; i < document.getElementsByTagName('input').length; ++i) {
+ element = document.getElementsByTagName('input')[i];
+ synthesizeMouseAtCenter(element, {});
+ yield undefined;
+ };
+
+ MockColorPicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a>
+<p id="display"></p>
+<div id="content">
+ <input type='color' data-type='update'>
+ <input type='color' data-type='cancel'>
+ <input type='color' data-type='done'>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_date_bad_input.html b/dom/html/test/forms/test_input_date_bad_input.html
new file mode 100644
index 0000000000..516d48263f
--- /dev/null
+++ b/dom/html/test/forms/test_input_date_bad_input.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1372369
+-->
+<head>
+ <title>Test for &lt;input type='date'&gt; bad input validity state</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input { background-color: rgb(0,0,0) !important; }
+ :valid { background-color: rgb(0,255,0) !important; }
+ :invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1372369">Mozilla Bug 1372369</a>
+<p id="display"></p>
+<div id="content">
+ <form>
+ <input type="date" id="input">
+ <form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for <input type='date'> bad input validity state **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+const DATE_BAD_INPUT_MSG = "Please enter a valid date.";
+const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+
+function checkValidity(aElement, aIsBadInput) {
+ is(aElement.validity.valid, !aIsBadInput,
+ "validity.valid should be " + (aIsBadInput ? "false" : "true"));
+ is(aElement.validity.badInput, !!aIsBadInput,
+ "validity.badInput should be " + (aIsBadInput ? "true" : "false"));
+ is(aElement.validationMessage, aIsBadInput ? DATE_BAD_INPUT_MSG : "",
+ "validationMessage should be: " + (aIsBadInput ? DATE_BAD_INPUT_MSG : ""));
+
+ is(window.getComputedStyle(aElement).getPropertyValue('background-color'),
+ aIsBadInput ? "rgb(255, 0, 0)" : "rgb(0, 255, 0)",
+ (aIsBadInput ? ":invalid" : "valid") + " pseudo-class should apply");
+}
+
+function sendKeys(aKey) {
+ if (aKey.startsWith("KEY_")) {
+ synthesizeKey(aKey);
+ } else {
+ sendString(aKey);
+ }
+}
+
+function test() {
+ var elem = document.getElementById("input");
+
+ elem.focus();
+ sendKeys("02312017");
+ elem.blur();
+ checkValidity(elem, true);
+
+ elem.focus();
+ sendKeys("02292016");
+ elem.blur();
+ checkValidity(elem, false);
+
+ elem.focus();
+ sendKeys("06312000");
+ elem.blur();
+ checkValidity(elem, true);
+
+ // Removing some of the fields keeps the input as invalid.
+ elem.focus();
+ sendKeys("KEY_Backspace");
+ elem.blur();
+ checkValidity(elem, true);
+
+ // Removing all of the fields manually makes the input valid (but empty) again.
+ elem.focus();
+ sendKeys("KEY_ArrowRight");
+ sendKeys("KEY_Backspace");
+ sendKeys("KEY_ArrowRight");
+ sendKeys("KEY_Delete");
+ elem.blur();
+ checkValidity(elem, false);
+
+ elem.focus();
+ sendKeys("02292017");
+ elem.blur();
+ checkValidity(elem, true);
+
+ // Clearing all fields should clear bad input validity state as well.
+ elem.focus();
+ synthesizeKey("KEY_Backspace", { accelKey: true });
+ checkValidity(elem, false);
+
+ sendKeys("22334444");
+ elem.blur();
+ elem.focus();
+ synthesizeKey("KEY_Delete", { accelKey: true });
+ checkValidity(elem, false);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_date_key_events.html b/dom/html/test/forms/test_input_date_key_events.html
new file mode 100644
index 0000000000..387cb37af7
--- /dev/null
+++ b/dom/html/test/forms/test_input_date_key_events.html
@@ -0,0 +1,270 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1286182
+-->
+<head>
+ <title>Test key events for date control</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1286182">Mozilla Bug 1286182</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1804669">Mozilla Bug 1804669</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="date">
+ <div id="host"></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+var testData = [
+ /**
+ * keys: keys to send to the input element.
+ * initialVal: initial value set to the input element.
+ * expectedVal: expected value of the input element after sending the keys.
+ */
+ {
+ // Type 11222016, default order is month, day, year.
+ keys: ["11222016"],
+ initialVal: "",
+ expectedVal: "2016-11-22"
+ },
+ {
+ // Type 3 in the month field will automatically advance to the day field,
+ // then type 5 in the day field will automatically advance to the year
+ // field.
+ keys: ["352016"],
+ initialVal: "",
+ expectedVal: "2016-03-05"
+ },
+ {
+ // Type 13 in the month field will set it to the maximum month, which is
+ // 12.
+ keys: ["13012016"],
+ initialVal: "",
+ expectedVal: "2016-12-01"
+ },
+ {
+ // Type 00 in the month field will set it to the minimum month, which is 1.
+ keys: ["00012016"],
+ initialVal: "",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // Type 33 in the day field will set it to the maximum day, which is 31.
+ keys: ["12332016"],
+ initialVal: "",
+ expectedVal: "2016-12-31"
+ },
+ {
+ // Type 00 in the day field will set it to the minimum day, which is 1.
+ keys: ["12002016"],
+ initialVal: "",
+ expectedVal: "2016-12-01"
+ },
+ {
+ // Type 275769 in the year field will set it to 0069, because the
+ // 5th digit will erase the previous 4 digits.
+ keys: ["0101275769"],
+ initialVal: "",
+ expectedVal: "0069-01-01"
+ },
+ {
+ // Type 0000 in the year field will set it to the minimum year, which is
+ // 0001.
+ keys: ["01010000"],
+ initialVal: "",
+ expectedVal: "0001-01-01"
+ },
+ {
+ // Advance to year field and decrement.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_ArrowDown"],
+ initialVal: "2016-11-25",
+ expectedVal: "2015-11-25"
+ },
+ {
+ // Right key should do the same thing as TAB key.
+ keys: ["KEY_ArrowRight", "KEY_ArrowRight", "KEY_ArrowDown"],
+ initialVal: "2016-11-25",
+ expectedVal: "2015-11-25"
+ },
+ {
+ // Advance to day field then back to month field and decrement.
+ keys: ["KEY_ArrowRight", "KEY_ArrowLeft", "KEY_ArrowDown"],
+ initialVal: "2000-05-01",
+ expectedVal: "2000-04-01"
+ },
+ {
+ // Focus starts on the first field, month in this case, and increment.
+ keys: ["KEY_ArrowUp"],
+ initialVal: "2000-03-01",
+ expectedVal: "2000-04-01"
+ },
+ {
+ // Advance to day field and decrement.
+ keys: ["KEY_Tab", "KEY_ArrowDown"],
+ initialVal: "1234-01-01",
+ expectedVal: "1234-01-31"
+ },
+ {
+ // Advance to day field and increment.
+ keys: ["KEY_Tab", "KEY_ArrowUp"],
+ initialVal: "1234-01-01",
+ expectedVal: "1234-01-02"
+ },
+ {
+ // PageUp on month field increments month by 3.
+ keys: ["KEY_PageUp"],
+ initialVal: "1999-01-01",
+ expectedVal: "1999-04-01"
+ },
+ {
+ // PageDown on month field decrements month by 3.
+ keys: ["KEY_PageDown"],
+ initialVal: "1999-01-01",
+ expectedVal: "1999-10-01"
+ },
+ {
+ // PageUp on day field increments day by 7.
+ keys: ["KEY_Tab", "KEY_PageUp"],
+ initialVal: "1999-01-01",
+ expectedVal: "1999-01-08"
+ },
+ {
+ // PageDown on day field decrements day by 7.
+ keys: ["KEY_Tab", "KEY_PageDown"],
+ initialVal: "1999-01-01",
+ expectedVal: "1999-01-25"
+ },
+ {
+ // PageUp on year field increments year by 10.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_PageUp"],
+ initialVal: "1999-01-01",
+ expectedVal: "2009-01-01"
+ },
+ {
+ // PageDown on year field decrements year by 10.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_PageDown"],
+ initialVal: "1999-01-01",
+ expectedVal: "1989-01-01"
+ },
+ {
+ // Home key on month field sets it to the minimum month, which is 01.
+ keys: ["KEY_Home"],
+ initialVal: "2016-06-01",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // End key on month field sets it to the maximum month, which is 12.
+ keys: ["KEY_End"],
+ initialVal: "2016-06-01",
+ expectedVal: "2016-12-01"
+ },
+ {
+ // Home key on day field sets it to the minimum day, which is 01.
+ keys: ["KEY_Tab", "KEY_Home"],
+ initialVal: "2016-01-10",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // End key on day field sets it to the maximum day, which is 31.
+ keys: ["KEY_Tab", "KEY_End"],
+ initialVal: "2016-01-10",
+ expectedVal: "2016-01-31"
+ },
+ {
+ // Home key should have no effect on year field.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_Home"],
+ initialVal: "2016-01-01",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // End key should have no effect on year field.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_End"],
+ initialVal: "2016-01-01",
+ expectedVal: "2016-01-01"
+ },
+ {
+ // Incomplete value maps to empty .value.
+ keys: ["1111"],
+ initialVal: "",
+ expectedVal: ""
+ },
+ {
+ // Backspace key should clean a month field and map to empty .value.
+ keys: ["KEY_Backspace"],
+ initialVal: "2016-01-01",
+ expectedVal: ""
+ },
+ {
+ // Backspace key should clean a day field and map to empty .value.
+ keys: ["KEY_Tab", "KEY_Backspace"],
+ initialVal: "2016-01-01",
+ expectedVal: ""
+ },
+ {
+ // Backspace key should clean a year field and map to empty .value.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_Backspace"],
+ initialVal: "2016-01-01",
+ expectedVal: ""
+ },
+ {
+ // Backspace key on Calendar button should not change a value.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_Tab", "KEY_Backspace"],
+ initialVal: "2016-01-01",
+ expectedVal: "2016-01-01"
+ },
+];
+
+function sendKeys(aKeys) {
+ for (let i = 0; i < aKeys.length; i++) {
+ let key = aKeys[i];
+ if (key.startsWith("KEY_")) {
+ synthesizeKey(key);
+ } else {
+ sendString(key);
+ }
+ }
+}
+
+function test() {
+ document.querySelector("#host").attachShadow({ mode: "open" }).innerHTML = `
+ <input type="date">
+ `;
+
+ function chromeListener(e) {
+ ok(false, "Picker should not be opened when dispatching untrusted click.");
+ }
+
+ for (const elem of [document.getElementById("input"), document.getElementById("host").shadowRoot.querySelector("input")]) {
+ for (let { keys, initialVal, expectedVal } of testData) {
+ elem.focus();
+ elem.value = initialVal;
+ sendKeys(keys);
+ is(elem.value, expectedVal,
+ "Test with " + keys + ", result should be " + expectedVal);
+ elem.value = "";
+ elem.blur();
+ }
+ SpecialPowers.addChromeEventListener("MozOpenDateTimePicker",
+ chromeListener);
+ elem.click();
+ SpecialPowers.removeChromeEventListener("MozOpenDateTimePicker",
+ chromeListener);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_calendar_button.html b/dom/html/test/forms/test_input_datetime_calendar_button.html
new file mode 100644
index 0000000000..970eee9027
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_calendar_button.html
@@ -0,0 +1,179 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1479708
+-->
+<head>
+<title>Test required date/datetime-local input's Calendar button</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Created for <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1479708">Mozilla Bug 1479708</a> and updated by <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1676068">Mozilla Bug 1676068</a> and <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1865885">Mozilla Bug 1865885</a>
+<p id="display"></p>
+<div id="content">
+<input type="date" id="id_date" value="2017-06-08">
+<input type="time" id="id_time" value="10:30">
+<input type="datetime-local" id="id_datetime-local" value="2017-06-08T10:30">
+<input type="date" id="id_date_required" value="2017-06-08" required>
+<input type="time" id="id_time_required" value="10:30" required>
+<input type="datetime-local" id="id_datetime-local_required" value="2017-06-08T10:30" required>
+<input type="date" id="id_date_readonly" value="2017-06-08" readonly>
+<input type="time" id="id_time_readonly" value="10:30" readonly>
+<input type="datetime-local" id="id_datetime-local_readonly" value="2017-06-08T10:30" readonly>
+<input type="date" id="id_date_disabled" value="2017-06-08" disabled>
+<input type="time" id="id_time_disabled" value="10:30" disabled>
+<input type="datetime-local" id="id_datetime-local_disabled" value="2017-06-08T10:30" disabled>
+</div>
+<pre id="test">
+<script class="testbody">
+
+const kTypes = ["date", "time", "datetime-local"];
+
+function id_for_type(type, kind) {
+ return "id_" + type + (kind ? "_" + kind : "");
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ // Initial load.
+ assert_calendar_visible_all("");
+ assert_calendar_visible_all("required");
+ assert_calendar_hidden_all("readonly");
+ assert_calendar_hidden_all("disabled");
+
+ // Dynamic toggling.
+ test_make_readonly("");
+ test_make_editable("readonly");
+ test_disabled_field_disabled();
+
+ // Now toggle the inputs to the initial state, but while being
+ // display: none. This tests for bug 1567191.
+ for (const input of document.querySelectorAll("input")) {
+ input.style.display = "none";
+ is(input.getBoundingClientRect().width, 0, "Should be undisplayed");
+ }
+
+ test_make_readonly("readonly");
+ test_make_editable("");
+
+ // And test other toggling as well.
+ test_readonly_field_disabled();
+ test_disabled_field_disabled();
+
+ SimpleTest.finish();
+});
+
+function test_disabled_field_disabled() {
+ for (let type of kTypes) {
+ const id = id_for_type(type, "disabled");
+ const input = document.getElementById(id);
+
+ ok(input.disabled, `#${id} Should be disabled`);
+ ok(
+ is_calendar_button_hidden(id),
+ `disabled's Calendar button is hidden (${id})`
+ );
+
+ input.disabled = false;
+ ok(!input.disabled, `#${id} Should not be disabled anymore`);
+ if (type === "time") {
+ assert_calendar_hidden(id);
+ } else {
+ ok(
+ !is_calendar_button_hidden(id),
+ `enabled field's Calendar button is not hidden (${id})`
+ );
+ }
+
+ input.disabled = true; // reset to the original state.
+ }
+}
+
+function test_readonly_field_disabled() {
+ for (let type of kTypes) {
+ const id = id_for_type(type, "readonly");
+ const input = document.getElementById(id);
+
+ ok(input.readOnly, `#${id} Should be read-only`);
+ ok(is_calendar_button_hidden(id), `readonly field's Calendar button is hidden (${id})`);
+
+ input.readOnly = false;
+ ok(!input.readOnly, `#${id} Should not be read-only anymore`);
+ if (type === "time") {
+ assert_calendar_hidden(id);
+ } else {
+ ok(
+ !is_calendar_button_hidden(id),
+ `non-readonly field's Calendar button is not hidden (${id})`
+ );
+ }
+
+ input.readOnly = true; // reset to the original state.
+ }
+}
+
+function test_make_readonly(kind) {
+ for (let type of kTypes) {
+ const id = id_for_type(type, kind);
+ const input = document.getElementById(id);
+ is(input.readOnly, false, `Precondition: input #${id} is editable`);
+
+ input.readOnly = true;
+ assert_calendar_hidden(id);
+ }
+}
+
+function test_make_editable(kind) {
+ for (let type of kTypes) {
+ const id = id_for_type(type, kind);
+ const input = document.getElementById(id);
+ is(input.readOnly, true, `Precondition: input #${id} is read-only`);
+
+ input.readOnly = false;
+ if (type === "time") {
+ assert_calendar_hidden(id);
+ } else {
+ assert_calendar_visible(id);
+ }
+ }
+}
+
+function assert_calendar_visible_all(kind) {
+ for (let type of kTypes) {
+ if (type === "time") {
+ assert_calendar_hidden(id_for_type(type, kind));
+ } else {
+ assert_calendar_visible(id_for_type(type, kind));
+ }
+ }
+}
+function assert_calendar_visible(id) {
+ const isCalendarButtonHidden = is_calendar_button_hidden(id);
+ ok(!isCalendarButtonHidden, `Calendar button is not hidden on #${id}`);
+}
+
+function assert_calendar_hidden_all(kind) {
+ for (let type of kTypes) {
+ assert_calendar_hidden(id_for_type(type, kind));
+ }
+}
+
+function assert_calendar_hidden(id) {
+ const isCalendarButtonHidden = is_calendar_button_hidden(id);
+ ok(isCalendarButtonHidden, `Calendar button is hidden on #${id}`);
+}
+
+function is_calendar_button_hidden(id) {
+ const input = document.getElementById(id);
+ const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
+ const calendarButton = shadowRoot.getElementById("calendar-button");
+ const calendarButtonDisplay = SpecialPowers.wrap(window).getComputedStyle(calendarButton).display;
+ return calendarButtonDisplay === "none";
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_disabled_focus.html b/dom/html/test/forms/test_input_datetime_disabled_focus.html
new file mode 100644
index 0000000000..68a89b1780
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_disabled_focus.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<title>Test for bugs 1772841 and 1865885</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1772841">Mozilla Bug 1772841</a> and <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1865885">Mozilla Bug 1865885</a>
+<div id="content">
+ <!-- Disabled -->
+ <input type="date" id="date" disabled>
+ <input type="time" id="time" disabled>
+ <input type="datetime-local" id="datetime-local" disabled>
+ <fieldset id="fieldset" disabled>
+ <input type="date" id="fieldset-date">
+ <input type="time" id="fieldset-time">
+ <input type="datetime-local" id="fieldset-datetime-local">
+ </fieldset>
+
+ <!-- Dynamically disabled -->
+ <input type="date" id="date1">
+ <input type="time" id="time1">
+ <input type="datetime-local" id="datetime-local1">
+ <fieldset id="fieldset1">
+ <input type="date" id="fieldset-date1">
+ <input type="time" id="fieldset-time1">
+ <input type="datetime-local" id="fieldset-datetime-local1">
+ </fieldset>
+
+ <!-- Dynamically enabled -->
+ <input type="date" id="date2" disabled>
+ <input type="time" id="time2" disabled>
+ <input type="datetime-local" id="datetime-local2" disabled>
+ <fieldset id="fieldset2" disabled>
+ <input type="date" id="fieldset-date2">
+ <input type="time" id="fieldset-time2">
+ <input type="datetime-local" id="fieldset-datetime-local2">
+ </fieldset>
+</div>
+<script>
+ /*
+ * Test for bugs 1772841 and 1865885
+ * This test checks that when a datetime input element is disabled by itself
+ * or from its containing fieldset, it should not be focusable by click.
+ **/
+
+ add_task(async function() {
+ await SimpleTest.promiseFocus(window);
+ for (let inputId of ["time", "date", "datetime-local", "fieldset-time", "fieldset-date", "fieldset-datetime-local"]) {
+ testFocusState(inputId, /* isDisabled = */ true);
+ testDynamicChange(inputId, "1", /* isDisabling = */ true);
+ testDynamicChange(inputId, "2", /* isDisabling = */ false);
+ }
+ })
+ function testFocusState(inputId, isDisabled) {
+ let input = document.getElementById(inputId);
+
+ document.getElementById("content").click();
+ input.click();
+ if (isDisabled) {
+ isnot(document.activeElement, input, `This disabled ${inputId} input should not be focusable by click`);
+ } else {
+ // The click method won't set the focus on clicked input, thus we
+ // only check that the state is changed to enabled here
+ ok(!input.disabled, `This ${inputId} input is not disabled`);
+ }
+
+ document.getElementById("content").click();
+ synthesizeMouseAtCenter(input, {});
+ if (isDisabled) {
+ isnot(document.activeElement, input, `This disabled ${inputId} input should not be focusable by click`);
+ } else {
+ is(document.activeElement, input, `This enabled ${inputId} input should be focusable by click`);
+ }
+ }
+ function testDynamicChange(inputId, index, isDisabling) {
+ if (inputId.split("-")[0] === "fieldset") {
+ document.getElementById("fieldset" + index).disabled = isDisabling;
+ } else {
+ document.getElementById(inputId + index).disabled = isDisabling;
+ }
+ testFocusState(inputId + index, /* isDisabled = */ isDisabling);
+ }
+</script>
diff --git a/dom/html/test/forms/test_input_datetime_focus_blur.html b/dom/html/test/forms/test_input_datetime_focus_blur.html
new file mode 100644
index 0000000000..bff7b2ceb8
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_focus_blur.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
+-->
+<head>
+ <title>Test focus/blur behaviour for date/time input types</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input_time" type="time">
+ <input id="input_date" type="date">
+ <input id="input_datetime-local" type="datetime-local">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1288591.
+ * This test checks whether date/time input types' .focus()/.blur() works
+ * correctly. This test also checks when focusing on an date/time input element,
+ * the focus is redirected to the anonymous text control, but the
+ * document.activeElement still returns date/time input element.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function testFocusBlur(type) {
+ let input = document.getElementById("input_" + type);
+ input.focus();
+
+ // The active element returns the date/time input element.
+ let activeElement = document.activeElement;
+ is(activeElement, input, "activeElement should be the date/time input element");
+ is(activeElement.localName, "input", "activeElement should be an input element");
+ is(activeElement.type, type, "activeElement should be of type " + type);
+
+ // Use FocusManager to check that the actual focus is on the anonymous
+ // text control.
+ let fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]
+ .getService(SpecialPowers.Ci.nsIFocusManager);
+ let focusedElement = fm.focusedElement;
+ is(focusedElement.localName, "span", "focusedElement should be an span element");
+
+ input.blur();
+ isnot(document.activeElement, input, "activeElement should no longer be the datetime input element");
+}
+
+function test() {
+ for (let inputType of ["time", "date", "datetime-local"]) {
+ testFocusBlur(inputType);
+ }
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_focus_blur_events.html b/dom/html/test/forms/test_input_datetime_focus_blur_events.html
new file mode 100644
index 0000000000..2e4e918119
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_focus_blur_events.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1301306
+-->
+<head>
+<title>Test for Bug 1301306</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1301306">Mozilla Bug 722599</a>
+<p id="display"></p>
+<div id="content">
+<input type="time" id="input_time" onfocus="++focusEvents[0]"
+ onblur="++blurEvents[0]" onfocusin="++focusInEvents[0]"
+ onfocusout="++focusOutEvents[0]">
+<input type="date" id="input_date" onfocus="++focusEvents[1]"
+ onblur="++blurEvents[1]" onfocusin="++focusInEvents[1]"
+ onfocusout="++focusOutEvents[1]">
+<input type="datetime-local" id="input_datetime-local" onfocus="++focusEvents[2]"
+ onblur="++blurEvents[2]" onfocusin="++focusInEvents[2]"
+ onfocusout="++focusOutEvents[2]">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/**
+ * Test for Bug 1301306.
+ * This test checks that when moving inside the time input element, e.g. jumping
+ * through the inner text boxes, does not fire extra focus/blur events.
+ **/
+
+var inputTypes = ["time", "date", "datetime-local"];
+var focusEvents = [0, 0, 0];
+var focusInEvents = [0, 0, 0];
+var focusOutEvents = [0, 0, 0];
+var blurEvents = [0, 0, 0];
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function test() {
+ for (var i = 0; i < inputTypes.length; i++) {
+ var input = document.getElementById("input_" + inputTypes[i]);
+
+ input.focus();
+ is(focusEvents[i], 1, inputTypes[i] + " input element should have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event.");
+
+ // Move around inside the input element's input box.
+ synthesizeKey("KEY_Tab");
+ is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " time input element should not have dispatched blur event.");
+
+ synthesizeKey("KEY_ArrowRight");
+ is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event.");
+
+ synthesizeKey("KEY_ArrowLeft");
+ is(focusEvents[i], 1,inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event.");
+
+ synthesizeKey("KEY_ArrowRight");
+ is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event.");
+ is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event.");
+
+ input.blur();
+ is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event.");
+ is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event.");
+ is(focusOutEvents[i], 1, inputTypes[i] + " input element should have dispatched focusout event.");
+ is(blurEvents[i], 1, inputTypes[i] + " input element should have dispatched blur event.");
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_focus_state.html b/dom/html/test/forms/test_input_datetime_focus_state.html
new file mode 100644
index 0000000000..3b771f2394
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_focus_state.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1346085
+-->
+<head>
+ <title>Test moving focus in onfocus/onblur handler</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1346085">Mozilla Bug 1346085</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input_time" type="time">
+ <input id="input_date" type="date">
+ <input id="input_dummy" type="text">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1346085.
+ * This test checks whether date/time input types' focus state are set
+ * correctly, event when moving focus in onfocus/onblur handler.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function testFocusState(type) {
+ let input = document.getElementById("input_" + type);
+
+ input.focus();
+ let focus = document.querySelector(":focus");
+ let focusRing = document.querySelector(":-moz-focusring");
+ is(focus, input, "input should have :focus state after focus");
+ is(focusRing, input, "input should have :-moz-focusring state after focus");
+
+ input.blur();
+ focus = document.querySelector(":focus");
+ focusRing = document.querySelector(":-moz-focusring");
+ isnot(focus, input, "input should not have :focus state after blur");
+ isnot(focusRing, input, "input should not have :-moz-focusring state after blur");
+
+ input.addEventListener("focus", function() {
+ document.getElementById("input_dummy").focus();
+ }, { once: true });
+
+ input.focus();
+ focus = document.querySelector(":focus");
+ focusRing = document.querySelector(":-moz-focusring");
+ isnot(focus, input, "input should not have :focus state when moving focus in onfocus handler");
+ isnot(focusRing, input, "input should not have :-moz-focusring state when moving focus in onfocus handler");
+
+ input.addEventListener("blur", function() {
+ document.getElementById("input_dummy").focus();
+ }, { once: true });
+
+ input.blur();
+ focus = document.querySelector(":focus");
+ focusRing = document.querySelector(":-moz-focusring");
+ isnot(focus, input, "input should not have :focus state when moving focus in onblur handler");
+ isnot(focusRing, input, "input should not have :-moz-focusring state when moving focus in onblur handler");
+}
+
+function test() {
+ let inputTypes = ["time", "date"];
+
+ for (let i = 0; i < inputTypes.length; i++) {
+ testFocusState(inputTypes[i]);
+ }
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_hidden.html b/dom/html/test/forms/test_input_datetime_hidden.html
new file mode 100644
index 0000000000..7d8a6766a9
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_hidden.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1514040
+-->
+<head>
+ <title>Test construction of hidden date input type</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1514040">Mozilla Bug 1514040</a>
+<p id="display"></p>
+<div id="content">
+ <input id="date" type="date" hidden value="1947-02-28">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+let el = document.getElementById("date");
+ok(el.hidden, "element is hidden");
+is(el.value, "1947-02-28", ".value is set correctly");
+let fieldElements = Array.from(SpecialPowers.wrap(el).openOrClosedShadowRoot.querySelectorAll(".datetime-edit-field"));
+is(fieldElements[0].textContent, "02", "month is set");
+is(fieldElements[1].textContent, "28", "day is set");
+is(fieldElements[2].textContent, "1947", "year is set");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_input_change_events.html b/dom/html/test/forms/test_input_datetime_input_change_events.html
new file mode 100644
index 0000000000..63c8012252
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_input_change_events.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1370858
+-->
+<head>
+<title>Test for Bugs 1370858 and 1804881</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1370858">Mozilla Bug 1370858</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1804881">Mozilla Bug 1804881</a>
+<p id="display"></p>
+<div id="content">
+<input type="time" id="input_time" onchange="++changeEvents[0]"
+ oninput="++inputEvents[0]">
+<input type="date" id="input_date" onchange="++changeEvents[1]"
+ oninput="++inputEvents[1]">
+<input type="datetime-local" id="input_datetime-local" onchange="++changeEvents[2]"
+ oninput="++inputEvents[2]">
+</div>
+<pre id="test">
+<script class="testbody">
+
+/**
+ * Test for Bug 1370858.
+ * Test that change and input events are (not) fired for date/time inputs.
+ **/
+
+const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+
+var inputTypes = ["time", "date", "datetime-local"];
+var changeEvents = [0, 0, 0];
+var inputEvents = [0, 0, 0];
+var values = ["10:30", "2017-06-08", "2017-06-08T10:30"];
+var expectedValues = [
+ ["09:30", "01:30", "01:25", "", "01:59", "13:59", ""],
+ ["2017-05-08", "2017-01-08", "2017-01-25", "", "2017-01-31", "2017-01-31", ""],
+ ["2017-05-08T10:30", "2017-01-08T10:30", "2017-01-25T10:30", "", "2017-01-31T10:30", "2017-01-31T10:30", ""]
+];
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function test() {
+ for (var i = 0; i < inputTypes.length; i++) {
+ var input = document.getElementById("input_" + inputTypes[i]);
+
+ is(changeEvents[i], 0, "Number of change events should be 0 at start.");
+ is(inputEvents[i], 0, "Number of input events should be 0 at start.");
+
+ // Test that change and input events are not dispatched setting .value by
+ // script.
+ input.value = values[i];
+ is(input.value, values[i], "Check that value was set correctly (0).");
+ is(changeEvents[i], 0, "Change event should not have dispatched (0).");
+ is(inputEvents[i], 0, "Input event should not have dispatched (0).");
+
+ // Test that change and input events are fired when changing the value using
+ // up/down keys.
+ input.focus();
+ synthesizeKey("KEY_ArrowDown");
+ is(input.value, expectedValues[i][0], "Check that value was set correctly (1).");
+ is(changeEvents[i], 1, "Change event should be dispatched (1).");
+ is(inputEvents[i], 1, "Input event should be dispatched (1).");
+
+ // Test that change and input events are fired when changing the value with
+ // the keyboard.
+ sendString("01");
+ // We get event per character.
+ is(input.value, expectedValues[i][1], "Check that value was set correctly (2).");
+ is(changeEvents[i], 3, "Change event should be dispatched (2).");
+ is(inputEvents[i], 3, "Input event should be dispatched (2).");
+
+ // Test that change and input events are fired when changing the value with
+ // both the numeric keyboard and digit keys.
+ synthesizeKey("2", { code: "Numpad2" });
+ synthesizeKey("5");
+ // We get event per character.
+ is(input.value, expectedValues[i][2], "Check that value was set correctly (3).");
+ is(changeEvents[i], 5, "Change event should be dispatched (3).");
+ is(inputEvents[i], 5, "Input event should be dispatched (3).");
+
+ // Test that change and input events are not fired when navigating with Tab.
+ // Return to the previously focused field (minutes, day, day).
+ synthesizeKey("KEY_Tab", { shiftKey: true });
+ is(input.value, expectedValues[i][2], "Check that value was not changed (4).");
+ is(changeEvents[i], 5, "Change event should not be dispatched (4).");
+ is(inputEvents[i], 5, "Input event should not be dispatched (4).");
+
+ // Test that change and input events are fired when using Backspace.
+ synthesizeKey("KEY_Backspace");
+ // We get event per character.
+ is(input.value, expectedValues[i][3], "Check that value was set correctly (5).");
+ is(changeEvents[i], 6, "Change event should be dispatched (5).");
+ is(inputEvents[i], 6, "Input event should be dispatched (5).");
+
+ // Test that change and input events are fired when using Home key.
+ synthesizeKey("KEY_End");
+ // We get event per character.
+ is(input.value, expectedValues[i][4], "Check that value was set correctly (6).");
+ is(changeEvents[i], 7, "Change event should be dispatched (6).");
+ is(inputEvents[i], 7, "Input event should be dispatched (6).");
+
+ // Test that change and input events are fired for time and not fired
+ // for others when changing the value with a letter key.
+ // Navigate to the next field (time of the day, year, year).
+ synthesizeKey("KEY_Tab");
+ synthesizeKey("P");
+ // We get event per character.
+ is(input.value, expectedValues[i][5], "Check that value was set correctly (7).");
+ if (i === 0) {
+ // For the time input, the time of the day should be focused and it,
+ // as an AM/PM toggle, should change to "PM" when the "p" key is pressed
+ is(changeEvents[i], 8, "Change event should be dispatched (7).");
+ is(inputEvents[i], 8, "Input event should be dispatched (7).");
+ } else {
+ // For the date and datetime inputs, the year should be focused and it,
+ // as a numeric value, should not change when the "p" key is pressed
+ is(changeEvents[i], 7, "Change event should not be dispatched (7).");
+ is(inputEvents[i], 7, "Input event should not be dispatched (7).");
+ }
+
+ // Test that change and input events are fired when clearing the value
+ // using a Ctrl/Cmd+Delete/Backspace key combination
+ let events = (i === 0) ? 9 : 8;
+ synthesizeKey("KEY_Backspace", { accelKey: true });
+ // We get one event
+ is(input.value, expectedValues[i][6], "Check that value was cleared out correctly (8).");
+ is(changeEvents[i], events, "Change event should be dispatched (8).");
+ is(inputEvents[i], events, "Input event should be dispatched (8).");
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_readonly.html b/dom/html/test/forms/test_input_datetime_readonly.html
new file mode 100644
index 0000000000..aa7b40753b
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_readonly.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<title>Test for bug 1461509</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<input id="i" type="date" value="1995-11-20" readonly required>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ let input = document.getElementById("i");
+ let value = input.value;
+
+ isnot(value, "", "should have a value");
+
+ input.focus();
+ synthesizeKey("KEY_Backspace");
+ is(input.value, value, "Value shouldn't change");
+ SimpleTest.finish();
+});
+</script>
diff --git a/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html b/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html
new file mode 100644
index 0000000000..393de9fdee
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1446722
+-->
+<head>
+<title>Test for bug 1446722</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script type="application/javascript" src="utils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1446722">Mozilla bug 1446722</a>
+<p id="display"></p>
+<div id="content">
+<form>
+<input type="time" id="input_time" value="10:30" onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="date" id="input_date" value="2012-05-06" onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="time" id="input_time2" value="11:30" onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="date" id="input_date2" value="2014-07-08"
+ onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="time" id="input_time3" value="12:30" onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="date" id="input_date3" value="2014-08-09"
+ onchange="++numberChangeEvents"
+ oninput="++numberInputEvents">
+<input type="reset" id="input_reset">
+</form>
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript">
+
+/**
+ * Test for bug 1446722.
+ *
+ * Test change and input events are fired for date and time inputs when the
+ * default value is reset from the date UI and the time UI.
+ * Test they are not fired when the value is changed via a script.
+ * Test clicking the reset button of a form does not fire these events.
+ **/
+
+const INPUT_FIELD_ID_PREFIX = "input_";
+
+var numberChangeEvents = 0;
+var numberInputEvents = 0;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test_reset_in_script_does_not_trigger_change_and_input_event(
+ "time2", numberChangeEvents, numberInputEvents);
+ test_reset_in_script_does_not_trigger_change_and_input_event(
+ "date2", numberChangeEvents, numberInputEvents);
+
+ test_reset_form_does_not_trigger_change_and_input_events("time3", "14:00",
+ numberChangeEvents, numberInputEvents);
+ test_reset_form_does_not_trigger_change_and_input_events("date3", "2016-01-01",
+ numberChangeEvents, numberInputEvents);
+
+ SimpleTest.finish();
+});
+
+function test_reset_in_script_does_not_trigger_change_and_input_event(
+ inputFieldIdSuffix, oldNumberChangeEvents, oldNumberInputEvents) {
+ const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix;
+ var input = document.getElementById(inputFieldName);
+
+ is(input.value, input.defaultValue,
+ "Check " + inputFieldName + "'s default value is initialized correctly.");
+ is(numberChangeEvents, oldNumberChangeEvents,
+ "Check numberChangeEvents is initialized correctly for " + inputFieldName +
+ ".");
+ is(numberInputEvents, oldNumberInputEvents,
+ "Check numberInputEvents is initialized correctly for " + inputFieldName +
+ ".");
+
+ input.value = "";
+
+ is(numberChangeEvents, oldNumberChangeEvents,
+ "Change event should not be dispatched for " + inputFieldName + ".");
+ is(numberInputEvents, oldNumberInputEvents,
+ "Input event should not be dispatched for " + inputFieldName + ".");
+}
+
+function test_reset_form_does_not_trigger_change_and_input_events(
+ inputFieldIdSuffix, newValue, oldNumberChangeEvents, oldNumberInputEvents) {
+ const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix;
+ const inputFieldResetButtonName = "input_reset";
+ var input = document.getElementById(inputFieldName);
+
+ is(input.value, input.defaultValue,
+ "Check " + inputFieldName + "'s default value is initialized correctly.");
+ isnot(input.defaultValue, newValue, "Check default value differs from newValue for " +
+ inputFieldName + ".");
+ is(numberChangeEvents, oldNumberChangeEvents,
+ "Check numberChangeEvents is initialized correctly for " + inputFieldName +
+ ".");
+ is(numberInputEvents, oldNumberInputEvents,
+ "Check numberInputEvents is initialized correctly for " + inputFieldName +
+ ".");
+
+ input.value = newValue;
+
+ var resetButton = document.getElementById(inputFieldResetButtonName);
+ synthesizeMouseAtCenter(resetButton, {});
+
+ is(input.value, input.defaultValue, "Check value is reset to default for " +
+ inputFieldName + ".");
+ is(numberChangeEvents, oldNumberChangeEvents,
+ "Change event should not be dispatched for " + inputFieldResetButtonName + ".");
+ is(numberInputEvents, oldNumberInputEvents,
+ "Input event should not be dispatched for " + inputFieldResetButtonName + ".");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_datetime_tabindex.html b/dom/html/test/forms/test_input_datetime_tabindex.html
new file mode 100644
index 0000000000..207a7a8a8e
--- /dev/null
+++ b/dom/html/test/forms/test_input_datetime_tabindex.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
+-->
+<head>
+ <title>Test tabindex attribute for date/time input types</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a>
+<p id="display"></p>
+<div id="content">
+ <input id="time1" type="time" tabindex="0">
+ <input id="time2" type="time" tabindex="-1">
+ <input id="time3" type="time" tabindex="0">
+ <input id="time4" type="time" disabled>
+ <input id="date1" type="date" tabindex="0">
+ <input id="date2" type="date" tabindex="-1">
+ <input id="date3" type="date" tabindex="0">
+ <input id="date4" type="date" disabled>
+ <input id="datetime-local1" type="datetime-local" tabindex="0">
+ <input id="datetime-local2" type="datetime-local" tabindex="-1">
+ <input id="datetime-local3" type="datetime-local" tabindex="0">
+ <input id="datetime-local4" type="datetime-local" disabled>
+</div>
+<pre id="test">
+<script>
+/**
+ * Test for Bug 1288591.
+ * This test checks whether date/time input types tabindex attribute works
+ * correctly.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function checkInnerTextboxTabindex(input, tabindex) {
+ let fields = SpecialPowers.wrap(input).openOrClosedShadowRoot.querySelectorAll(".datetime-edit-field");
+
+ for (let field of fields) {
+ is(field.tabIndex, tabindex, "tabIndex in the inner textbox should be correct");
+ }
+
+}
+
+function testTabindex(type) {
+ let input1 = document.getElementById(type + "1");
+ let input2 = document.getElementById(type + "2");
+ let input3 = document.getElementById(type + "3");
+ let input4 = document.getElementById(type + "4");
+
+ input1.focus();
+ is(document.activeElement, input1,
+ "input element with tabindex=0 is focusable");
+
+ // Time input does not include a Calendar button
+ let fieldCount;
+ if (type == "datetime-local") {
+ fieldCount = 7;
+ } else if (type == "date") {
+ fieldCount = 4;
+ } else {
+ fieldCount = 3;
+ };
+
+ // Advance through inner fields.
+ for (let i = 0; i < fieldCount - 1; ++i) {
+ synthesizeKey("KEY_Tab");
+ is(document.activeElement, input1,
+ "input element with tabindex=0 is tabbable");
+ }
+
+ // Advance to next element
+ synthesizeKey("KEY_Tab");
+ is(document.activeElement, input3,
+ "input element with tabindex=-1 is not tabbable");
+
+ input2.focus();
+ is(document.activeElement, input2,
+ "input element with tabindex=-1 is still focusable");
+
+ checkInnerTextboxTabindex(input1, 0);
+ checkInnerTextboxTabindex(input2, -1);
+ checkInnerTextboxTabindex(input3, 0);
+
+ // Changing the tabindex attribute dynamically.
+ input3.setAttribute("tabindex", "-1");
+
+ synthesizeKey("KEY_Tab"); // need only one TAB since input2 is not tabbable
+
+ isnot(document.activeElement, input3,
+ "element with tabindex changed to -1 should not be tabbable");
+ isnot(document.activeElement, input4,
+ "disabled element should not be tabbable");
+
+ checkInnerTextboxTabindex(input3, -1);
+}
+
+function test() {
+ for (let inputType of ["time", "date", "datetime-local"]) {
+ testTabindex(inputType);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_defaultValue.html b/dom/html/test/forms/test_input_defaultValue.html
new file mode 100644
index 0000000000..03849d7f54
--- /dev/null
+++ b/dom/html/test/forms/test_input_defaultValue.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=977029
+-->
+<head>
+ <title>Test for Bug 977029</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<div id="content">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=977029">Bug 977029</a>
+ <p>
+ Goal of this test is to check that modifying defaultValue and value attribute
+ of input types is working as expected.
+ </p>
+ <form>
+ <input id='a' type="color" value="#00ff00">
+ <input id='b' type="text" value="foo">
+ <input id='c' type="email" value="foo">
+ <input id='d' type="date" value="2010-09-20">
+ <input id='e' type="search" value="foo">
+ <input id='f' type="tel" value="foo">
+ <input id='g' type="url" value="foo">
+ <input id='h' type="number" value="42">
+ <input id='i' type="range" value="42" min="0" max="100">
+ <input id='j' type="time" value="17:00:25.54">
+ </form>
+</div>
+<script type="application/javascript">
+
+// [ element id | original defaultValue | another value | another default value]
+// Preferably use only valid values: the goal of this test isn't to test the
+// value sanitization algorithm (for input types which have one) as this is
+// already part of another test)
+var testData = [["a", "#00ff00", "#00aaaa", "#00ccaa"],
+ ["b", "foo", "bar", "tulip"],
+ ["c", "foo", "foo@bar.org", "tulip"],
+ ["d", "2010-09-20", "2012-09-21", ""],
+ ["e", "foo", "bar", "tulip"],
+ ["f", "foo", "bar", "tulip"],
+ ["g", "foo", "bar", "tulip"],
+ ["h", "42", "1337", "3"],
+ ["i", "42", "17", "3"],
+ ["j", "17:00:25.54", "07:00:25", "03:00:03"],
+ ];
+
+for (var data of testData) {
+ id = data[0];
+ input = document.getElementById(id);
+ originalDefaultValue = data[1];
+ is(originalDefaultValue, input.defaultValue,
+ "Default value isn't the expected one");
+ is(originalDefaultValue, input.value,
+ "input.value original value is different from defaultValue");
+ input.defaultValue = data[2]
+ is(input.defaultValue, input.value,
+ "Changing default value before value was changed should change value too");
+ input.value = data[3];
+ input.defaultValue = originalDefaultValue;
+ is(input.value, data[3],
+ "Changing default value after value was changed should not change value");
+ input.value = data[2];
+ is(originalDefaultValue, input.defaultValue,
+ "defaultValue shouldn't change when changing value");
+ input.defaultValue = data[3];
+ is(input.defaultValue, data[3],
+ "defaultValue should have changed");
+ // Change the value...
+ input.value = data[2];
+ is(input.value, data[2],
+ "value should have changed");
+ // ...then reset the form
+ input.form.reset();
+ is(input.defaultValue, input.value,
+ "reset form should bring back the default value");
+}
+</script>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_input_email.html b/dom/html/test/forms/test_input_email.html
new file mode 100644
index 0000000000..96ff939215
--- /dev/null
+++ b/dom/html/test/forms/test_input_email.html
@@ -0,0 +1,237 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=555559
+https://bugzilla.mozilla.org/show_bug.cgi?id=668817
+https://bugzilla.mozilla.org/show_bug.cgi?id=854812
+-->
+<head>
+ <title>Test for &lt;input type='email'&gt; validity</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=555559">Mozilla Bug 555559</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=668817">Mozilla Bug 668817</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=854812">Mozilla Bug 854812</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form>
+ <input type='email' name='email' id='i' oninvalid="invalidEventHandler(event);">
+ <form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for <input type='email'> validity **/
+
+var gInvalid = false;
+
+function invalidEventHandler(e)
+{
+ is(e.type, "invalid", "Invalid event type should be invalid");
+ gInvalid = true;
+}
+
+function checkValidEmailAddress(element)
+{
+ gInvalid = false;
+ ok(!element.validity.typeMismatch && !element.validity.badInput,
+ "Element should not suffer from type mismatch or bad input (with value='"+element.value+"')");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "Element should be valid");
+ ok(!gInvalid, "The invalid event should not have been thrown");
+ is(element.validationMessage, '',
+ "Validation message should be the empty string");
+ ok(element.matches(":valid"), ":valid pseudo-class should apply");
+}
+
+const VALID = 0;
+const TYPE_MISMATCH = 1 << 0;
+const BAD_INPUT = 1 << 1;
+
+function checkInvalidEmailAddress(element, failedValidityStates)
+{
+ info("Checking " + element.value);
+ gInvalid = false;
+ var expectTypeMismatch = !!(failedValidityStates & TYPE_MISMATCH);
+ var expectBadInput = !!(failedValidityStates & BAD_INPUT);
+ ok(element.validity.typeMismatch == expectTypeMismatch,
+ "Element should " + (expectTypeMismatch ? "" : "not ") + "suffer from type mismatch (with value='"+element.value+"')");
+ ok(element.validity.badInput == expectBadInput,
+ "Element should " + (expectBadInput ? "" : "not ") + "suffer from bad input (with value='"+element.value+"')");
+ ok(!element.validity.valid, "Element should not be valid");
+ ok(!element.checkValidity(), "Element should not be valid");
+ ok(gInvalid, "The invalid event should have been thrown");
+ is(element.validationMessage, "Please enter an email address.",
+ "Validation message is not valid");
+ ok(element.matches(":invalid"), ":invalid pseudo-class should apply");
+}
+
+function testEmailAddress(aElement, aValue, aMultiple, aValidityFailures)
+{
+ aElement.multiple = aMultiple;
+ aElement.value = aValue;
+
+ if (!aValidityFailures) {
+ checkValidEmailAddress(aElement);
+ } else {
+ checkInvalidEmailAddress(aElement, aValidityFailures);
+ }
+}
+
+var email = document.forms[0].elements[0];
+
+// Simple values, checking the e-mail syntax validity.
+var values = [
+ [ '' ], // The empty string shouldn't be considered as invalid.
+ [ 'foo@bar.com', VALID ],
+ [ ' foo@bar.com', VALID ],
+ [ 'foo@bar.com ', VALID ],
+ [ '\r\n foo@bar.com', VALID ],
+ [ 'foo@bar.com \n\r', VALID ],
+ [ '\n\n \r\rfoo@bar.com\n\n \r\r', VALID ],
+ [ '\n\r \n\rfoo@bar.com\n\r \n\r', VALID ],
+ [ 'tulip', TYPE_MISMATCH ],
+ // Some checks on the user part of the address.
+ [ '@bar.com', TYPE_MISMATCH ],
+ [ 'f\noo@bar.com', VALID ],
+ [ 'f\roo@bar.com', VALID ],
+ [ 'f\r\noo@bar.com', VALID ],
+ [ 'fü@foo.com', TYPE_MISMATCH ],
+ // Some checks for the domain part.
+ [ 'foo@bar', VALID ],
+ [ 'foo@b', VALID ],
+ [ 'foo@', TYPE_MISMATCH ],
+ [ 'foo@bar.', TYPE_MISMATCH ],
+ [ 'foo@foo.bar', VALID ],
+ [ 'foo@foo..bar', TYPE_MISMATCH ],
+ [ 'foo@.bar', TYPE_MISMATCH ],
+ [ 'foo@tulip.foo.bar', VALID ],
+ [ 'foo@tulip.foo-bar', VALID ],
+ [ 'foo@1.2', VALID ],
+ [ 'foo@127.0.0.1', VALID ],
+ [ 'foo@1.2.3', VALID ],
+ [ 'foo@b\nar.com', VALID ],
+ [ 'foo@b\rar.com', VALID ],
+ [ 'foo@b\r\nar.com', VALID ],
+ [ 'foo@.', TYPE_MISMATCH ],
+ [ 'foo@fü.com', VALID ],
+ [ 'foo@fu.cüm', VALID ],
+ [ 'thisUsernameIsLongerThanSixtyThreeCharactersInLengthRightAboutNow@mozilla.tld', VALID ],
+ // Long strings with UTF-8 in username.
+ [ 'this.is.email.should.be.longer.than.sixty.four.characters.föö@mözillä.tld', TYPE_MISMATCH ],
+ [ 'this-is-email-should-be-longer-than-sixty-four-characters-föö@mözillä.tld', TYPE_MISMATCH, true ],
+ // Long labels (labels greater than 63 chars long are not allowed).
+ [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss', VALID ],
+ [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', VALID ],
+ [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', VALID ],
+ [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss', VALID ],
+ [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+ // Long labels with UTF-8 (punycode encoding will increase the label to more than 63 chars).
+ [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+ [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+ // The domains labels (sub-domains or tld) can't start or finish with a '-'
+ [ 'foo@foo-bar', VALID ],
+ [ 'foo@-foo', TYPE_MISMATCH ],
+ [ 'foo@foo-.bar', TYPE_MISMATCH ],
+ [ 'foo@-.-', TYPE_MISMATCH ],
+ [ 'foo@fo-o.bar', VALID ],
+ [ 'foo@fo-o.-bar', TYPE_MISMATCH ],
+ [ 'foo@fo-o.bar-', TYPE_MISMATCH ],
+ [ 'foo@fo-o.-', TYPE_MISMATCH ],
+ [ 'foo@fo--o', VALID ],
+];
+
+// Multiple values, we don't check e-mail validity, only multiple stuff.
+var multipleValues = [
+ [ 'foo@bar.com, foo@bar.com', VALID ],
+ [ 'foo@bar.com,foo@bar.com', VALID ],
+ [ 'foo@bar.com,foo@bar.com,foo@bar.com', VALID ],
+ [ ' foo@bar.com , foo@bar.com ', VALID ],
+ [ '\tfoo@bar.com\t,\tfoo@bar.com\t', VALID ],
+ [ '\rfoo@bar.com\r,\rfoo@bar.com\r', VALID ],
+ [ '\nfoo@bar.com\n,\nfoo@bar.com\n', VALID ],
+ [ '\ffoo@bar.com\f,\ffoo@bar.com\f', VALID ],
+ [ '\t foo@bar.com\r,\nfoo@bar.com\f', VALID ],
+ [ 'foo@b,ar.com,foo@bar.com', TYPE_MISMATCH ],
+ [ 'foo@bar.com,foo@bar.com,', TYPE_MISMATCH ],
+ [ ' foo@bar.com , foo@bar.com , ', TYPE_MISMATCH ],
+ [ ',foo@bar.com,foo@bar.com', TYPE_MISMATCH ],
+ [ ',foo@bar.com,foo@bar.com', TYPE_MISMATCH ],
+ [ 'foo@bar.com,,,foo@bar.com', TYPE_MISMATCH ],
+ [ 'foo@bar.com;foo@bar.com', TYPE_MISMATCH ],
+ [ '<foo@bar.com>, <foo@bar.com>', TYPE_MISMATCH ],
+ [ 'foo@bar, foo@bar.com', VALID ],
+ [ 'foo@bar.com, foo', TYPE_MISMATCH ],
+ [ 'foo, foo@bar.com', TYPE_MISMATCH ],
+];
+
+/* Additional username checks. */
+
+var legalCharacters = "abcdefghijklmnopqrstuvwxyz";
+legalCharacters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+legalCharacters += "0123456789";
+legalCharacters += "!#$%&'*+-/=?^_`{|}~.";
+
+// Add all username legal characters individually to the list.
+for (c of legalCharacters) {
+ values.push([c + "@bar.com", VALID]);
+}
+// Add the concatenation of all legal characters too.
+values.push([legalCharacters + "@bar.com", VALID]);
+
+// Add username illegal characters, the same way.
+var illegalCharacters = "()<>[]:;@\\, \t";
+for (c of illegalCharacters) {
+ values.push([illegalCharacters + "@bar.com", TYPE_MISMATCH]);
+}
+
+/* Additional domain checks. */
+
+legalCharacters = "abcdefghijklmnopqrstuvwxyz";
+legalCharacters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+legalCharacters += "0123456789";
+
+// Add domain legal characters (except '.' and '-' because they are special).
+for (c of legalCharacters) {
+ values.push(["foo@foo.bar" + c, VALID]);
+}
+// Add the concatenation of all legal characters too.
+values.push(["foo@bar." + legalCharacters, VALID]);
+
+// Add domain illegal characters.
+illegalCharacters = "()<>[]:;@\\,!#$%&'*+/=?^_`{|}~ \t";
+for (c of illegalCharacters) {
+ values.push(['foo@foo.ba' + c + 'r', TYPE_MISMATCH]);
+}
+
+values.forEach(function([value, valid, todo]) {
+ if (todo === true) {
+ email.value = value;
+ todo_is(email.validity.valid, true, "value should be valid");
+ } else {
+ testEmailAddress(email, value, false, valid);
+ }
+});
+
+multipleValues.forEach(function([value, valid]) {
+ testEmailAddress(email, value, true, valid);
+});
+
+// Make sure setting multiple changes the value.
+email.multiple = false;
+email.value = "foo@bar.com, foo@bar.com";
+checkInvalidEmailAddress(email, TYPE_MISMATCH);
+email.multiple = true;
+checkValidEmailAddress(email);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_event.html b/dom/html/test/forms/test_input_event.html
new file mode 100644
index 0000000000..72863ca335
--- /dev/null
+++ b/dom/html/test/forms/test_input_event.html
@@ -0,0 +1,409 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=851780
+-->
+<head>
+<title>Test for input event</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=851780">Mozilla Bug 851780</a>
+<p id="display"></p>
+<div id="content"></div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ /** Test for input event. This is highly based on test_change_event.html **/
+
+ const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
+
+ let expectedInputType = "";
+ let expectedData = null;
+ let expectedBeforeInputCancelable = false;
+ function checkBeforeInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"beforeinput" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.inputType, expectedInputType,
+ `inputType of "beforeinput" event should be "${expectedInputType}" ${aDescription}`);
+ is(aEvent.data, expectedData,
+ `data of "beforeinput" event should be ${expectedData} ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer of "beforeinput" event should be null ${aDescription}`);
+ is(aEvent.getTargetRanges().length, 0,
+ `getTargetRanges() of "beforeinput" event should return empty array ${aDescription}`);
+ is(aEvent.cancelable, expectedBeforeInputCancelable,
+ `"beforeinput" event for "${expectedInputType}" should ${expectedBeforeInputCancelable ? "be" : "not be"} cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"beforeinput" event should always bubble ${aDescription}`);
+ }
+
+ let skipExpectedDataCheck = false;
+ function checkIfInputIsInputEvent(aEvent, aDescription) {
+ ok(aEvent instanceof InputEvent,
+ `"input" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.inputType, expectedInputType,
+ `inputType should be "${expectedInputType}" ${aDescription}`);
+ if (!skipExpectedDataCheck)
+ is(aEvent.data, expectedData, `data should be ${expectedData} ${aDescription}`);
+ else
+ info(`data is ${aEvent.data} ${aDescription}`);
+ is(aEvent.dataTransfer, null,
+ `dataTransfer should be null ${aDescription}`);
+ is(aEvent.cancelable, false,
+ `"input" event should be never cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"input" event should always bubble ${aDescription}`);
+ }
+
+ function checkIfInputIsEvent(aEvent, aDescription) {
+ ok(aEvent instanceof Event && !(aEvent instanceof UIEvent),
+ `"input" event should be dispatched with InputEvent interface ${aDescription}`);
+ is(aEvent.cancelable, false,
+ `"input" event should be never cancelable ${aDescription}`);
+ is(aEvent.bubbles, true,
+ `"input" event should always bubble ${aDescription}`);
+ }
+
+ let textareaInput = 0, textareaBeforeInput = 0;
+ let textTypes = ["text", "email", "search", "tel", "url", "password"];
+ let textBeforeInput = [0, 0, 0, 0, 0, 0];
+ let textInput = [0, 0, 0, 0, 0, 0];
+ let nonTextTypes = ["button", "submit", "image", "reset", "radio", "checkbox"];
+ let nonTextBeforeInput = [0, 0, 0, 0, 0, 0];
+ let nonTextInput = [0, 0, 0, 0, 0, 0];
+ let rangeInput = 0, rangeBeforeInput = 0;
+ let numberInput = 0, numberBeforeInput = 0;
+
+ // Don't create elements whose event listener attributes are required before enabling `beforeinput` event.
+ function init() {
+ document.getElementById("content").innerHTML =
+ `<input type="file" id="fileInput">
+ <textarea id="textarea"></textarea>
+ <input type="text" id="input_text">
+ <input type="email" id="input_email">
+ <input type="search" id="input_search">
+ <input type="tel" id="input_tel">
+ <input type="url" id="input_url">
+ <input type="password" id="input_password">
+
+ <!-- "Non-text" inputs-->
+ <input type="button" id="input_button">
+ <input type="submit" id="input_submit">
+ <input type="image" id="input_image">
+ <input type="reset" id="input_reset">
+ <input type="radio" id="input_radio">
+ <input type="checkbox" id="input_checkbox">
+ <input type="range" id="input_range">
+ <input type="number" id="input_number">`;
+
+ document.getElementById("textarea").addEventListener("beforeinput", (aEvent) => {
+ ++textareaBeforeInput;
+ checkBeforeInputEvent(aEvent, "on textarea element");
+ });
+ document.getElementById("textarea").addEventListener("input", (aEvent) => {
+ ++textareaInput;
+ checkIfInputIsInputEvent(aEvent, "on textarea element");
+ });
+
+ // These are the type were the input event apply.
+ for (let id of ["input_text", "input_email", "input_search", "input_tel", "input_url", "input_password"]) {
+ document.getElementById(id).addEventListener("beforeinput", (aEvent) => {
+ ++textBeforeInput[textTypes.indexOf(aEvent.target.type)];
+ checkBeforeInputEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
+ });
+ document.getElementById(id).addEventListener("input", (aEvent) => {
+ ++textInput[textTypes.indexOf(aEvent.target.type)];
+ checkIfInputIsInputEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
+ });
+ }
+
+ // These are the type were the input event does not apply.
+ for (let id of ["input_button", "input_submit", "input_image", "input_reset", "input_radio", "input_checkbox"]) {
+ document.getElementById(id).addEventListener("beforeinput", (aEvent) => {
+ ++nonTextBeforeInput[nonTextTypes.indexOf(aEvent.target.type)];
+ });
+ document.getElementById(id).addEventListener("input", (aEvent) => {
+ ++nonTextInput[nonTextTypes.indexOf(aEvent.target.type)];
+ checkIfInputIsEvent(aEvent, `on input element whose type is ${aEvent.target.type}`);
+ });
+ }
+
+ document.getElementById("input_range").addEventListener("beforeinput", (aEvent) => {
+ ++rangeBeforeInput;
+ });
+ document.getElementById("input_range").addEventListener("input", (aEvent) => {
+ ++rangeInput;
+ checkIfInputIsEvent(aEvent, "on input element whose type is range");
+ });
+
+ document.getElementById("input_number").addEventListener("beforeinput", (aEvent) => {
+ ++numberBeforeInput;
+ });
+ document.getElementById("input_number").addEventListener("input", (aEvent) => {
+ ++numberInput;
+ checkIfInputIsInputEvent(aEvent, "on input element whose type is number");
+ });
+ }
+
+ var MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ function testUserInput() {
+ // Simulating an OK click and with a file name return.
+ MockFilePicker.useBlobFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ var input = document.getElementById('fileInput');
+ input.focus();
+
+ input.addEventListener("beforeinput", function (aEvent) {
+ ok(false, "beforeinput event shouldn't be dispatched on file input.");
+ });
+ input.addEventListener("input", function (aEvent) {
+ ok(true, "input event should've been dispatched on file input.");
+ checkIfInputIsEvent(aEvent, "on file input");
+ });
+
+ input.click();
+ SimpleTest.executeSoon(testUserInput2);
+ }
+
+ function testUserInput2() {
+ // Some generic checks for types that support the input event.
+ for (var i = 0; i < textTypes.length; ++i) {
+ input = document.getElementById("input_" + textTypes[i]);
+ input.focus();
+ expectedInputType = "insertLineBreak";
+ expectedData = null;
+ expectedBeforeInputCancelable = true;
+ synthesizeKey("KEY_Enter");
+ is(textBeforeInput[i], 1, "beforeinput event should've been dispatched on " + textTypes[i] + " input element");
+ is(textInput[i], 0, "input event shouldn't be dispatched on " + textTypes[i] + " input element");
+
+ expectedInputType = "insertText";
+ expectedData = "m";
+ expectedBeforeInputCancelable = true;
+ sendString("m");
+ is(textBeforeInput[i], 2, textTypes[i] + " input element should've been dispatched beforeinput event.");
+ is(textInput[i], 1, textTypes[i] + " input element should've been dispatched input event.");
+ expectedInputType = "insertLineBreak";
+ expectedData = null;
+ expectedBeforeInputCancelable = true;
+ synthesizeKey("KEY_Enter", {shiftKey: true});
+ is(textBeforeInput[i], 3, "input event should've been dispatched on " + textTypes[i] + " input element");
+ is(textInput[i], 1, "input event shouldn't be dispatched on " + textTypes[i] + " input element");
+
+ expectedInputType = "deleteContentBackward";
+ expectedData = null;
+ expectedBeforeInputCancelable = true;
+ synthesizeKey("KEY_Backspace");
+ is(textBeforeInput[i], 4, textTypes[i] + " input element should've been dispatched beforeinput event.");
+ is(textInput[i], 2, textTypes[i] + " input element should've been dispatched input event.");
+ }
+
+ // Some scenarios of value changing from script and from user input.
+ input = document.getElementById("input_text");
+ input.focus();
+ expectedInputType = "insertText";
+ expectedData = "f";
+ expectedBeforeInputCancelable = true;
+ sendString("f");
+ is(textBeforeInput[0], 5, "beforeinput event should've been dispatched");
+ is(textInput[0], 3, "input event should've been dispatched");
+ input.blur();
+ is(textBeforeInput[0], 5, "input event should not have been dispatched");
+ is(textInput[0], 3, "input event should not have been dispatched");
+
+ input.focus();
+ input.value = 'foo';
+ is(textBeforeInput[0], 5, "beforeinput event should not have been dispatched");
+ is(textInput[0], 3, "input event should not have been dispatched");
+ input.blur();
+ is(textBeforeInput[0], 5, "beforeinput event should not have been dispatched");
+ is(textInput[0], 3, "input event should not have been dispatched");
+
+ input.focus();
+ expectedInputType = "insertText";
+ expectedData = "f";
+ expectedBeforeInputCancelable = true;
+ sendString("f");
+ is(textBeforeInput[0], 6, "beforeinput event should've been dispatched");
+ is(textInput[0], 4, "input event should've been dispatched");
+ input.value = 'bar';
+ is(textBeforeInput[0], 6, "beforeinput event should not have been dispatched");
+ is(textInput[0], 4, "input event should not have been dispatched");
+ input.blur();
+ is(textBeforeInput[0], 6, "beforeinput event should not have been dispatched");
+ is(textInput[0], 4, "input event should not have been dispatched");
+
+ // Same for textarea.
+ var textarea = document.getElementById("textarea");
+ textarea.focus();
+ expectedInputType = "insertText";
+ expectedData = "f";
+ expectedBeforeInputCancelable = true;
+ sendString("f");
+ is(textareaBeforeInput, 1, "beforeinput event should've been dispatched");
+ is(textareaInput, 1, "input event should've been dispatched");
+ textarea.blur();
+ is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched");
+ is(textareaInput, 1, "input event should not have been dispatched");
+
+ textarea.focus();
+ textarea.value = 'foo';
+ is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched");
+ is(textareaInput, 1, "input event should not have been dispatched");
+ textarea.blur();
+ is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched");
+ is(textareaInput, 1, "input event should not have been dispatched");
+
+ textarea.focus();
+ expectedInputType = "insertText";
+ expectedData = "f";
+ expectedBeforeInputCancelable = true;
+ sendString("f");
+ is(textareaBeforeInput, 2, "beforeinput event should've been dispatched");
+ is(textareaInput, 2, "input event should've been dispatched");
+ textarea.value = 'bar';
+ is(textareaBeforeInput, 2, "beforeinput event should not have been dispatched");
+ is(textareaInput, 2, "input event should not have been dispatched");
+ expectedInputType = "deleteContentBackward";
+ expectedData = null;
+ expectedBeforeInputCancelable = true;
+ synthesizeKey("KEY_Backspace");
+ is(textareaBeforeInput, 3, "beforeinput event should've been dispatched");
+ is(textareaInput, 3, "input event should've been dispatched");
+ textarea.blur();
+ is(textareaBeforeInput, 3, "beforeinput event should not have been dispatched");
+ is(textareaInput, 3, "input event should not have been dispatched");
+
+ // Non-text input tests:
+ for (var i = 0; i < nonTextTypes.length; ++i) {
+ // Button, submit, image and reset input type tests.
+ if (i < 4) {
+ input = document.getElementById("input_" + nonTextTypes[i]);
+ input.focus();
+ input.click();
+ is(nonTextBeforeInput[i], 0, "beforeinput event doesn't apply");
+ is(nonTextInput[i], 0, "input event doesn't apply");
+ input.blur();
+ is(nonTextBeforeInput[i], 0, "beforeinput event doesn't apply");
+ is(nonTextInput[i], 0, "input event doesn't apply");
+ }
+ // For radio and checkboxes, input event should be dispatched.
+ else {
+ input = document.getElementById("input_" + nonTextTypes[i]);
+ input.focus();
+ input.click();
+ is(nonTextBeforeInput[i], 0, "beforeinput event should not have been dispatched");
+ is(nonTextInput[i], 1, "input event should've been dispatched");
+ input.blur();
+ is(nonTextBeforeInput[i], 0, "beforeinput event should not have been dispatched");
+ is(nonTextInput[i], 1, "input event should not have been dispatched");
+
+ // Test that input event is not dispatched if click event is cancelled.
+ function preventDefault(e) {
+ e.preventDefault();
+ }
+ input.addEventListener("click", preventDefault);
+ input.click();
+ is(nonTextBeforeInput[i], 0, "beforeinput event shouldn't be dispatched if click event is cancelled");
+ is(nonTextInput[i], 1, "input event shouldn't be dispatched if click event is cancelled");
+ input.removeEventListener("click", preventDefault);
+ }
+ }
+
+ // Type changes.
+ var input = document.createElement('input');
+ input.type = 'text';
+ input.value = 'foo';
+ input.onbeforeinput = function () {
+ ok(false, "we shouldn't get a beforeinput event when the type changes");
+ };
+ input.oninput = function() {
+ ok(false, "we shouldn't get an input event when the type changes");
+ };
+ input.type = 'range';
+ isnot(input.value, 'foo');
+
+ // Tests for type='range'.
+ var range = document.getElementById("input_range");
+
+ range.focus();
+ sendString("a");
+ range.blur();
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on range input " +
+ "element for key changes that don't change its value");
+ is(rangeInput, 0, "input event shouldn't be dispatched on range input " +
+ "element for key changes that don't change its value");
+
+ range.focus();
+ synthesizeKey("KEY_Home");
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched even for key changes");
+ is(rangeInput, 1, "input event should be dispatched for key changes");
+ range.blur();
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on blur");
+ is(rangeInput, 1, "input event shouldn't be dispatched on blur");
+
+ range.focus();
+ var bcr = range.getBoundingClientRect();
+ var centerOfRangeX = bcr.width / 2;
+ var centerOfRangeY = bcr.height / 2;
+ synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, { type: "mousedown" });
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on mousedown if the value changes");
+ is(rangeInput, 2, "Input event should be dispatched on mousedown if the value changes");
+ synthesizeMouse(range, centerOfRangeX - 5, centerOfRangeY, { type: "mousemove" });
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched during a drag");
+ is(rangeInput, 3, "Input event should be dispatched during a drag");
+ synthesizeMouse(range, centerOfRangeX, centerOfRangeY, { type: "mouseup" });
+ is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched at the end of a drag");
+ is(rangeInput, 4, "Input event should be dispatched at the end of a drag");
+
+ // Tests for type='number'.
+ // We only test key events here since input events for mouse event changes
+ // are tested in test_input_number_mouse_events.html
+ var number = document.getElementById("input_number");
+
+ if (isDesktop) { // up/down arrow keys not supported on android
+ number.value = "";
+ number.focus();
+ // <input type="number">'s inputType value hasn't been decided, see
+ // https://github.com/w3c/input-events/issues/88
+ expectedInputType = "insertReplacementText";
+ expectedData = "1";
+ expectedBeforeInputCancelable = false;
+ synthesizeKey("KEY_ArrowUp");
+ is(numberBeforeInput, 1, "beforeinput event should be dispatched for up/down arrow key keypress");
+ is(numberInput, 1, "input event should be dispatched for up/down arrow key keypress");
+ is(number.value, "1", "sanity check value of number control after keypress");
+
+ // `data` will be the value of the input, but we can't change
+ // `expectedData` and use {repeat: 3} at the same time.
+ skipExpectedDataCheck = true;
+ synthesizeKey("KEY_ArrowDown", {repeat: 3});
+ is(numberBeforeInput, 4, "beforeinput event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated");
+ is(numberInput, 4, "input event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated");
+ is(number.value, "-2", "sanity check value of number control after multiple keydown events");
+ skipExpectedDataCheck = false;
+
+ number.blur();
+ is(numberBeforeInput, 4, "beforeinput event shouldn't be dispatched on blur");
+ is(numberInput, 4, "input event shouldn't be dispatched on blur");
+ }
+
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ document.addEventListener("DOMContentLoaded", () => {
+ init();
+ SimpleTest.waitForFocus(testUserInput);
+ }, {once: true});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_file_picker.html b/dom/html/test/forms/test_input_file_picker.html
new file mode 100644
index 0000000000..296c12bb7e
--- /dev/null
+++ b/dom/html/test/forms/test_input_file_picker.html
@@ -0,0 +1,280 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for &lt;input type='file'&gt; file picker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377624">Mozilla Bug 36619</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377624">Mozilla Bug 377624</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=565274">Mozilla Bug 565274</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=701353">Mozilla Bug 701353</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=826176">Mozilla Bug 826176</a>
+<p id="display"></p>
+<div id="content">
+ <input id='a' type='file' accept="image/*">
+ <input id='b' type='file' accept="audio/*">
+ <input id='c' type='file' accept="video/*">
+ <input id='d' type='file' accept="image/*, audio/* ">
+ <input id='e' type='file' accept=" image/*,video/*">
+ <input id='f' type='file' accept="audio/*,video/*">
+ <input id='g' type='file' accept="image/*, audio/* ,video/*">
+ <input id='h' type='file' accept="foo/baz,image/*,bogus/duh">
+ <input id='i' type='file' accept="mime/type;parameter,video/*">
+ <input id='j' type='file' accept="audio/*, audio/*, audio/*">
+ <input id='k' type="file" accept="image/gif,image/png">
+ <input id='l' type="file" accept="image/*,image/gif,image/png">
+ <input id='m' type="file" accept="image/gif,image/gif">
+ <input id='n' type="file" accept="">
+ <input id='o' type="file" accept=".test">
+ <input id='p' type="file" accept="image/gif,.csv">
+ <input id='q' type="file" accept="image/gif,.gif">
+ <input id='r' type="file" accept=".prefix,.prefixPlusSomething">
+ <input id='s' type="file" accept=".xls,.xlsx">
+ <input id='t' type="file" accept=".mp3,.wav,.flac">
+ <input id='u' type="file" accept=".xls, .xlsx">
+ <input id='v' type="file" accept=".xlsx, .xls">
+ <input id='w' type="file" accept=".xlsx; .xls">
+ <input id='x' type="file" accept=".xls, .xlsx">
+ <input id='y' type="file" accept=".xlsx, .xls">
+ <input id='z' type='file' accept="i/am,a,pathological,;,,,,test/case">
+ <input id='A' type="file" accept=".xlsx, .xls*">
+ <input id='mix-ref' type="file" accept="image/jpeg">
+ <input id='mix' type="file" accept="image/jpeg,.jpg">
+ <input id='hidden' hidden type='file'>
+ <input id='untrusted-click' type='file'>
+ <input id='prevent-default' type='file'>
+ <input id='prevent-default-false' type='file'>
+ <input id='right-click' type='file'>
+ <input id='middle-click' type='file'>
+ <input id='left-click' type='file'>
+ <label id='label-1'>foo<input type='file'></label>
+ <label id='label-2' for='labeled-2'>foo</label><input id='labeled-2' type='file'></label>
+ <label id='label-3'>foo<input type='file'></label>
+ <label id='label-4' for='labeled-4'>foo</label><input id='labeled-4' type='file'></label>
+ <input id='by-button' type='file'>
+ <button id='button-click' onclick="document.getElementById('by-button').click();">foo</button>
+ <button id='button-down' onclick="document.getElementById('by-button').click();">foo</button>
+ <button id='button-up' onclick="document.getElementById('by-button').click();">foo</button>
+ <div id='div-click' onclick="document.getElementById('by-button').click();" tabindex='1'>foo</div>
+ <div id='div-click-on-demand' onclick="var i=document.createElement('input'); i.type='file'; i.click();" tabindex='1'>foo</div>
+ <div id='div-keydown' onkeydown="document.getElementById('by-button').click();" tabindex='1'>foo</div>
+ <a id='link-click' href="javascript:document.getElementById('by-button').click();" tabindex='1'>foo</a>
+ <input id='show-picker' type='file'>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * This test checks various scenarios and make sure that a file picker is being
+ * shown in all of them (minus a few exceptions).
+ * |testData| defines the tests to do and |launchNextTest| can be used to have
+ * specific behaviour for some tests. Everything else should just work.
+ */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+// The following lists are from toolkit/content/filepicker.properties which is used by filePicker
+var imageExtensionList = "*.jpe; *.jpg; *.jpeg; *.gif; *.png; *.bmp; *.ico; *.svg; *.svgz; *.tif; *.tiff; *.ai; *.drw; *.pct; *.psp; *.xcf; *.psd; *.raw; *.webp; *.heic"
+
+var audioExtensionList = "*.aac; *.aif; *.flac; *.iff; *.m4a; *.m4b; *.mid; *.midi; *.mp3; *.mpa; *.mpc; *.oga; *.ogg; *.opus; *.ra; *.ram; *.snd; *.wav; *.wma"
+
+var videoExtensionList = "*.avi; *.divx; *.flv; *.m4v; *.mkv; *.mov; *.mp4; *.mpeg; *.mpg; *.ogm; *.ogv; *.ogx; *.rm; *.rmvb; *.smil; *.webm; *.wmv; *.xvid"
+
+// [ element name | number of filters | extension list or filter mask | filter index ]
+var testData = [["a", 1, MockFilePicker.filterImages, 1],
+ ["b", 1, MockFilePicker.filterAudio, 1],
+ ["c", 1, MockFilePicker.filterVideo, 1],
+ ["d", 3, imageExtensionList + "; " + audioExtensionList, 1],
+ ["e", 3, imageExtensionList + "; " + videoExtensionList, 1],
+ ["f", 3, audioExtensionList + "; " + videoExtensionList, 1],
+ ["g", 4, imageExtensionList + "; " + audioExtensionList + "; " + videoExtensionList, 1],
+ ["h", 1, MockFilePicker.filterImages, 1],
+ ["i", 1, MockFilePicker.filterVideo, 1],
+ ["j", 1, MockFilePicker.filterAudio, 1],
+ ["k", 3, "*.gif; *.png", 1],
+ ["l", 4, imageExtensionList + "; " + "*.gif; *.png", 1],
+ ["m", 1, "*.gif", 1],
+ ["n", 0, undefined, 0],
+ ["o", 1, "*.test", 1],
+ ["p", 3, "*.gif; *.csv", 1],
+ ["q", 1, "*.gif", 1],
+ ["r", 3, "*.prefix; *.prefixPlusSomething", 1],
+ ["s", 3, "*.xls; *.xlsx", 1],
+ ["t", 4, "*.mp3; *.wav; *.flac", 1],
+ ["u", 3, "*.xls; *.xlsx", 1],
+ ["v", 3, "*.xlsx; *.xls", 1],
+ ["w", 0, undefined, 0],
+ ["x", 3, "*.xls; *.xlsx", 1],
+ ["y", 3, "*.xlsx; *.xls", 1],
+ ["z", 0, undefined, 0],
+ ["A", 1, "*.xlsx", 1],
+ // Note: mix and mix-ref tests extension lists are checked differently: see SimpleTest.executeSoon below
+ ["mix-ref", undefined, undefined, undefined],
+ ["mix", 1, undefined, 1],
+ ["hidden", 0, undefined, 0],
+ ["untrusted-click", 0, undefined, 0],
+ ["prevent-default", 0, undefined, 0, true],
+ ["prevent-default-false", 0, undefined, 0, true],
+ ["right-click", 0, undefined, 0, true],
+ ["middle-click", 0, undefined, 0, true],
+ ["left-click", 0, undefined, 0],
+ ["label-1", 0, undefined, 0],
+ ["label-2", 0, undefined, 0],
+ ["label-3", 0, undefined, 0],
+ ["label-4", 0, undefined, 0],
+ ["button-click", 0, undefined, 0],
+ ["button-down", 0, undefined, 0],
+ ["button-up", 0, undefined, 0],
+ ["div-click", 0, undefined, 0],
+ ["div-click-on-demand", 0, undefined, 0],
+ ["div-keydown", 0, undefined, 0],
+ ["link-click", 0, undefined, 0],
+ ["show-picker", 0, undefined, 0],
+ ];
+
+var currentTest = 0;
+var filterAllAdded;
+var filters;
+var filterIndex;
+var mixRefExtensionList;
+
+// Make sure picker works with popup blocker enabled and no allowed events
+SpecialPowers.pushPrefEnv({'set': [["dom.popup_allowed_events", ""]]}, runTests);
+
+function launchNextTest() {
+ MockFilePicker.shown = false;
+ filterAllAdded = false;
+ filters = [];
+ filterIndex = 0;
+
+ // Focusing the element will scroll them into view so making sure the clicks
+ // will work.
+ document.getElementById(testData[currentTest][0]).focus();
+
+ if (testData[currentTest][0] == "untrusted-click") {
+ var e = document.createEvent('MouseEvents');
+ e.initEvent('click', true, false);
+ document.getElementById(testData[currentTest][0]).dispatchEvent(e);
+ // All tests that should *NOT* show a file picker.
+ } else if (testData[currentTest][0] == "prevent-default" ||
+ testData[currentTest][0] == "prevent-default-false" ||
+ testData[currentTest][0] == "right-click" ||
+ testData[currentTest][0] == "middle-click") {
+ if (testData[currentTest][0] == "right-click" ||
+ testData[currentTest][0] == "middle-click") {
+ var b = testData[currentTest][0] == "middle-click" ? 1 : 2;
+ synthesizeMouseAtCenter(document.getElementById(testData[currentTest][0]),
+ { button: b });
+ } else {
+ if (testData[currentTest][0] == "prevent-default-false") {
+ document.getElementById(testData[currentTest][0]).onclick = function() {
+ return false;
+ };
+ } else {
+ document.getElementById(testData[currentTest][0]).onclick = function(event) {
+ event.preventDefault();
+ };
+ }
+ document.getElementById(testData[currentTest][0]).click();
+ }
+
+ // Wait a bit and assume we can continue. If the file picker shows later,
+ // behaviour is uncertain but that would be a random green, no big deal...
+ setTimeout(function() {
+ ok(true, "we should be there without a file picker being opened");
+ ++currentTest;
+ launchNextTest();
+ }, 500);
+ } else if (testData[currentTest][0] == 'label-3' ||
+ testData[currentTest][0] == 'label-4') {
+ synthesizeMouse(document.getElementById(testData[currentTest][0]), 5, 5, {});
+ } else if (testData[currentTest][0] == 'button-click' ||
+ testData[currentTest][0] == 'button-down' ||
+ testData[currentTest][0] == 'button-up' ||
+ testData[currentTest][0] == 'div-click' ||
+ testData[currentTest][0] == 'div-click-on-demand' ||
+ testData[currentTest][0] == 'link-click') {
+ synthesizeMouseAtCenter(document.getElementById(testData[currentTest][0]), {});
+ } else if (testData[currentTest][0] == 'div-keydown') {
+ sendString("a");
+ } else if (testData[currentTest][0] == 'show-picker') {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ document.getElementById(testData[currentTest][0]).showPicker();
+ } else {
+ document.getElementById(testData[currentTest][0]).click();
+ }
+}
+
+function runTests() {
+ MockFilePicker.appendFilterCallback = function(filepicker, title, val) {
+ filters.push(val);
+ };
+ MockFilePicker.appendFiltersCallback = function(filepicker, val) {
+ if (val === MockFilePicker.filterAll) {
+ filterAllAdded = true;
+ } else {
+ filters.push(val);
+ }
+ };
+ MockFilePicker.showCallback = function(filepicker) {
+ if (testData[currentTest][4]) {
+ ok(false, "we shouldn't have a file picker showing!");
+ return;
+ }
+
+ filterIndex = filepicker.filterIndex;
+ testName = testData[currentTest][0];
+ SimpleTest.executeSoon(function () {
+ ok(MockFilePicker.shown,
+ "File picker show method should have been called (" + testName + ")");
+ ok(filterAllAdded,
+ "filterAll is missing (" + testName + ")");
+ if (testName == "mix-ref") {
+ // Used only for reference for next test: nothing to be tested here
+ mixRefExtensionList = filters[0];
+ if (mixRefExtensionList == undefined) {
+ mixRefExtensionList = "";
+ }
+ } else {
+ if (testName == "mix") {
+ // Mixing mime type and file extension filters ("image/jpeg" and
+ // ".jpg" here) shouldn't restrict the list but only extend it, if file
+ // extension filter isn't a duplicate
+ ok(filters[0].includes(mixRefExtensionList),
+ "Mixing mime types and file extension filters shouldn't restrict extension list: " +
+ mixRefExtensionList + " | " + filters[0]);
+ ok(filters[0].includes("*.jpg"),
+ "Filter should contain '.jpg' extension. Filter was:" + filters[0]);
+ } else {
+ is(filters[0], testData[currentTest][2],
+ "Correct filters should have been added (" + testName + ")");
+ is(filters.length, testData[currentTest][1],
+ "appendFilters not called as often as expected (" + testName + ")");
+ }
+ is(filterIndex, testData[currentTest][3],
+ "File picker should show the correct filter index (" + testName + ")");
+ }
+
+ if (++currentTest == testData.length) {
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+ } else {
+ launchNextTest();
+ }
+ });
+ };
+
+ launchNextTest();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_hasBeenTypePassword.html b/dom/html/test/forms/test_input_hasBeenTypePassword.html
new file mode 100644
index 0000000000..ac577ae3a9
--- /dev/null
+++ b/dom/html/test/forms/test_input_hasBeenTypePassword.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1330228
+-->
+<head>
+ <title>Test input.hasBeenTypePassword</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1330228">Mozilla Bug 1330228</a>
+<script type="application/javascript">
+
+/** Test input.hasBeenTypePassword **/
+
+var gInputTestData = [
+/* type result */
+ ["tel", false],
+ ["text", false],
+ ["button", false],
+ ["checkbox", false],
+ ["file", false],
+ ["hidden", false],
+ ["reset", false],
+ ["image", false],
+ ["radio", false],
+ ["submit", false],
+ ["search", false],
+ ["email", false],
+ ["url", false],
+ ["number", false],
+ ["range", false],
+ ["date", false],
+ ["time", false],
+ ["color", false],
+ ["month", false],
+ ["week", false],
+ ["datetime-local", false],
+ ["", false],
+ // "password" must be last since we re-use the same <input>.
+ ["password", true],
+];
+
+function checkHasBeenTypePasswordValue(aInput, aResult) {
+ is(aInput.hasBeenTypePassword, aResult,
+ "hasBeenTypePassword should return " + aResult + " for " +
+ aInput.getAttribute("type"));
+}
+
+// Use SpecialPowers since the API is ChromeOnly.
+var input = SpecialPowers.wrap(document.createElement("input"));
+// Check if the method returns the correct value on the first pass.
+for (let [type, expected] of gInputTestData) {
+ input.type = type;
+ checkHasBeenTypePasswordValue(input, expected);
+}
+
+// Now do a second pass but expect `hasBeenTypePassword` to always be true now
+// that the type was 'password'.
+for (let [type] of gInputTestData) {
+ input.type = type;
+ checkHasBeenTypePasswordValue(input, true);
+}
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html b/dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html
new file mode 100644
index 0000000000..70a0f8427e
--- /dev/null
+++ b/dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1330228
+-->
+<head>
+ <title>Test hasBeenTypePassword is used with bfcache</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1330228">Mozilla Bug 1330228</a>
+<p id="display">
+ <iframe id="testframe" src="file_login_fields.html"></iframe>
+</p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test hasBeenTypePassword is used with bfcache **/
+SimpleTest.waitForExplicitFinish();
+
+function afterLoad() {
+ var iframeDoc = $("testframe").contentDocument;
+
+ /* change all the form controls */
+ iframeDoc.getElementById("un").value = "username";
+ iframeDoc.getElementById("pw1").value = "password1";
+
+ // Convert pw2 to a password field temporarily to test hasBeenTypePassword.
+ // We don't want the initial or final value to be type=password or we may
+ // not test the right scenario.
+ iframeDoc.getElementById("pw2").type = "password";
+ iframeDoc.getElementById("pw2").value = "password2";
+ iframeDoc.getElementById("pw2").type = "";
+
+ /* navigate the page */
+ $("testframe").setAttribute("onload", "afterNavigation()");
+ // Use a click on an <a> so that the current page is included in session history.
+ iframeDoc.getElementById("navigate").click();
+}
+
+addLoadEvent(afterLoad);
+
+function afterNavigation() {
+ info("Navigated to a new document");
+ var iframeDoc = $("testframe").contentDocument;
+ $("testframe").setAttribute("onload", "afterBack()");
+ // Calling `history.back()` on the contentWindow from here doesn't use bfcache
+ // so call it from within the contentDocument.
+ iframeDoc.getElementById("back").click();
+}
+
+function afterBack() {
+ info("Should be back showing the first document from bfcache");
+ var iframeDoc = $("testframe").contentDocument;
+
+ is(iframeDoc.getElementById("un").value, "username",
+ "username field value remembered");
+ is(iframeDoc.getElementById("pw1").value, "",
+ "type=password field value not remembered");
+ is(iframeDoc.getElementById("pw2").value, "",
+ "former type=password field value not remembered");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_list_attribute.html b/dom/html/test/forms/test_input_list_attribute.html
new file mode 100644
index 0000000000..62a07dd91a
--- /dev/null
+++ b/dom/html/test/forms/test_input_list_attribute.html
@@ -0,0 +1,253 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=556007
+-->
+<head>
+ <title>Test for Bug 556007</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=556007">Mozilla Bug 556007</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 556007 **/
+
+function test0() {
+ var input = document.createElement("input");
+ ok("list" in input, "list should be an input element IDL attribute");
+}
+
+// Default .list returns null.
+function test1(aContent, aInput) {
+ return null;
+}
+
+// Regular test case.
+function test2(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+ aInput.setAttribute('list', 'd');
+
+ return datalist;
+}
+
+// If none of the element is in doc.
+function test3(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+
+ return null;
+}
+
+// If one of the element isn't in doc.
+function test4(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(aInput);
+ aInput.setAttribute('list', 'd');
+
+ return null;
+}
+
+// If one of the element isn't in doc.
+function test5(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(datalist);
+ aInput.setAttribute('list', 'd');
+
+ return null;
+}
+
+// If datalist is added before input.
+function test6(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(datalist);
+ aContent.appendChild(aInput);
+ aInput.setAttribute('list', 'd');
+
+ return datalist;
+}
+
+// If setAttribute is set before datalist and input are in doc.
+function test7(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+
+ aContent.appendChild(datalist);
+ aContent.appendChild(aInput);
+
+ return datalist;
+}
+
+// If setAttribute is set before datalist is in doc.
+function test8(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aContent.appendChild(aInput);
+ aInput.setAttribute('list', 'd');
+
+ aContent.appendChild(datalist);
+
+ return datalist;
+}
+
+// If setAttribute is set before datalist is created.
+function test9(aContent, aInput) {
+ aContent.appendChild(aInput);
+ aInput.setAttribute('list', 'd');
+
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+ aContent.appendChild(datalist);
+
+ return datalist;
+}
+
+// If another datalist is added _after_ the first one, with the same id.
+function test10(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+ var datalist2 = document.createElement("datalist");
+ datalist2.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+ aContent.appendChild(datalist2);
+
+ return datalist;
+}
+
+// If another datalist is added _before_ the first one with the same id.
+function test11(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+ var datalist2 = document.createElement("datalist");
+ datalist2.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+ aContent.insertBefore(datalist2, datalist);
+
+ return datalist2;
+}
+
+// If datalist changes it's id.
+function test12(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+
+ datalist.id = 'foo';
+
+ return null;
+}
+
+// If datalist is removed.
+function test13(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+ aContent.removeChild(datalist);
+
+ return null;
+}
+
+// If id contain spaces.
+function test14(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = 'a b c d';
+
+ aInput.setAttribute('list', 'a b c d');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+
+ return datalist;
+}
+
+// If id is the empty string.
+function test15(aContent, aInput) {
+ var datalist = document.createElement("datalist");
+ datalist.id = '';
+
+ aInput.setAttribute('list', '');
+ aContent.appendChild(aInput);
+ aContent.appendChild(datalist);
+
+ return null;
+}
+
+// If the id doesn't point to a datalist.
+function test16(aContent, aInput) {
+ var input = document.createElement("input");
+ input.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(input);
+
+ return null;
+}
+
+// If the first element with the id isn't a datalist.
+function test17(aContent, aInput) {
+ var input = document.createElement("input");
+ input.id = 'd';
+ var datalist = document.createElement("datalist");
+ datalist.id = 'd';
+
+ aInput.setAttribute('list', 'd');
+ aContent.appendChild(aInput);
+ aContent.appendChild(input);
+ aContent.appendChild(datalist);
+
+ return null;
+}
+
+var tests = [ test1, test2, test3, test4, test5, test6, test7, test8, test9,
+ test10, test11, test12, test13, test14, test15, test16, test17 ];
+
+test0();
+
+for (var test of tests) {
+ var content = document.getElementById('content');
+
+ // Clean-up.
+ content.textContent = '';
+
+ var input = document.createElement("input");
+ var res = test(content, input);
+
+ is(input.list, res, "input.list should be " + res);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_data.js b/dom/html/test/forms/test_input_number_data.js
new file mode 100644
index 0000000000..9ec53f136f
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_data.js
@@ -0,0 +1,54 @@
+var tests = [
+ {
+ desc: "British English",
+ langTag: "en-GB",
+ inputWithGrouping: "123,456.78",
+ inputWithoutGrouping: "123456.78",
+ value: 123456.78,
+ },
+ {
+ desc: "Farsi",
+ langTag: "fa",
+ inputWithGrouping: "۱۲۳٬۴۵۶٫۷۸",
+ inputWithoutGrouping: "۱۲۳۴۵۶٫۷۸",
+ value: 123456.78,
+ },
+ {
+ desc: "French",
+ langTag: "fr-FR",
+ inputWithGrouping: "123 456,78",
+ inputWithoutGrouping: "123456,78",
+ value: 123456.78,
+ },
+ {
+ desc: "German",
+ langTag: "de",
+ inputWithGrouping: "123.456,78",
+ inputWithoutGrouping: "123456,78",
+ value: 123456.78,
+ },
+ // Bug 1509057 disables grouping separators for now, so this test isn't
+ // currently relevant.
+ // Extra german test to check that a locale that uses '.' as its grouping
+ // separator doesn't result in it being invalid (due to step mismatch) due
+ // to the de-localization code mishandling numbers that look like other
+ // numbers formatted for English speakers (i.e. treating this as 123.456
+ // instead of 123456):
+ //{ desc: "German (test 2)",
+ // langTag: "de", inputWithGrouping: "123.456",
+ // inputWithoutGrouping: "123456", value: 123456
+ //},
+ {
+ desc: "Hebrew",
+ langTag: "he",
+ inputWithGrouping: "123,456.78",
+ inputWithoutGrouping: "123456.78",
+ value: 123456.78,
+ },
+];
+
+var invalidTests = [
+ // Right now this will pass in a 'de' build, but not in the 'en' build that
+ // are used for testing. See bug 1216831.
+ // { desc: "Invalid German", langTag: "de", input: "12.34" }
+];
diff --git a/dom/html/test/forms/test_input_number_focus.html b/dom/html/test/forms/test_input_number_focus.html
new file mode 100644
index 0000000000..4126ecc496
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_focus.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1268556
+-->
+<head>
+ <title>Test focus behaviour for &lt;input type='number'&gt;</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #input_test_style_display {
+ display: none;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268556">Mozilla Bug 1268556</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1057858">Mozilla Bug 1057858</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input_test_redirect" type="number">
+ <input id="input_test_style_display" type="number" >
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 1268556.
+ * This test checks that when focusing on an input type=number, the focus is
+ * redirected to the anonymous text control, but the document.activeElement
+ * still returns the <input type=number>.
+ *
+ * Tests for bug 1057858.
+ * Checks that adding an element and immediately focusing it triggers exactly
+ * one "focus" event and no "blur" events. The same for switching
+ * `style.display` from `none` to `block`.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test_focus_redirects_to_text_control_but_not_for_activeElement();
+ test_add_element_and_focus_check_one_focus_event();
+ test_style_display_none_change_to_block_check_one_focus_event();
+ SimpleTest.finish();
+});
+
+function test_focus_redirects_to_text_control_but_not_for_activeElement() {
+ document.activeElement.blur();
+ var number = document.getElementById("input_test_redirect");
+ number.focus();
+
+ // The active element returns the input type=number.
+ var activeElement = document.activeElement;
+ is (activeElement, number, "activeElement should be the number element");
+ is (activeElement.localName, "input", "activeElement should be an input element");
+ is (activeElement.getAttribute("type"), "number", "activeElement should of type number");
+
+ // Use FocusManager to check that the actual focus is on the anonymous
+ // text control.
+ var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]
+ .getService(SpecialPowers.Ci.nsIFocusManager);
+ var focusedElement = fm.focusedElement;
+ is (focusedElement.localName, "input", "focusedElement should be an input element");
+ is (focusedElement.getAttribute("type"), "number", "focusedElement should of type number");
+}
+
+var blurEventCounter = 0;
+var focusEventCounter = 0;
+
+function append_input_element_with_event_listeners_to_dom() {
+ var inputElement = document.createElement("input");
+ inputElement.type = "number";
+ inputElement.addEventListener("blur", function() { ++blurEventCounter; });
+ inputElement.addEventListener("focus", function() { ++focusEventCounter; });
+ var content = document.getElementById("content");
+ content.appendChild(inputElement);
+ return inputElement;
+}
+
+function test_add_element_and_focus_check_one_focus_event() {
+ document.activeElement.blur();
+ var inputElement = append_input_element_with_event_listeners_to_dom();
+
+ blurEventCounter = 0;
+ focusEventCounter = 0;
+ inputElement.focus();
+
+ is(blurEventCounter, 0, "After focus: no blur events observed.");
+ is(focusEventCounter, 1, "After focus: exactly one focus event observed.");
+}
+
+function test_style_display_none_change_to_block_check_one_focus_event() {
+ document.activeElement.blur();
+ var inputElement = document.getElementById("input_test_style_display");
+ inputElement.addEventListener("blur", function() { ++blurEventCounter; });
+ inputElement.addEventListener("focus", function() { ++focusEventCounter; });
+
+ blurEventCounter = 0;
+ focusEventCounter = 0;
+ inputElement.style.display = "block";
+ inputElement.focus();
+
+ is(blurEventCounter, 0, "After focus: no blur events observed.");
+ is(focusEventCounter, 1, "After focus: exactly one focus event observed.");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_key_events.html b/dom/html/test/forms/test_input_number_key_events.html
new file mode 100644
index 0000000000..eb537f5617
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_key_events.html
@@ -0,0 +1,238 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=935506
+-->
+<head>
+ <title>Test key events for number control</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935506">Mozilla Bug 935506</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="number">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 935506
+ * This test checks how the value of <input type=number> changes in response to
+ * key events while it is in various states.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+const defaultMinimum = "NaN";
+const defaultMaximum = "NaN";
+const defaultStep = 1;
+
+// Helpers:
+// For the sake of simplicity, we do not currently support fractional value,
+// step, etc.
+
+function getMinimum(element) {
+ return Number(element.min || defaultMinimum);
+}
+
+function getMaximum(element) {
+ return Number(element.max || defaultMaximum);
+}
+
+function getDefaultValue(element) {
+ return 0;
+}
+
+function getValue(element) {
+ return Number(element.value || getDefaultValue(element));
+}
+
+function getStep(element) {
+ if (element.step == "any") {
+ return "any";
+ }
+ var step = Number(element.step || defaultStep);
+ return step <= 0 ? defaultStep : step;
+}
+
+function getStepBase(element) {
+ return Number(element.getAttribute("min") || "NaN") ||
+ Number(element.getAttribute("value") || "NaN") || 0;
+}
+
+function hasStepMismatch(element) {
+ var value = element.value;
+ if (value == "") {
+ value = 0;
+ }
+ var step = getStep(element);
+ if (step == "any") {
+ return false;
+ }
+ return ((value - getStepBase(element)) % step) != 0;
+}
+
+function floorModulo(x, y) {
+ return (x - y * Math.floor(x / y));
+}
+
+function expectedValueAfterStepUpOrDown(stepFactor, element) {
+ var value = getValue(element);
+ if (isNaN(value)) {
+ value = 0;
+ }
+ var step = getStep(element);
+ if (step == "any") {
+ step = 1;
+ }
+
+ var minimum = getMinimum(element);
+ var maximum = getMaximum(element);
+ if (!isNaN(maximum)) {
+ // "max - (max - stepBase) % step" is the nearest valid value to max.
+ maximum = maximum - floorModulo(maximum - getStepBase(element), step);
+ }
+
+ // Cases where we are clearly going in the wrong way.
+ // We don't use ValidityState because we can be higher than the maximal
+ // allowed value and still not suffer from range overflow in the case of
+ // of the value specified in @max isn't in the step.
+ if ((value <= minimum && stepFactor < 0) ||
+ (value >= maximum && stepFactor > 0)) {
+ return value;
+ }
+
+ if (hasStepMismatch(element) &&
+ value != minimum && value != maximum) {
+ if (stepFactor > 0) {
+ value -= floorModulo(value - getStepBase(element), step);
+ } else if (stepFactor < 0) {
+ value -= floorModulo(value - getStepBase(element), step);
+ value += step;
+ }
+ }
+
+ value += step * stepFactor;
+
+ // When stepUp() is called and the value is below minimum, we should clamp on
+ // minimum unless stepUp() moves us higher than minimum.
+ if (element.validity.rangeUnderflow && stepFactor > 0 &&
+ value <= minimum) {
+ value = minimum;
+ } else if (element.validity.rangeOverflow && stepFactor < 0 &&
+ value >= maximum) {
+ value = maximum;
+ } else if (stepFactor < 0 && !isNaN(minimum)) {
+ value = Math.max(value, minimum);
+ } else if (stepFactor > 0 && !isNaN(maximum)) {
+ value = Math.min(value, maximum);
+ }
+
+ return value;
+}
+
+function expectedValAfterKeyEvent(key, element) {
+ return expectedValueAfterStepUpOrDown(key == "KEY_ArrowUp" ? 1 : -1, element);
+}
+
+function test() {
+ var elem = document.getElementById("input");
+ elem.focus();
+
+ elem.min = -5;
+ elem.max = 5;
+ elem.step = 2;
+ var defaultValue = 0;
+ var oldVal, expectedVal;
+
+ for (key of ["KEY_ArrowUp", "KEY_ArrowDown"]) {
+ // Start at middle:
+ oldVal = elem.value = -1;
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set between min/max (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");
+
+ // Start at maximum:
+ oldVal = elem.value = elem.max;
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the maximum (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");
+
+ // Start at minimum:
+ oldVal = elem.value = elem.min;
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the minimum (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");
+
+ // Test preventDefault():
+ elem.addEventListener("keydown", evt => evt.preventDefault(), {once: true});
+ oldVal = elem.value = 0;
+ expectedVal = 0;
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control where scripted preventDefault() should prevent the value changing");
+
+ // Test step="any" behavior:
+ var oldStep = elem.step;
+ elem.step = "any";
+ oldVal = elem.value = 0;
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the midpoint and step='any' (" + oldVal + ")");
+ elem.step = oldStep; // restore
+
+ // Test that invalid input blocks UI initiated stepping:
+ oldVal = elem.value = "";
+ elem.select();
+ sendString("abc");
+ synthesizeKey(key);
+ is(elem.value, "", "Test " + key + " does nothing when the input is invalid");
+
+ // Test that no value does not block UI initiated stepping:
+ oldVal = elem.value = "";
+ elem.setAttribute("required", "required");
+ elem.select();
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the empty string and with the 'required' attribute set");
+
+ // Same again:
+ expectedVal = expectedValAfterKeyEvent(key, elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control");
+
+ // Reset 'required' attribute:
+ elem.removeAttribute("required");
+ }
+
+ // Test that key events are correctly dispatched
+ elem.max = "";
+ elem.value = "";
+ sendString("7837281");
+ is(elem.value, "7837281", "Test keypress event dispatch for number control");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_l10n.html b/dom/html/test/forms/test_input_number_l10n.html
new file mode 100644
index 0000000000..c8202028ed
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_l10n.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=844744
+-->
+<head>
+ <title>Test localization of number control input</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="test_input_number_data.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=844744">Mozilla Bug 844744</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="number" step="any">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 844744
+ * This test checks that localized input that is typed into <input type=number>
+ * is correctly handled.
+ **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ startTests();
+ SimpleTest.finish();
+});
+
+var elem;
+
+function runTest(test) {
+ elem.lang = test.langTag;
+ elem.value = 0;
+ elem.focus();
+ elem.select();
+ sendString(test.inputWithGrouping);
+ is(elem.value, "", "Test " + test.desc + " ('" + test.langTag +
+ "') localization with grouping separator");
+ elem.value = 0;
+ elem.select();
+ sendString(test.inputWithoutGrouping);
+ is(elem.valueAsNumber, test.value, "Test " + test.desc + " ('" + test.langTag +
+ "') localization without grouping separator");
+ is(elem.value, String(test.value), "Test " + test.desc + " ('" + test.langTag +
+ "') localization without grouping separator as string");
+}
+
+function runInvalidInputTest(test) {
+ elem.lang = test.langTag;
+ elem.value = 0;
+ elem.focus();
+ elem.select();
+ sendString(test.input);
+ is(elem.value, "", "Test " + test.desc + " ('" + test.langTag +
+ "') with invalid input: " + test.input);
+}
+
+function startTests() {
+ elem = document.getElementById("input");
+ for (var test of tests) {
+ runTest(test, elem);
+ }
+ for (var test of invalidTests) {
+ runInvalidInputTest(test, elem);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_mouse_events.html b/dom/html/test/forms/test_input_number_mouse_events.html
new file mode 100644
index 0000000000..a3e5732beb
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_mouse_events.html
@@ -0,0 +1,272 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=935501
+-->
+<head>
+ <title>Test mouse events for number</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <style>
+ input {
+ margin: 0;
+ border: 0;
+ padding: 0;
+ width: 200px;
+ box-sizing: border-box;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935501">Mozilla Bug 935501</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="number">
+</div>
+<pre id="test">
+<script>
+
+const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+/**
+ * Test for Bug 935501
+ * This test checks how the value of <input type=number> changes in response to
+ * various mouse events.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+SimpleTest.waitForFocus(function() {
+ test();
+});
+
+const kIsWin = AppConstants.platform == "win";
+const kIsLinux = AppConstants.platform == "linux";
+
+var input = document.getElementById("input");
+var inputRect = input.getBoundingClientRect();
+
+// Points over the input's spin-up and spin-down buttons (as offsets from the
+// top-left of the input's bounding client rect):
+const SPIN_UP_X = inputRect.width - 3;
+const SPIN_UP_Y = 3;
+const SPIN_DOWN_X = inputRect.width - 3;
+const SPIN_DOWN_Y = inputRect.height - 3;
+
+function checkInputEvent(aEvent, aDescription) {
+ // Probably, key operation should fire "input" event with InputEvent interface.
+ // See https://github.com/w3c/input-events/issues/88
+ ok(aEvent instanceof InputEvent, `"input" event should be dispatched with InputEvent interface on input element whose type is number ${aDescription}`);
+ is(aEvent.cancelable, false, `"input" event should be never cancelable on input element whose type is number ${aDescription}`);
+ is(aEvent.bubbles, true, `"input" event should always bubble on input element whose type is number ${aDescription}`);
+ info(`Data: ${aEvent.data}, value: ${aEvent.target.value}`);
+}
+
+function test() {
+ input.value = 0;
+
+ // Test click on spin-up button:
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+ is(input.value, "1", "Test step-up on mousedown on spin-up button");
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ is(input.value, "1", "Test mouseup on spin-up button");
+
+ // Test click on spin-down button:
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+ is(input.value, "0", "Test step-down on mousedown on spin-down button");
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+ is(input.value, "0", "Test mouseup on spin-down button");
+
+ // Test clicks with modifiers that mean we should ignore the click:
+ var modifiersIgnore = ["altGrKey", "fnKey"];
+ if (kIsWin || kIsLinux) {
+ modifiersIgnore.push("metaKey");
+ }
+ for (var modifier of modifiersIgnore) {
+ input.value = 0;
+ var eventParams = { type: "mousedown" };
+ eventParams[modifier] = true;
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, eventParams);
+ is(input.value, "0", "We should ignore mousedown on spin-up button with modifier " + modifier);
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ }
+
+ // Test clicks with modifiers that mean we should allow the click:
+ var modifiersAllow = ["shiftKey", "ctrlKey", "altKey"];
+ if (!modifiersIgnore.includes("metaKey")) {
+ modifiersAllow.push("metaKey");
+ }
+ for (var modifier of modifiersAllow) {
+ input.value = 0;
+ var eventParams = { type: "mousedown" };
+ eventParams[modifier] = true;
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, eventParams);
+ is(input.value, "1", "We should allow mousedown on spin-up button with modifier " + modifier);
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ }
+
+ // Test step="any" behavior:
+ input.value = 0;
+ var oldStep = input.step;
+ input.step = "any";
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+ is(input.value, "1", "Test step-up on mousedown on spin-up button with step='any'");
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ is(input.value, "1", "Test mouseup on spin-up button with step='any'");
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+ is(input.value, "0", "Test step-down on mousedown on spin-down button with step='any'");
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+ is(input.value, "0", "Test mouseup on spin-down button with step='any'");
+ input.step = oldStep; // restore
+
+ // Test that preventDefault() works:
+ function preventDefault(e) {
+ e.preventDefault();
+ }
+ input.value = 1;
+ input.addEventListener("mousedown", preventDefault);
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, {});
+ is(input.value, "1", "Test that preventDefault() works for click on spin-up button");
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, {});
+ is(input.value, "1", "Test that preventDefault() works for click on spin-down button");
+ input.removeEventListener("mousedown", preventDefault);
+
+ // Test for bug 1707070.
+ input.style.paddingRight = "30px";
+ input.getBoundingClientRect(); // flush layout
+
+ input.value = 0;
+ synthesizeMouse(input, SPIN_UP_X - 30, SPIN_UP_Y, { type: "mousedown" });
+ is(input.value, "1", "Spinner down works on with padding (mousedown)");
+ synthesizeMouse(input, SPIN_UP_X - 30, SPIN_UP_Y, { type: "mouseup" });
+ is(input.value, "1", "Spinner down works with padding (mouseup)");
+
+ synthesizeMouse(input, SPIN_DOWN_X - 30, SPIN_DOWN_Y, { type: "mousedown" });
+ is(input.value, "0", "Spinner works with padding (mousedown)");
+ synthesizeMouse(input, SPIN_DOWN_X - 30, SPIN_DOWN_Y, { type: "mouseup" });
+ is(input.value, "0", "Spinner works with padding (mouseup)");
+
+ input.style.paddingRight = "";
+ input.getBoundingClientRect(); // flush layout
+
+ // Run the spin tests:
+ runNextSpinTest();
+}
+
+function runNextSpinTest() {
+ var nextTest = spinTests.shift();
+ if (!nextTest) {
+ SimpleTest.finish();
+ return;
+ }
+ nextTest();
+}
+
+function waitForTick() {
+ return new Promise(SimpleTest.executeSoon);
+}
+
+const SETTIMEOUT_DELAY = 500;
+
+var spinTests = [
+ // Test spining when the mouse button is kept depressed on the spin-up
+ // button, then moved over the spin-down button:
+ function() {
+ var inputEventCount = 0;
+ input.value = 0;
+ input.addEventListener("input", async function(evt) {
+ ++inputEventCount;
+ checkInputEvent(evt, "#1");
+ if (inputEventCount == 3) {
+ is(input.value, "3", "Testing spin-up button");
+ await waitForTick();
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousemove" });
+ } else if (inputEventCount == 6) {
+ is(input.value, "0", "Testing spin direction is reversed after mouse moves from spin-up button to spin-down button");
+ input.removeEventListener("input", arguments.callee);
+ await waitForTick();
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+ runNextSpinTest();
+ }
+ });
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+ },
+
+ // Test spining when the mouse button is kept depressed on the spin-down
+ // button, then moved over the spin-up button:
+ function() {
+ var inputEventCount = 0;
+ input.value = 0;
+ input.addEventListener("input", async function(evt) {
+ ++inputEventCount;
+ checkInputEvent(evt, "#2");
+ if (inputEventCount == 3) {
+ is(input.value, "-3", "Testing spin-down button");
+ await waitForTick();
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousemove" });
+ } else if (inputEventCount == 6) {
+ is(input.value, "0", "Testing spin direction is reversed after mouse moves from spin-down button to spin-up button");
+ input.removeEventListener("input", arguments.callee);
+ await waitForTick();
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" });
+ runNextSpinTest();
+ }
+ });
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+ },
+
+ // Test that the spin is stopped when the mouse button is depressod on the
+ // spin-up button, then moved outside both buttons once the spin starts:
+ function() {
+ var inputEventCount = 0;
+ input.value = 0;
+ input.addEventListener("input", async function(evt) {
+ ++inputEventCount;
+ checkInputEvent(evt, "#3");
+ if (inputEventCount == 3) {
+ await waitForTick();
+ synthesizeMouse(input, -1, -1, { type: "mousemove" });
+ var eventHandler = arguments.callee;
+ setTimeout(function() {
+ is(input.value, "3", "Testing moving the mouse outside the spin buttons stops the spin");
+ is(inputEventCount, 3, "Testing moving the mouse outside the spin buttons stops the spin input events");
+ input.removeEventListener("input", eventHandler);
+ synthesizeMouse(input, -1, -1, { type: "mouseup" });
+ runNextSpinTest();
+ }, SETTIMEOUT_DELAY);
+ }
+ });
+ synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" });
+ },
+
+ // Test that changing the input type in the middle of a spin cancels the spin:
+ function() {
+ var inputEventCount = 0;
+ input.value = 0;
+ input.addEventListener("input", function(evt) {
+ ++inputEventCount;
+ checkInputEvent(evt, "#4");
+ if (inputEventCount == 3) {
+ input.type = "text"
+ var eventHandler = arguments.callee;
+ setTimeout(function() {
+ is(input.value, "-3", "Testing changing input type during a spin stops the spin");
+ is(inputEventCount, 3, "Testing changing input type during a spin stops the spin input events");
+ input.removeEventListener("input", eventHandler);
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" });
+ input.type = "number"; // restore
+ runNextSpinTest();
+ }, SETTIMEOUT_DELAY);
+ }
+ });
+ synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" });
+ }
+];
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_placeholder_shown.html b/dom/html/test/forms/test_input_number_placeholder_shown.html
new file mode 100644
index 0000000000..c9c2a7f515
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_placeholder_shown.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<title>Test for :placeholder-shown on input elements and invalid values.</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<style>
+input {
+ border: 1px solid purple;
+}
+input:placeholder-shown {
+ border-color: blue;
+}
+</style>
+<input type="number" placeholder="foo">
+<script>
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+ });
+
+ function test() {
+ let input = document.querySelector('input');
+ input.focus();
+ is(getComputedStyle(input).borderLeftColor, "rgb(0, 0, 255)",
+ ":placeholder-shown should apply")
+ sendString("x");
+ isnot(getComputedStyle(input).borderLeftColor, "rgb(0, 0, 255)",
+ ":placeholder-shown should not apply, even though the value is invalid")
+ }
+</script>
diff --git a/dom/html/test/forms/test_input_number_rounding.html b/dom/html/test/forms/test_input_number_rounding.html
new file mode 100644
index 0000000000..d162727557
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_rounding.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=783607
+-->
+<head>
+ <title>Test rounding behaviour for &lt;input type='number'&gt;</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=783607">Mozilla Bug 783607</a>
+<p id="display"></p>
+<div id="content">
+ <input id=number type=number value=0 step=0.01 max=1>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 783607.
+ * This test checks that when <input type=number> has fractional step values,
+ * the values that a content author will see in their script will not have
+ * ugly rounding errors.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+/**
+ * We can _NOT_ generate these values by looping and simply incrementing a
+ * variable by 0.01 and stringifying it, since we'll end up with strings like
+ * "0.060000000000000005" due to the inability of binary floating point numbers
+ * to accurately represent decimal values.
+ */
+var stepVals = [
+ "0", "0.01", "0.02", "0.03", "0.04", "0.05", "0.06", "0.07", "0.08", "0.09",
+ "0.1", "0.11", "0.12", "0.13", "0.14", "0.15", "0.16", "0.17", "0.18", "0.19",
+ "0.2", "0.21", "0.22", "0.23", "0.24", "0.25", "0.26", "0.27", "0.28", "0.29",
+ "0.3", "0.31", "0.32", "0.33", "0.34", "0.35", "0.36", "0.37", "0.38", "0.39",
+ "0.4", "0.41", "0.42", "0.43", "0.44", "0.45", "0.46", "0.47", "0.48", "0.49",
+ "0.5", "0.51", "0.52", "0.53", "0.54", "0.55", "0.56", "0.57", "0.58", "0.59",
+ "0.6", "0.61", "0.62", "0.63", "0.64", "0.65", "0.66", "0.67", "0.68", "0.69",
+ "0.7", "0.71", "0.72", "0.73", "0.74", "0.75", "0.76", "0.77", "0.78", "0.79",
+ "0.8", "0.81", "0.82", "0.83", "0.84", "0.85", "0.86", "0.87", "0.88", "0.89",
+ "0.9", "0.91", "0.92", "0.93", "0.94", "0.95", "0.96", "0.97", "0.98", "0.99",
+ "1"
+];
+
+var pgUpDnVals = [
+ "0", "0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "1"
+];
+
+function test() {
+ var elem = document.getElementById("number");
+
+ elem.focus();
+
+ /**
+ * TODO:
+ * When <input type='number'> widget will have a widge we should test PAGE_UP,
+ * PAGE_DOWN, UP and DOWN keys. For the moment, there is no widget so those
+ * keys do not have any effect.
+ * The tests using those keys as marked as todo_is() hoping that at least part
+ * of them will fail when the widget will be implemented.
+ */
+
+/* No other implementations implement this, so we don't either, for now.
+ Seems like it might be nice though.
+
+ for (var i = 1; i < pgUpDnVals.length; ++i) {
+ synthesizeKey("KEY_PageUp");
+ todo_is(elem.value, pgUpDnVals[i], "Test KEY_PageUp");
+ is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]);
+ }
+
+ for (var i = pgUpDnVals.length - 2; i >= 0; --i) {
+ synthesizeKey("KEY_PageDown");
+ // TODO: this condition is there because the todo_is() below would pass otherwise.
+ if (stepVals[i] == 0) { continue; }
+ todo_is(elem.value, pgUpDnVals[i], "Test KEY_PageDown");
+ is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]);
+ }
+*/
+
+ for (var i = 1; i < stepVals.length; ++i) {
+ synthesizeKey("KEY_ArrowUp");
+ is(elem.value, stepVals[i], "Test KEY_ArrowUp");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = stepVals.length - 2; i >= 0; --i) {
+ synthesizeKey("KEY_ArrowDown");
+ // TODO: this condition is there because the todo_is() below would pass otherwise.
+ if (stepVals[i] == 0) { continue; }
+ is(elem.value, stepVals[i], "Test KEY_ArrowDown");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = 1; i < stepVals.length; ++i) {
+ elem.stepUp();
+ is(elem.value, stepVals[i], "Test stepUp()");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = stepVals.length - 2; i >= 0; --i) {
+ elem.stepDown();
+ is(elem.value, stepVals[i], "Test stepDown()");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_number_validation.html b/dom/html/test/forms/test_input_number_validation.html
new file mode 100644
index 0000000000..c19c1fde1c
--- /dev/null
+++ b/dom/html/test/forms/test_input_number_validation.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=827161
+-->
+<head>
+ <title>Test validation of number control input</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="test_input_number_data.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=827161">Mozilla Bug 827161</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="number" step="0.01" oninvalid="invalidEventHandler(event);">
+ <input id="requiredinput" type="number" step="0.01" required
+ oninvalid="invalidEventHandler(event);">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 827161.
+ * This test checks that validation works correctly for <input type=number>.
+ **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ startTests();
+ SimpleTest.finish();
+});
+
+var elem;
+
+function runTest(test) {
+ elem.lang = test.langTag;
+
+ gInvalid = false; // reset
+ var desc = `${test.desc} (lang='${test.langTag}', id='${elem.id}')`;
+ elem.value = 0;
+ elem.focus();
+ elem.select();
+ sendString(test.inputWithGrouping);
+ checkIsInvalid(elem, `${desc} with grouping separator`);
+ sendChar("a");
+ checkIsInvalid(elem, `${desc} with grouping separator`);
+
+ gInvalid = false; // reset
+ elem.value = 0;
+ elem.select();
+ sendString(test.inputWithoutGrouping);
+ checkIsValid(elem, `${desc} without grouping separator`);
+ sendChar("a");
+ checkIsInvalid(elem, `${desc} without grouping separator`);
+}
+
+function runInvalidInputTest(test) {
+ elem.lang = test.langTag;
+
+ gInvalid = false; // reset
+ var desc = `${test.desc} (lang='${test.langTag}', id='${elem.id}')`;
+ elem.value = 0;
+ elem.focus();
+ elem.select();
+ sendString(test.input);
+ checkIsInvalid(elem, `${desc} with invalid input ${test.input}`);
+}
+
+function startTests() {
+ elem = document.getElementById("input");
+ for (var test of tests) {
+ runTest(test);
+ }
+ for (var test of invalidTests) {
+ runInvalidInputTest(test);
+ }
+ elem = document.getElementById("requiredinput");
+ for (var test of tests) {
+ runTest(test);
+ }
+
+ gInvalid = false; // reset
+ elem.value = "";
+ checkIsInvalidEmptyValue(elem, "empty value");
+}
+
+var gInvalid = false;
+
+function invalidEventHandler(e)
+{
+ is(e.type, "invalid", "Invalid event type should be 'invalid'");
+ gInvalid = true;
+}
+
+function checkIsValid(element, infoStr)
+{
+ ok(!element.validity.badInput,
+ "Element should not suffer from bad input for " + infoStr);
+ ok(element.validity.valid, "Element should be valid for " + infoStr);
+ ok(element.checkValidity(), "checkValidity() should return true for " + infoStr);
+ ok(!gInvalid, "The invalid event should not have been thrown for " + infoStr);
+ is(element.validationMessage, '',
+ "Validation message should be the empty string for " + infoStr);
+ ok(element.matches(":valid"), ":valid pseudo-class should apply for " + infoStr);
+}
+
+function checkIsInvalid(element, infoStr)
+{
+ ok(element.validity.badInput,
+ "Element should suffer from bad input for " + infoStr);
+ ok(!element.validity.valid, "Element should not be valid for " + infoStr);
+ ok(!element.checkValidity(), "checkValidity() should return false for " + infoStr);
+ ok(gInvalid, "The invalid event should have been thrown for " + infoStr);
+ is(element.validationMessage, "Please enter a number.",
+ "Validation message is not the expected message for " + infoStr);
+ ok(element.matches(":invalid"), ":invalid pseudo-class should apply for " + infoStr);
+}
+
+function checkIsInvalidEmptyValue(element, infoStr)
+{
+ ok(!element.validity.badInput,
+ "Element should not suffer from bad input for " + infoStr);
+ ok(element.validity.valueMissing,
+ "Element should suffer from value missing for " + infoStr);
+ ok(!element.validity.valid, "Element should not be valid for " + infoStr);
+ ok(!element.checkValidity(), "checkValidity() should return false for " + infoStr);
+ ok(gInvalid, "The invalid event should have been thrown for " + infoStr);
+ is(element.validationMessage, "Please enter a number.",
+ "Validation message is not the expected message for " + infoStr);
+ ok(element.matches(":invalid"), ":invalid pseudo-class should apply for " + infoStr);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_password_click_show_password_button.html b/dom/html/test/forms/test_input_password_click_show_password_button.html
new file mode 100644
index 0000000000..76f4e066f5
--- /dev/null
+++ b/dom/html/test/forms/test_input_password_click_show_password_button.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=502258
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 502258</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script>
+
+ SimpleTest.waitForExplicitFinish();
+
+ async function click_show_password(aId) {
+ var wu = SpecialPowers.getDOMWindowUtils(window);
+ var element = document.getElementById(aId);
+ element.focus();
+ await new Promise(resolve => setTimeout(resolve, 0));
+ var rect = element.getBoundingClientRect();
+ var x = rect.right - 8;
+ var y = rect.top + 8;
+ wu.sendMouseEvent("mousedown", x, y, 0, 1, 0);
+ wu.sendMouseEvent("mouseup", x, y, 0, 1, 0);
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ }
+
+ async function test_show_password(aId) {
+ var wu = SpecialPowers.getDOMWindowUtils(window);
+ var element = document.getElementById(aId);
+
+ var baseSnapshot = await snapshotWindow(window);
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ element.type = "text";
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ var typeTextSnapshot = await snapshotWindow(window);
+ results = compareSnapshots(baseSnapshot, typeTextSnapshot, true);
+ ok(results[0], aId + ": type=text should render the same as type=password that is showing the password");
+
+ // Re-setting value shouldn't change anything.
+ // eslint-disable-next-line no-self-assign
+ element.value = element.value;
+ var tmpSnapshot = await snapshotWindow(window);
+
+ results = compareSnapshots(baseSnapshot, tmpSnapshot, true);
+ ok(results[0], aId + ": re-setting the value should change nothing");
+ }
+
+ async function reset_show_password(aId, concealedSnapshot) {
+ var element = document.getElementById(aId);
+ element.type = "password";
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ var typePasswordSnapshot = await snapshotWindow(window);
+ results = compareSnapshots(concealedSnapshot, typePasswordSnapshot, true);
+ ok(results[0], aId + ": changing the type attribute should conceal the password again");
+ }
+
+ async function runTest() {
+ await SpecialPowers.pushPrefEnv({set: [["layout.forms.reveal-password-button.enabled", true]]});
+ document.getElementById("content").style.display = "";
+ document.getElementById("content").getBoundingClientRect();
+ var concealedSnapshot = await snapshotWindow(window);
+ // test1 checks that the Show Password button becomes invisible when the value becomes empty
+ document.getElementById('test1').value = "123";
+ await click_show_password('test1');
+ document.getElementById('test1').value = "";
+ // test2 checks that clicking the Show Password button unmasks the value
+ await click_show_password('test2');
+ await test_show_password('test1');
+ await test_show_password('test2');
+ // checks that changing the type attribute resets thhe revealed state
+ await reset_show_password('test2', concealedSnapshot);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(runTest);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502258">Mozilla Bug 502258</a>
+<p id="display"></p>
+<style>input {appearance:none} .ref {display:none}</style>
+<div id="content" style="display: none">
+ <input id="test1" type=password>
+ <div style="position:relative; margin: 1em 0;">
+ <input id="test2" type=password value="123" style="position:absolute">
+ <div style="position:absolute; top:0;left:10ch; width:20ch; height:2em; background:black; pointer-events:none"></div>
+ </div>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_password_show_password_button.html b/dom/html/test/forms/test_input_password_show_password_button.html
new file mode 100644
index 0000000000..09bec8ae82
--- /dev/null
+++ b/dom/html/test/forms/test_input_password_show_password_button.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=502258
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 502258</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ async function test_append_char(aId) {
+ let element = document.getElementById(aId);
+ element.focus();
+
+ let baseSnapshot = await snapshotWindow(window);
+
+ element.selectionStart = element.selectionEnd = element.value.length;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ sendString('f');
+
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
+
+ let selectionAtTheEndSnapshot = await snapshotWindow(window);
+ assertSnapshots(baseSnapshot, selectionAtTheEndSnapshot, /* equal = */ false, /* fuzz = */ null, "baseSnapshot", "selectionAtTheEndSnapshot");
+
+ // Re-setting value shouldn't change anything.
+ // eslint-disable-next-line no-self-assign
+ element.value = element.value;
+ let tmpSnapshot = await snapshotWindow(window);
+
+ assertSnapshots(baseSnapshot, tmpSnapshot, /* equal = */ false, /* fuzz = */ null, "baseSnapshot", "tmpSnapshot");
+ assertSnapshots(selectionAtTheEndSnapshot, tmpSnapshot, /* equal = */ true, /* fuzz = */ null, "selectionAtTheEndSnapshot", "tmpSnapshot");
+
+ element.selectionStart = element.selectionEnd = 0;
+ element.blur();
+ }
+
+ async function runTest() {
+ await SpecialPowers.pushPrefEnv({set: [["layout.forms.reveal-password-button.enabled", true]]});
+ document.getElementById("content").style.display = "";
+ document.getElementById("content").getBoundingClientRect();
+ await test_append_char('test1');
+ await test_append_char('test2');
+ await test_append_char('test3');
+ await test_append_char('test4');
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(runTest);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502258">Mozilla Bug 502258</a>
+<p id="display"></p>
+<style>input {appearance:none}</style>
+<div id="content" style="display: none">
+ <input id="test1" type=password>
+ <input id="test2" type=password value="123">
+ <!-- text value masked off -->
+ <div style="position:relative; margin: 1em 0;">
+ <input id="test3" type=password style="position:absolute">
+ <div style="position:absolute; top:0;left:0; width:10ch; height:2em; background:black"></div>
+ </div>
+ <br>
+ <!-- Show Password button masked off -->
+ <div style="position:relative; margin: 1em 0;">
+ <input id="test4" type=password style="position:absolute">
+ <div style="position:absolute; top:0;left:10ch; width:20ch; height:2em; background:black"></div>
+ </div>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_radio_indeterminate.html b/dom/html/test/forms/test_input_radio_indeterminate.html
new file mode 100644
index 0000000000..0fe7028b1e
--- /dev/null
+++ b/dom/html/test/forms/test_input_radio_indeterminate.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885359
+-->
+<head>
+ <title>Test for Bug 885359</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885359">Mozilla Bug 343444</a>
+<p id="display"></p>
+<form>
+ <input type="radio" id='radio1'/><br/>
+
+ <input type="radio" id="g1radio1" name="group1"/>
+ <input type="radio" id="g1radio2" name="group1"/></br>
+ <input type="radio" id="g1radio3" name="group1"/></br>
+
+ <input type="radio" id="g2radio1" name="group2"/>
+ <input type="radio" id="g2radio2" name="group2" checked/></br>
+</form>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var radio1 = document.getElementById("radio1");
+var g1radio1 = document.getElementById("g1radio1");
+var g1radio2 = document.getElementById("g1radio2");
+var g1radio3 = document.getElementById("g1radio3");
+var g2radio1 = document.getElementById("g2radio1");
+var g2radio2 = document.getElementById("g2radio2");
+
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function verifyIndeterminateState(aElement, aIsIndeterminate, aMessage) {
+ is(aElement.mozMatchesSelector(':indeterminate'), aIsIndeterminate, aMessage);
+}
+
+function test() {
+ // Initial State.
+ verifyIndeterminateState(radio1, true,
+ "Unchecked radio in its own group (no name attribute)");
+ verifyIndeterminateState(g1radio1, true, "No selected radio in its group");
+ verifyIndeterminateState(g1radio2, true, "No selected radio in its group");
+ verifyIndeterminateState(g1radio3, true, "No selected radio in its group");
+ verifyIndeterminateState(g2radio1, false, "Selected radio in its group");
+ verifyIndeterminateState(g2radio2, false, "Selected radio in its group");
+
+ // Selecting radio buttion.
+ g1radio1.checked = true;
+ verifyIndeterminateState(g1radio1, false,
+ "Selecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g1radio2, false,
+ "Selecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g1radio3, false,
+ "Selecting a radio should affect all radios in the group");
+
+ // Changing the selected radio button.
+ g1radio3.checked = true;
+ verifyIndeterminateState(g1radio1, false,
+ "Selecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g1radio2, false,
+ "Selecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g1radio3, false,
+ "Selecting a radio should affect all radios in the group");
+
+ // Deselecting radio button.
+ g2radio2.checked = false;
+ verifyIndeterminateState(g2radio1, true,
+ "Deselecting a radio should affect all radios in the group");
+ verifyIndeterminateState(g2radio2, true,
+ "Deselecting a radio should affect all radios in the group");
+
+ // Move a selected radio button to another group.
+ g1radio3.name = "group2";
+
+ // The radios' state in the original group becomes indeterminated.
+ verifyIndeterminateState(g1radio1, true,
+ "Removing a radio from a group should affect all radios in the group");
+ verifyIndeterminateState(g1radio2, true,
+ "Removing a radio from a group should affect all radios in the group");
+
+ // The radios' state in the new group becomes determinated.
+ verifyIndeterminateState(g1radio3, false,
+ "Adding a radio from a group should affect all radios in the group");
+ verifyIndeterminateState(g2radio1, false,
+ "Adding a radio from a group should affect all radios in the group");
+ verifyIndeterminateState(g2radio2, false,
+ "Adding a radio from a group should affect all radios in the group");
+
+ // Change input type to 'text'.
+ g1radio3.type = "text";
+ verifyIndeterminateState(g1radio3, false,
+ "Input type text does not have an indeterminate state");
+ verifyIndeterminateState(g2radio1, true,
+ "Changing input type should affect all radios in the group");
+ verifyIndeterminateState(g2radio2, true,
+ "Changing input type should affect all radios in the group");
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_input_radio_radiogroup.html b/dom/html/test/forms/test_input_radio_radiogroup.html
new file mode 100644
index 0000000000..62767def72
--- /dev/null
+++ b/dom/html/test/forms/test_input_radio_radiogroup.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=343444
+-->
+<head>
+ <title>Test for Bug 343444</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=343444">Mozilla Bug 343444</a>
+<p id="display"></p>
+<form>
+ <fieldset id="testradio">
+ <input type="radio" name="testradio" id="start"></input>
+ <input type="text" name="testradio"></input>
+ <input type="text" name="testradio"></input>
+ <input type="radio" name="testradio"></input>
+ <input type="text" name="testradio"></input>
+ <input type="radio" name="testradio"></input>
+ <input type="text" name="testradio"></input>
+ <input type="radio" name="testradio"></input>
+ <input type="radio" name="testradio"></input>
+ <input type="text" name="testradio"></input>
+ </fieldset>
+
+ <fieldset>
+ <input type="radio" name="testtwo" id="start2"></input>
+ <input type="radio" name="testtwo"></input>
+ <input type="radio" name="error" id="testtwo"></input>
+ <input type="radio" name="testtwo" id="end"></input>
+ </fieldset>
+
+ <fieldset>
+ <input type="radio" name="testthree" id="start3"></input>
+ <input type="radio" name="errorthree" id="testthree"></input>
+ </fieldset>
+</form>
+<script class="testbody" type="text/javascript">
+/** Test for Bug 343444 **/
+SimpleTest.waitForExplicitFinish();
+startTest();
+function startTest() {
+ document.getElementById("start").focus();
+ var count=0;
+ while (count < 2) {
+ sendKey("DOWN");
+ is(document.activeElement.type, "radio", "radioGroup should ignore non-radio input fields");
+ if (document.activeElement.id == "start") {
+ count++;
+ }
+ }
+
+ document.getElementById("start2").focus();
+ count = 0;
+ while (count < 3) {
+ is(document.activeElement.name, "testtwo",
+ "radioGroup should only contain elements with the same @name")
+ sendKey("DOWN");
+ count++;
+ }
+
+ document.getElementById("start3").focus();
+ sendKey("DOWN");
+ is(document.activeElement.name, "testthree", "we don't have an infinite-loop");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_input_radio_required.html b/dom/html/test/forms/test_input_radio_required.html
new file mode 100644
index 0000000000..ae02aab2ff
--- /dev/null
+++ b/dom/html/test/forms/test_input_radio_required.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER}
+-->
+<head>
+ <title>Test for Bug 1100535</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1100535">Mozilla Bug 1100535</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<form>
+ <input type="radio" name="a">
+</form>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ var input = document.querySelector("input");
+ input.setAttribute("required", "x");
+ input.setAttribute("required", "y");
+ is(document.forms[0].checkValidity(), false);
+ input.required = false;
+ is(document.forms[0].checkValidity(), true);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_range_attr_order.html b/dom/html/test/forms/test_input_range_attr_order.html
new file mode 100644
index 0000000000..dc3f1ac95c
--- /dev/null
+++ b/dom/html/test/forms/test_input_range_attr_order.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=841941
+-->
+<head>
+ <title>Test @min/@max/@step order for range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=841941">Mozilla Bug 841941</a>
+<p id="display"></p>
+<div id="content">
+ <input type=range value=2 max=1.5 step=0.5>
+ <input type=range value=2 step=0.5 max=1.5>
+ <input type=range value=2 max=1.5 step=0.5>
+ <input type=range value=2 step=0.5 max=1.5>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 841941
+ * This test checks that the order in which @min/@max/@step are specified in
+ * markup makes no difference to the value that <input type=range> will be
+ * given. Basically this checks that sanitization of the value does not occur
+ * until after the parser has finished with the element.
+ */
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+function test() {
+ var ranges = document.querySelectorAll("input[type=range]");
+ for (var i = 0; i < ranges.length; i++) {
+ is(ranges.item(i).value, "1.5", "Check sanitization order for range " + i);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_range_key_events.html b/dom/html/test/forms/test_input_range_key_events.html
new file mode 100644
index 0000000000..6daf572916
--- /dev/null
+++ b/dom/html/test/forms/test_input_range_key_events.html
@@ -0,0 +1,207 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=843725
+-->
+<head>
+ <title>Test key events for range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=843725">Mozilla Bug 843725</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 843725
+ * This test checks how the value of <input type=range> changes in response to
+ * various key events while it is in various states.
+ **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+const defaultMinimum = 0;
+const defaultMaximum = 100;
+const defaultStep = 1;
+
+// Helpers:
+// For the sake of simplicity, we do not currently support fractional value,
+// step, etc.
+
+function minimum(element) {
+ return Number(element.min || defaultMinimum);
+}
+
+function maximum(element) {
+ return Number(element.max || defaultMaximum);
+}
+
+function range(element) {
+ var max = maximum(element);
+ var min = minimum(element);
+ if (max < min) {
+ return 0;
+ }
+ return max - min;
+}
+
+function defaultValue(element) {
+ return minimum(element) + range(element)/2;
+}
+
+function value(element) {
+ return Number(element.value || defaultValue(element));
+}
+
+function step(element) {
+ var stepSize = Number(element.step || defaultStep);
+ return stepSize <= 0 ? defaultStep : stepSize;
+}
+
+function clampToRange(val, element) {
+ var min = minimum(element);
+ var max = maximum(element);
+ if (max < min) {
+ return min;
+ }
+ if (val < min) {
+ return min;
+ }
+ if (val > max) {
+ return max;
+ }
+ return val;
+}
+
+// Functions used to specify expected test results:
+
+function valuePlusStep(element) {
+ return clampToRange(value(element) + step(element), element);
+}
+
+function valueMinusStep(element) {
+ return clampToRange(value(element) - step(element), element);
+}
+
+/**
+ * Returns the current value of the range plus whichever is greater of either
+ * 10% of the range or its current step value, clamped to the range's minimum/
+ * maximum. The reason for using the step if it is greater than 10% of the
+ * range is because otherwise the PgUp/PgDn keys would do nothing in that case.
+ */
+function valuePlusTenPctOrStep(element) {
+ var tenPct = range(element)/10;
+ var stp = step(element);
+ return clampToRange(value(element) + Math.max(tenPct, stp), element);
+}
+
+function valueMinusTenPctOrStep(element) {
+ var tenPct = range(element)/10;
+ var stp = step(element);
+ return clampToRange(value(element) - Math.max(tenPct, stp), element);
+}
+
+// Test table:
+
+const LTR = "ltr";
+const RTL = "rtl";
+
+var testTable = [
+ ["KEY_ArrowLeft", LTR, valueMinusStep],
+ ["KEY_ArrowLeft", RTL, valuePlusStep],
+ ["KEY_ArrowRight", LTR, valuePlusStep],
+ ["KEY_ArrowRight", RTL, valueMinusStep],
+ ["KEY_ArrowUp", LTR, valuePlusStep],
+ ["KEY_ArrowUp", RTL, valuePlusStep],
+ ["KEY_ArrowDown", LTR, valueMinusStep],
+ ["KEY_ArrowDown", RTL, valueMinusStep],
+ ["KEY_PageUp", LTR, valuePlusTenPctOrStep],
+ ["KEY_PageUp", RTL, valuePlusTenPctOrStep],
+ ["KEY_PageDown", LTR, valueMinusTenPctOrStep],
+ ["KEY_PageDown", RTL, valueMinusTenPctOrStep],
+ ["KEY_Home", LTR, minimum],
+ ["KEY_Home", RTL, minimum],
+ ["KEY_End", LTR, maximum],
+ ["KEY_End", RTL, maximum],
+]
+
+function test() {
+ var elem = document.createElement("input");
+ elem.type = "range";
+
+ var content = document.getElementById("content");
+ content.appendChild(elem);
+ elem.focus();
+
+ for (test of testTable) {
+ var [key, dir, expectedFunc] = test;
+ var oldVal, expectedVal;
+
+ elem.step = "2";
+ elem.style.direction = dir;
+ var flush = document.body.clientWidth;
+
+ // Start at middle:
+ elem.value = oldVal = defaultValue(elem);
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with value set to the midpoint (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range");
+
+ // Start at maximum:
+ elem.value = oldVal = maximum(elem);
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with value set to the maximum (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range");
+
+ // Start at minimum:
+ elem.value = oldVal = minimum(elem);
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with value set to the minimum (" + oldVal + ")");
+
+ // Same again:
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range");
+
+ // Test for a step value that is greater than 10% of the range:
+ elem.step = 20;
+ elem.value = 60;
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with a step that is greater than 10% of the range (step=" + elem.step + ")");
+
+ // Same again:
+ expectedVal = expectedFunc(elem);
+ synthesizeKey(key);
+ is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range");
+
+ // reset step:
+ elem.step = 2;
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_range_mouse_and_touch_events.html b/dom/html/test/forms/test_input_range_mouse_and_touch_events.html
new file mode 100644
index 0000000000..5957ede81d
--- /dev/null
+++ b/dom/html/test/forms/test_input_range_mouse_and_touch_events.html
@@ -0,0 +1,240 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=846380
+-->
+<head>
+ <title>Test mouse and touch events for range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <style>
+ /* synthesizeMouse and synthesizeFunc uses getBoundingClientRect. We set
+ * the following properties to avoid fractional values in the rect returned
+ * by getBoundingClientRect in order to avoid rounding that would occur
+ * when event coordinates are internally converted to be relative to the
+ * top-left of the element. (Such rounding would make it difficult to
+ * predict exactly what value the input should take on for events at
+ * certain coordinates.)
+ */
+ input { margin: 0 ! important; width: 200px ! important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=846380">Mozilla Bug 846380</a>
+<p id="display"></p>
+<div id="content">
+ <input id="range" type="range">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+/**
+ * Test for Bug 846380
+ * This test checks how the value of <input type=range> changes in response to
+ * various mouse and touch events.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test(synthesizeMouse, "click", "mousedown", "mousemove", "mouseup");
+ test(synthesizeTouch, "tap", "touchstart", "touchmove", "touchend");
+ SimpleTest.finish();
+});
+
+const kIsWin = AppConstants.platform == "win";
+const kIsLinux = AppConstants.platform == "linux";
+
+const MIDDLE_OF_RANGE = "50";
+const MINIMUM_OF_RANGE = "0";
+const MAXIMUM_OF_RANGE = "100";
+const QUARTER_OF_RANGE = "25";
+const THREE_QUARTERS_OF_RANGE = "75";
+
+function flush() {
+ // Flush style, specifically to flush the 'direction' property so that the
+ // browser uses the new value for thumb positioning.
+ document.body.clientWidth;
+}
+
+function test(synthesizeFunc, clickOrTap, startName, moveName, endName) {
+ var elem = document.getElementById("range");
+ elem.focus();
+ flush();
+
+ var width = parseFloat(window.getComputedStyle(elem).width);
+ var height = parseFloat(window.getComputedStyle(elem).height);
+ var borderLeft = parseFloat(window.getComputedStyle(elem).borderLeftWidth);
+ var borderTop = parseFloat(window.getComputedStyle(elem).borderTopWidth);
+ var paddingLeft = parseFloat(window.getComputedStyle(elem).paddingLeft);
+ var paddingTop = parseFloat(window.getComputedStyle(elem).paddingTop);
+
+ // Extrema for mouse/touch events:
+ var midY = height / 2 + borderTop + paddingTop;
+ var minX = borderLeft + paddingLeft;
+ var midX = minX + width / 2;
+ var maxX = minX + width;
+
+ // Test click/tap in the middle of the range:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, {});
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + clickOrTap + " in middle of range");
+
+ // Test mouse/touch dragging of ltr range:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range");
+ synthesizeFunc(elem, minX, midY, { type: moveName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to left of ltr range");
+
+ synthesizeFunc(elem, maxX, midY, { type: moveName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to right of ltr range (" + moveName + ")");
+
+ synthesizeFunc(elem, maxX, midY, { type: endName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to right of ltr range (" + endName + ")");
+
+ // Test mouse/touch dragging of rtl range:
+ elem.value = QUARTER_OF_RANGE;
+ elem.style.direction = "rtl";
+ flush();
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of rtl range");
+ synthesizeFunc(elem, minX, midY, { type: moveName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to left of rtl range");
+
+ synthesizeFunc(elem, maxX, midY, { type: moveName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to right of rtl range (" + moveName + ")");
+
+ synthesizeFunc(elem, maxX, midY, { type: endName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to right of rtl range (" + endName + ")");
+
+ elem.style.direction = "ltr"; // reset direction
+ flush();
+
+ // Test mouse/touch capturing by moving pointer to a position outside the range:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range");
+ synthesizeFunc(elem, maxX+100, midY, { type: moveName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to position outside range (" + moveName + ")");
+
+ synthesizeFunc(elem, maxX+100, midY, { type: endName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to position outside range (" + endName + ")");
+
+ // Test mouse/touch capturing by moving pointer to a position outside a rtl range:
+ elem.value = QUARTER_OF_RANGE;
+ elem.style.direction = "rtl";
+ flush();
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of rtl range");
+ synthesizeFunc(elem, maxX+100, midY, { type: moveName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to position outside range (" + moveName + ")");
+
+ synthesizeFunc(elem, maxX+100, midY, { type: endName });
+ is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to position outside range (" + endName + ")");
+
+ elem.style.direction = "ltr"; // reset direction
+ flush();
+
+ // Test mouse/touch events with certain modifiers are ignored:
+ var modifiersIgnore = ["ctrlKey", "altGrKey", "fnKey"];
+ if (kIsWin || kIsLinux) {
+ modifiersIgnore.push("metaKey");
+ }
+ for (var modifier of modifiersIgnore) {
+ elem.value = QUARTER_OF_RANGE;
+ var eventParams = {};
+ eventParams[modifier] = true;
+ synthesizeFunc(elem, midX, midY, eventParams);
+ is(elem.value, QUARTER_OF_RANGE, "Test " + clickOrTap + " in the middle of range with " + modifier + " modifier key is ignored");
+ }
+
+ // Test mouse/touch events with certain modifiers are allowed:
+ var modifiersAllow = ["shiftKey", "altKey"];
+ if (!modifiersIgnore.includes("metaKey")) {
+ modifiersAllow.push("metaKey");
+ }
+ for (var modifier of modifiersAllow) {
+ elem.value = QUARTER_OF_RANGE;
+ var eventParams = {};
+ eventParams[modifier] = true;
+ synthesizeFunc(elem, midX, midY, eventParams);
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + clickOrTap + " in the middle of range with " + modifier + " modifier key is allowed");
+ }
+
+ // Test that preventDefault() works:
+ function preventDefault(e) {
+ e.preventDefault();
+ }
+ elem.value = QUARTER_OF_RANGE;
+ elem.addEventListener(startName, preventDefault);
+ synthesizeFunc(elem, midX, midY, {});
+ is(elem.value, QUARTER_OF_RANGE, "Test that preventDefault() works");
+ elem.removeEventListener(startName, preventDefault);
+
+ // Test that changing the input type in the middle of a drag cancels the drag:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range");
+ elem.type = "text";
+ is(elem.value, QUARTER_OF_RANGE, "Test that changing the input type cancels a drag");
+ synthesizeFunc(elem, midX, midY, { type: endName });
+ is(elem.value, QUARTER_OF_RANGE, "Test that changing the input type cancels a drag (after " + endName + ")");
+ elem.type = "range";
+
+ // Check that we do not drag when the mousedown/touchstart occurs outside the range:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, maxX+100, midY, { type: startName });
+ is(elem.value, QUARTER_OF_RANGE, "Test " + startName + " outside range doesn't change its value");
+ synthesizeFunc(elem, midX, midY, { type: moveName });
+ is(elem.value, QUARTER_OF_RANGE, "Test dragging is not occurring when " + startName + " was outside range");
+
+ synthesizeFunc(elem, midX, midY, { type: endName });
+ is(elem.value, QUARTER_OF_RANGE, "Test dragging is not occurring when " + startName + " was outside range");
+
+ elem.focus(); // RESTORE FOCUS SO WE GET THE FOCUSED STYLE FOR TESTING OR ELSE minX/midX/maxX may be wrong!
+
+ // Check what happens when a value changing key is pressed during a drag:
+ elem.value = QUARTER_OF_RANGE;
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range");
+ synthesizeKey("KEY_Home");
+ // The KEY_Home tests are disabled until I can figure out why they fail on Android -jwatt
+ //is(elem.value, MINIMUM_OF_RANGE, "Test KEY_Home during a drag sets the value to the minimum of the range");
+ synthesizeFunc(elem, midX+100, midY, { type: moveName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test " + moveName + " outside range after key press that occurred during a drag changes the value");
+ synthesizeFunc(elem, midX, midY, { type: moveName });
+ is(elem.value, MIDDLE_OF_RANGE, "Test " + moveName + " in middle of range");
+ synthesizeKey("KEY_Home");
+ //is(elem.value, MINIMUM_OF_RANGE, "Test KEY_Home during a drag sets the value to the minimum of the range (second time)");
+ synthesizeFunc(elem, maxX+100, midY, { type: endName });
+ is(elem.value, MAXIMUM_OF_RANGE, "Test " + endName + " outside range after key press that occurred during a drag changes the value");
+
+ function hideElement() {
+ elem.parentNode.style.display = 'none';
+ elem.parentNode.offsetLeft;
+ }
+
+ if (clickOrTap == "click") {
+ elem.addEventListener("mousedown", hideElement);
+ } else if (clickOrTap == "tap") {
+ elem.addEventListener("touchstart", hideElement);
+ }
+ synthesizeFunc(elem, midX, midY, { type: startName });
+ synthesizeFunc(elem, midX, midY, { type: endName });
+ elem.removeEventListener("mousedown", hideElement);
+ elem.removeEventListener("touchstart", hideElement);
+ ok(true, "Hiding the element during mousedown/touchstart shouldn't crash the process.");
+ elem.parentNode.style.display = "block";
+ elem.parentNode.offsetLeft;
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_range_rounding.html b/dom/html/test/forms/test_input_range_rounding.html
new file mode 100644
index 0000000000..9c3c21ce6e
--- /dev/null
+++ b/dom/html/test/forms/test_input_range_rounding.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=853525
+-->
+<head>
+ <title>Test key events for range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=853525">Mozilla Bug 853525</a>
+<p id="display"></p>
+<div id="content">
+ <input id=range type=range value=0 step=0.01 max=1>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/**
+ * Test for Bug 853525
+ * This test checks that when <input type=range> has fractional step values,
+ * the values that a content author will see in their script will not have
+ * ugly rounding errors.
+ **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+/**
+ * We can _NOT_ generate these values by looping and simply incrementing a
+ * variable by 0.01 and stringifying it, since we'll end up with strings like
+ * "0.060000000000000005" due to the inability of binary floating point numbers
+ * to accurately represent decimal values.
+ */
+var stepVals = [
+ "0", "0.01", "0.02", "0.03", "0.04", "0.05", "0.06", "0.07", "0.08", "0.09",
+ "0.1", "0.11", "0.12", "0.13", "0.14", "0.15", "0.16", "0.17", "0.18", "0.19",
+ "0.2", "0.21", "0.22", "0.23", "0.24", "0.25", "0.26", "0.27", "0.28", "0.29",
+ "0.3", "0.31", "0.32", "0.33", "0.34", "0.35", "0.36", "0.37", "0.38", "0.39",
+ "0.4", "0.41", "0.42", "0.43", "0.44", "0.45", "0.46", "0.47", "0.48", "0.49",
+ "0.5", "0.51", "0.52", "0.53", "0.54", "0.55", "0.56", "0.57", "0.58", "0.59",
+ "0.6", "0.61", "0.62", "0.63", "0.64", "0.65", "0.66", "0.67", "0.68", "0.69",
+ "0.7", "0.71", "0.72", "0.73", "0.74", "0.75", "0.76", "0.77", "0.78", "0.79",
+ "0.8", "0.81", "0.82", "0.83", "0.84", "0.85", "0.86", "0.87", "0.88", "0.89",
+ "0.9", "0.91", "0.92", "0.93", "0.94", "0.95", "0.96", "0.97", "0.98", "0.99",
+ "1"
+];
+
+var pgUpDnVals = [
+ "0", "0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "1"
+];
+
+function test() {
+ var elem = document.getElementById("range");
+
+ elem.focus();
+
+ for (var i = 1; i < pgUpDnVals.length; ++i) {
+ synthesizeKey("KEY_PageUp");
+ is(elem.value, pgUpDnVals[i], "Test KEY_PageUp");
+ is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]);
+ }
+
+ for (var i = pgUpDnVals.length - 2; i >= 0; --i) {
+ synthesizeKey("KEY_PageDown");
+ is(elem.value, pgUpDnVals[i], "Test KEY_PageDown");
+ is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]);
+ }
+
+ for (var i = 1; i < stepVals.length; ++i) {
+ synthesizeKey("KEY_ArrowUp");
+ is(elem.value, stepVals[i], "Test KEY_ArrowUp");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = stepVals.length - 2; i >= 0; --i) {
+ synthesizeKey("KEY_ArrowDown");
+ is(elem.value, stepVals[i], "Test KEY_ArrowDown");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = 1; i < stepVals.length; ++i) {
+ elem.stepUp();
+ is(elem.value, stepVals[i], "Test stepUp()");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+
+ for (var i = stepVals.length - 2; i >= 0; --i) {
+ elem.stepDown();
+ is(elem.value, stepVals[i], "Test stepDown()");
+ is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_sanitization.html b/dom/html/test/forms/test_input_sanitization.html
new file mode 100644
index 0000000000..474ddd621d
--- /dev/null
+++ b/dom/html/test/forms/test_input_sanitization.html
@@ -0,0 +1,585 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=549475
+-->
+<head>
+ <title>Test for Bug 549475</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549475">Mozilla Bug 549475</a>
+<p id="display"></p>
+<pre id="test">
+<div id='content'>
+ <form>
+ </form>
+</div>
+<script type="application/javascript">
+
+SimpleTest.requestLongerTimeout(2);
+
+/**
+ * This files tests the 'value sanitization algorithm' for the various input
+ * types. Note that an input's value is affected by more than just its type's
+ * value sanitization algorithm; e.g. some type=range has actions that the user
+ * agent must perform to change the element's value to avoid underflow/overflow
+ * and step mismatch (when possible). We specifically avoid triggering these
+ * other actions here so that this test only tests the value sanitization
+ * algorithm for the various input types.
+ *
+ * XXXjwatt splitting out testing of the value sanitization algorithm and
+ * "other things" that affect .value makes it harder to know what we're testing
+ * and what we've missed, because what's included in the value sanitization
+ * algorithm and what's not is different from input type to input type. It
+ * seems to me it would be better to have a test (maybe one per type) focused
+ * on testing .value for permutations of all other inputs that can affect it.
+ * The value sanitization algorithm is just an internal spec concept after all.
+ */
+
+// We buffer up the results of sets of sub-tests, and avoid outputting log
+// entries for them all if they all pass. Otherwise, we have an enormous amount
+// of test output.
+
+var delayedTests = [];
+var anyFailedDelayedTests = false;
+
+function delayed_is(actual, expected, description)
+{
+ var result = actual == expected;
+ delayedTests.push({ actual, expected, description });
+ if (!result) {
+ anyFailedDelayedTests = true;
+ }
+}
+
+function flushDelayedTests(description)
+{
+ if (anyFailedDelayedTests) {
+ info("Outputting individual results for \"" + description + "\" due to failures in subtests");
+ for (var test of delayedTests) {
+ is(test.actual, test.expected, test.description);
+ }
+ } else {
+ ok(true, description + " (" + delayedTests.length + " subtests)");
+ }
+ delayedTests = [];
+ anyFailedDelayedTests = false;
+}
+
+// We are excluding "file" because it's too different from the other types.
+// And it has no sanitizing algorithm.
+var inputTypes =
+[
+ "text", "password", "search", "tel", "hidden", "checkbox", "radio",
+ "submit", "image", "reset", "button", "email", "url", "number", "date",
+ "time", "range", "color", "month", "week", "datetime-local"
+];
+
+var valueModeValue =
+[
+ "text", "search", "url", "tel", "email", "password", "date", "datetime",
+ "month", "week", "time", "datetime-local", "number", "range", "color",
+];
+
+function sanitizeDate(aValue)
+{
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-date-string
+ function getNumbersOfDaysInMonth(aMonth, aYear) {
+ if (aMonth === 2) {
+ return (aYear % 400 === 0 || (aYear % 100 != 0 && aYear % 4 === 0)) ? 29 : 28;
+ }
+ return (aMonth === 1 || aMonth === 3 || aMonth === 5 || aMonth === 7 ||
+ aMonth === 8 || aMonth === 10 || aMonth === 12) ? 31 : 30;
+ }
+
+ var match = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})$/.exec(aValue);
+ if (!match) {
+ return "";
+ }
+ var year = Number(match[1]);
+ if (year === 0) {
+ return "";
+ }
+ var month = Number(match[2]);
+ if (month > 12 || month < 1) {
+ return "";
+ }
+ var day = Number(match[3]);
+ return 1 <= day && day <= getNumbersOfDaysInMonth(month, year) ? aValue : "";
+}
+
+function sanitizeTime(aValue)
+{
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-time-string
+ var match = /^([0-9]{2}):([0-9]{2})(.*)$/.exec(aValue);
+ if (!match) {
+ return "";
+ }
+ var hours = match[1];
+ if (hours < 0 || hours > 23) {
+ return "";
+ }
+ var minutes = match[2];
+ if (minutes < 0 || minutes > 59) {
+ return "";
+ }
+ var other = match[3];
+ if (other == "") {
+ return aValue;
+ }
+ match = /^:([0-9]{2})(.*)$/.exec(other);
+ if (!match) {
+ return "";
+ }
+ var seconds = match[1];
+ if (seconds < 0 || seconds > 59) {
+ return "";
+ }
+ var other = match[2];
+ if (other == "") {
+ return aValue;
+ }
+ match = /^.([0-9]{1,3})$/.exec(other);
+ if (!match) {
+ return "";
+ }
+ return aValue;
+}
+
+function sanitizeDateTimeLocal(aValue)
+{
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-local-date-and-time-string
+ if (aValue.length < 16) {
+ return "";
+ }
+
+ var sepIndex = aValue.indexOf("T");
+ if (sepIndex == -1) {
+ sepIndex = aValue.indexOf(" ");
+ if (sepIndex == -1) {
+ return "";
+ }
+ }
+
+ var [date, time] = aValue.split(aValue[sepIndex]);
+ if (!sanitizeDate(date)) {
+ return "";
+ }
+
+ if (!sanitizeTime(time)) {
+ return "";
+ }
+
+ // Normalize datetime-local string.
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-normalised-local-date-and-time-string
+ if (aValue[sepIndex] == " ") {
+ aValue = date + "T" + time;
+ }
+
+ if ((aValue.length - sepIndex) == 6) {
+ return aValue;
+ }
+
+ if ((aValue.length - sepIndex) > 9) {
+ var milliseconds = aValue.substring(sepIndex + 10);
+ if (Number(milliseconds) != 0) {
+ return aValue;
+ }
+ aValue = aValue.slice(0, sepIndex + 9);
+ }
+
+ var seconds = aValue.substring(sepIndex + 7);
+ if (Number(seconds) != 0) {
+ return aValue;
+ }
+ aValue = aValue.slice(0, sepIndex + 6);
+
+ return aValue;
+}
+
+function sanitizeValue(aType, aValue)
+{
+ // http://www.whatwg.org/html/#value-sanitization-algorithm
+ switch (aType) {
+ case "text":
+ case "password":
+ case "search":
+ case "tel":
+ return aValue.replace(/[\n\r]/g, "");
+ case "url":
+ case "email":
+ return aValue.replace(/[\n\r]/g, "").replace(/^[\u0020\u0009\t\u000a\u000c\u000d]+|[\u0020\u0009\t\u000a\u000c\u000d]+$/g, "");
+ case "number":
+ return isNaN(Number(aValue)) ? "" : aValue;
+ case "range":
+ var defaultMinimum = 0;
+ var defaultMaximum = 100;
+ var value = Number(aValue);
+ if (isNaN(value)) {
+ return ((defaultMaximum - defaultMinimum)/2).toString(); // "50"
+ }
+ if (value < defaultMinimum) {
+ return defaultMinimum.toString();
+ }
+ if (value > defaultMaximum) {
+ return defaultMaximum.toString();
+ }
+ return aValue;
+ case "date":
+ return sanitizeDate(aValue);
+ case "time":
+ return sanitizeTime(aValue);
+ case "month":
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-month-string
+ var match = /^([0-9]{4,})-([0-9]{2})$/.exec(aValue);
+ if (!match) {
+ return "";
+ }
+ var year = Number(match[1]);
+ if (year === 0) {
+ return "";
+ }
+ var month = Number(match[2]);
+ if (month > 12 || month < 1) {
+ return "";
+ }
+ return aValue;
+ case "week":
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-week-string
+ function isLeapYear(aYear) {
+ return ((aYear % 4 == 0) && (aYear % 100 != 0)) || (aYear % 400 == 0);
+ }
+ function getDayofWeek(aYear, aMonth, aDay) { /* 0 = Sunday */
+ // Tomohiko Sakamoto algorithm.
+ var monthTable = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
+ aYear -= Number(aMonth < 3);
+
+ return (aYear + parseInt(aYear / 4) - parseInt(aYear / 100) +
+ parseInt(aYear / 400) + monthTable[aMonth - 1] + aDay) % 7;
+ }
+ function getMaximumWeekInYear(aYear) {
+ var day = getDayofWeek(aYear, 1, 1);
+ return day == 4 || (day == 3 && isLeapYear(aYear)) ? 53 : 52;
+ }
+
+ var match = /^([0-9]{4,})-W([0-9]{2})$/.exec(aValue);
+ if (!match) {
+ return "";
+ }
+ var year = Number(match[1]);
+ if (year === 0) {
+ return "";
+ }
+ var week = Number(match[2]);
+ if (week > 53 || month < 1) {
+ return "";
+ }
+ return 1 <= week && week <= getMaximumWeekInYear(year) ? aValue : "";
+ case "datetime-local":
+ return sanitizeDateTimeLocal(aValue);
+ case "color":
+ return /^#[0-9A-Fa-f]{6}$/.exec(aValue) ? aValue.toLowerCase() : "#000000";
+ default:
+ return aValue;
+ }
+}
+
+function checkSanitizing(element, inputTypeDescription)
+{
+ var testData =
+ [
+ // For text, password, search, tel, email:
+ "\n\rfoo\n\r",
+ "foo\n\rbar",
+ " foo ",
+ " foo\n\r bar ",
+ // For url:
+ "\r\n foobar \n\r",
+ "\u000B foo \u000B",
+ "\u000A foo \u000A",
+ "\u000C foo \u000C",
+ "\u000d foo \u000d",
+ "\u0020 foo \u0020",
+ " \u0009 foo \u0009 ",
+ // For number and range:
+ "42",
+ "13.37",
+ "1.234567898765432",
+ "12foo",
+ "1e2",
+ "3E42",
+ // For date:
+ "1970-01-01",
+ "1234-12-12",
+ "1234567890-01-02",
+ "2012-12-31",
+ "2012-02-29",
+ "2000-02-29",
+ "1234",
+ "1234-",
+ "12345",
+ "1234-01",
+ "1234-012",
+ "1234-01-",
+ "12-12",
+ "999-01-01",
+ "1234-56-78-91",
+ "1234-567-78",
+ "1234--7-78",
+ "abcd-12-12",
+ "thisinotadate",
+ "2012-13-01",
+ "1234-12-42",
+ " 2012-13-01",
+ " 123-01-01",
+ "2012- 3-01",
+ "12- 10- 01",
+ " 12-0-1",
+ "2012-3-001",
+ "2012-12-00",
+ "2012-12-1r",
+ "2012-11-31",
+ "2011-02-29",
+ "2100-02-29",
+ "a2000-01-01",
+ "2000a-01-0'",
+ "20aa00-01-01",
+ "2000a2000-01-01",
+ "2000-1-1",
+ "2000-1-01",
+ "2000-01-1",
+ "2000-01-01 ",
+ "2000- 01-01",
+ "-1970-01-01",
+ "0000-00-00",
+ "0001-00-00",
+ "0000-01-01",
+ "1234-12 12",
+ "1234 12-12",
+ "1234 12 12",
+ // For time:
+ "1",
+ "10",
+ "10:",
+ "10:1",
+ "21:21",
+ ":21:21",
+ "-21:21",
+ " 21:21",
+ "21-21",
+ "21:21:",
+ "21:211",
+ "121:211",
+ "21:21 ",
+ "00:00",
+ "-1:00",
+ "24:00",
+ "00:60",
+ "01:01",
+ "23:59",
+ "99:99",
+ "8:30",
+ "19:2",
+ "19:a2",
+ "4c:19",
+ "10:.1",
+ "1.:10",
+ "13:37:42",
+ "13:37.42",
+ "13:37:42 ",
+ "13:37:42.",
+ "13:37:61.",
+ "13:37:00",
+ "13:37:99",
+ "13:37:b5",
+ "13:37:-1",
+ "13:37:.1",
+ "13:37:1.",
+ "13:37:42.001",
+ "13:37:42.001",
+ "13:37:42.abc",
+ "13:37:42.00c",
+ "13:37:42.a23",
+ "13:37:42.12e",
+ "13:37:42.1e1",
+ "13:37:42.e11",
+ "13:37:42.1",
+ "13:37:42.99",
+ "13:37:42.0",
+ "13:37:42.00",
+ "13:37:42.000",
+ "13:37:42.-1",
+ "13:37:42.1.1",
+ "13:37:42.1,1",
+ "13:37:42.",
+ "foo12:12",
+ "13:37:42.100000000000",
+ // For color
+ "#00ff00",
+ "#000000",
+ "red",
+ "#0f0",
+ "#FFFFAA",
+ "FFAABB",
+ "fFAaBb",
+ "FFAAZZ",
+ "ABCDEF",
+ "#7654321",
+ // For month
+ "1970-01",
+ "1234-12",
+ "123456789-01",
+ "2013-13",
+ "0000-00",
+ "2015-00",
+ "0001-01",
+ "1-1",
+ "888-05",
+ "2013-3",
+ "2013-may",
+ "2000-1a",
+ "2013-03-13",
+ "december",
+ "abcdef",
+ "12",
+ " 2013-03",
+ "2013 - 03",
+ "2013 03",
+ "2013/03",
+ // For week
+ "1970-W01",
+ "1970-W53",
+ "1964-W53",
+ "1900-W10",
+ "2004-W53",
+ "2065-W53",
+ "2099-W53",
+ "2010-W53",
+ "2016-W30",
+ "1900-W3",
+ "2016-w30",
+ "2016-30",
+ "16-W30",
+ "2016-Week30",
+ "2000-100",
+ "0000-W01",
+ "00-W01",
+ "123456-W05",
+ "1985-W100",
+ "week",
+ // For datetime-local
+ "1970-01-01T00:00",
+ "1970-01-01Z12:00",
+ "1970-01-01 00:00:00",
+ "1970-01-01T00:00:00.0",
+ "1970-01-01T00:00:00.00",
+ "1970-01-01T00:00:00.000",
+ "1970-01-01 00:00:00.20",
+ "1969-12-31 23:59",
+ "1969-12-31 23:59:00",
+ "1969-12-31 23:59:00.000",
+ "1969-12-31 23:59:00.30",
+ "123456-01-01T12:00",
+ "123456-01-01T12:00:00",
+ "123456-01-01T12:00:00.0",
+ "123456-01-01T12:00:00.00",
+ "123456-01-01T12:00:00.000",
+ "123456-01-01T12:00:30",
+ "123456-01-01T12:00:00.123",
+ "10000-12-31 20:00",
+ "10000-12-31 20:00:00",
+ "10000-12-31 20:00:00.0",
+ "10000-12-31 20:00:00.00",
+ "10000-12-31 20:00:00.000",
+ "10000-12-31 20:00:30",
+ "10000-12-31 20:00:00.123",
+ "2016-13-01T12:00",
+ "2016-12-32T12:00",
+ "2016-11-08 15:40:30.0",
+ "2016-11-08T15:40:30.00",
+ "2016-11-07T17:30:10",
+ "2016-12-1T12:45",
+ "2016-12-01T12:45:30.123456",
+ "2016-12-01T24:00",
+ "2016-12-01T12:88:30",
+ "2016-12-01T12:30:99",
+ "2016-12-01T12:30:100",
+ "2016-12-01",
+ "2016-12-01T",
+ "2016-Dec-01T00:00",
+ "12-05-2016T00:00",
+ "datetime-local"
+ ];
+
+ for (value of testData) {
+ element.setAttribute('value', value);
+ delayed_is(element.value, sanitizeValue(type, value),
+ "The value has not been correctly sanitized for type=" + type);
+ delayed_is(element.getAttribute('value'), value,
+ "The content value should not have been sanitized");
+
+ if (type in valueModeValue) {
+ element.setAttribute('value', 'tulip');
+ element.value = value;
+ delayed_is(element.value, sanitizeValue(type, value),
+ "The value has not been correctly sanitized for type=" + type);
+ delayed_is(element.getAttribute('value'), 'tulip',
+ "The content value should not have been sanitized");
+ }
+
+ element.setAttribute('value', '');
+ form.reset();
+ element.type = 'checkbox'; // We know this type has no sanitizing algorithm.
+ element.setAttribute('value', value);
+ delayed_is(element.value, value, "The value should not have been sanitized");
+ element.type = type;
+ delayed_is(element.value, sanitizeValue(type, value),
+ "The value has not been correctly sanitized for type=" + type);
+ delayed_is(element.getAttribute('value'), value,
+ "The content value should not have been sanitized");
+
+ element.setAttribute('value', '');
+ form.reset();
+ element.setAttribute('value', value);
+ form.reset();
+ delayed_is(element.value, sanitizeValue(type, value),
+ "The value has not been correctly sanitized for type=" + type);
+ delayed_is(element.getAttribute('value'), value,
+ "The content value should not have been sanitized");
+
+ // Cleaning-up.
+ element.setAttribute('value', '');
+ form.reset();
+ }
+
+ flushDelayedTests(inputTypeDescription);
+}
+
+for (type of inputTypes) {
+ var form = document.forms[0];
+ var element = document.createElement("input");
+ element.style.display = "none";
+ element.type = type;
+ form.appendChild(element);
+
+ checkSanitizing(element, "type=" + type + ", no frame, no editor");
+
+ element.style.display = "";
+ checkSanitizing(element, "type=" + type + ", frame, no editor");
+
+ element.focus();
+ element.blur();
+ checkSanitizing(element, "type=" + type + ", frame, editor");
+
+ element.style.display = "none";
+ checkSanitizing(element, "type=" + type + ", no frame, editor");
+
+ form.removeChild(element);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_setting_value.html b/dom/html/test/forms/test_input_setting_value.html
new file mode 100644
index 0000000000..b6ddd66d24
--- /dev/null
+++ b/dom/html/test/forms/test_input_setting_value.html
@@ -0,0 +1,619 @@
+<!DOCTYPE>
+<html>
+<head>
+ <title>Test for setting input value</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content"><input type="text"></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(() => {
+ const kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input");
+
+ let input = document.querySelector("input[type=text]");
+
+ // Setting value during composition causes committing composition before setting the value.
+ input.focus();
+ let description = 'Setting input value at first "compositionupdate" event: ';
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`);
+ input.value = "def";
+ is(input.value, "def", `${description}input value should be the specified value at "compositionupdate" event (after setting the value)`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "def", `${description}input value should be the specified value at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ todo_is(input.value, "def", `${description}input value should be the specified value at "input" event`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ todo_is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+
+ input.value = "";
+ description = 'Setting input value at second "compositionupdate" event: ';
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`);
+ input.value = "def";
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ is(input.value, "def", `${description}input value should be specified value at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "def", `${description}input value should be specified value at "input" event`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+
+ input.value = "";
+ description = 'Setting input value at "input" event for first composition update: ';
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+
+ input.value = "";
+ description = 'Setting input value at "input" event for second composition update: ';
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+
+ input.value = "";
+ description = 'Setting input value and reframing at "input" event for first composition update: ';
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ input.style.width = "1000px";
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and reframing at "input" event for second composition update: ';
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ input.style.width = "1000px";
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and reframing with flushing layout at "input" event for first composition update: ';
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ input.style.width = "1000px";
+ document.documentElement.scrollTop;
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and reframing with flushing layout at "input" event for second composition update: ';
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "ab",
+ "clauses":
+ [
+ { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 2, "length": 0 },
+ });
+ input.addEventListener("compositionupdate", (aEvent) => {
+ is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`);
+ }, {once: true});
+ input.addEventListener("compositionend", (aEvent) => {
+ todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`);
+ }, {once: true});
+ input.addEventListener("input", (aEvent) => {
+ is(input.value, "abc", `${description}input value should be the composition string at "input" event`);
+ input.value = "def";
+ input.style.width = "1000px";
+ document.documentElement.scrollTop;
+ is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`);
+ }, {once: true});
+ synthesizeCompositionChange(
+ { "composition":
+ { "string": "abc",
+ "clauses":
+ [
+ { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ "caret": { "start": 3, "length": 0 },
+ });
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`);
+ input.style.width = "";
+
+ // autocomplete and correcting misspelled word by spellchecker cause an "input" event with same path as setting input value.
+ input.value = "";
+ description = 'Setting input value at "input" event whose inputType is "insertReplacementText';
+ let inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`);
+ input.value = "def";
+ is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+
+ input.value = "";
+ description = 'Setting input value and reframing at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`);
+ input.value = "def";
+ input.style.width = "1000px";
+ is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and reframing with flushing layout at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`);
+ input.value = "def";
+ input.style.width = "1000px";
+ document.documentElement.scrollTop;
+ is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.style.width = "";
+
+ input.value = "";
+ description = 'Setting input value and destroying the frame at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`);
+ input.value = "def";
+ input.style.display = "none";
+ is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.style.display = "inline";
+
+ input.value = "";
+ description = 'Changing input type at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before changing type)`);
+ input.type = "button";
+ is(input.value, "abc", `${description}input value should keep inserted value at "input" event (after changing type)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(input.value, "abc", `${description}input value should keep inserted value after the last "input" event`);
+ is(input.type, "button", `${description}input type should be changed correctly`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.type = "text";
+ is(input.value, "abc", `${description}input value should keep inserted value immediately after restoring the type`);
+ todo(SpecialPowers.wrap(input).hasEditor, `${description}restoring input type should create editor if it's focused element`);
+ input.blur();
+ input.focus();
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "abc", `${description}input value should keep inserted value after creating editor`);
+
+ input.value = "";
+ description = 'Changing input type and flush layout at "input" event whose inputType is "insertReplacementText';
+ inputEventFired = false;
+ input.addEventListener("input", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ inputEventFired = true;
+ is(input.value, "abc", `${description}input value should be inserted value at "input" event (before changing type)`);
+ input.type = "button";
+ input.getBoundingClientRect().height;
+ is(input.value, "abc", `${description}input value should keep inserted value at "input" event (after changing type)`);
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("abc");
+ is(input.value, "abc", `${description}input value should keep inserted value after the last "input" event`);
+ is(input.type, "button", `${description}input type should be changed correctly`);
+ ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`);
+ input.type = "text";
+ is(input.value, "abc", `${description}input value should keep inserted value immediately after restoring the type`);
+ todo(SpecialPowers.wrap(input).hasEditor, `${description}restoring input type should create editor if it's focused element`);
+ input.blur();
+ input.focus();
+ is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value,
+ `${description}native anonymous text node should have exactly same value as value of <input> element`);
+ is(input.value, "abc", `${description}input value should keep inserted value after creating editor`);
+
+ function testSettingValueFromBeforeInput(aWithEditor, aPreventDefaultOfBeforeInput) {
+ let beforeInputEvents = [];
+ let inputEvents = [];
+ function recordEvent(aEvent) {
+ if (aEvent.type === "beforeinput") {
+ beforeInputEvents.push(aEvent);
+ } else {
+ inputEvents.push(aEvent);
+ }
+ }
+ let condition = `(${aWithEditor ? "with editor" : "without editor"}${aPreventDefaultOfBeforeInput ? ' and canceling "beforeinput" event' : ""}, the pref ${kSetUserInputCancelable ? "allows" : "disallows"} to cancel "beforeinput" event})`;
+ function Reset() {
+ beforeInputEvents = [];
+ inputEvents = [];
+ if (SpecialPowers.wrap(input).hasEditor != aWithEditor) {
+ if (aWithEditor) {
+ input.blur();
+ input.focus(); // Associate `TextEditor` with input
+ if (!SpecialPowers.wrap(input).hasEditor) {
+ ok(false, `${description}Failed to associate TextEditor with the input ${condition}`);
+ return false;
+ }
+ } else {
+ input.blur();
+ input.type = "button";
+ input.type = "text";
+ if (SpecialPowers.wrap(input).hasEditor) {
+ ok(false, `${description}Failed to disassociate TextEditor from the input ${condition}`);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText" ${condition}: `;
+ input.value = "abc";
+ if (!Reset()) {
+ return;
+ }
+ input.addEventListener("beforeinput", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
+ is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
+ is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
+ input.addEventListener("beforeinput", recordEvent);
+ input.addEventListener("input", recordEvent);
+ input.value = "hig";
+ if (aPreventDefaultOfBeforeInput) {
+ aEvent.preventDefault();
+ }
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("def");
+ is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
+ if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since "beforeinput" was canceled`);
+ } else {
+ // XXX This result is different from Chrome (verified with spellchecker).
+ // Chrome inserts the new text to current value and selected range.
+ // It might be reasonable, but we don't touch this for now since it
+ // requires a lot of changes.
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`);
+ is(inputEvents.length, 1, `${description}"input" event should be fired`);
+ if (inputEvents.length) {
+ is(inputEvents[0].inputType,
+ "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ is(inputEvents[0].data, "def",
+ `${description}data of "input" event should be the value specified by setUserInput()`);
+ }
+ }
+ input.removeEventListener("beforeinput", recordEvent);
+ input.removeEventListener("input", recordEvent);
+
+ description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText" and changing the type to "button" ${condition}: `;
+ input.value = "abc";
+ if (!Reset()) {
+ return;
+ }
+ input.addEventListener("beforeinput", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
+ is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
+ is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
+ input.addEventListener("beforeinput", recordEvent);
+ input.addEventListener("input", recordEvent);
+ input.value = "hig";
+ input.type = "button";
+ if (aPreventDefaultOfBeforeInput) {
+ aEvent.preventDefault();
+ }
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("def");
+ is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
+ if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since "beforeinput" was canceled`);
+ } else {
+ // XXX This result is same as Chrome (verified with spellchecker).
+ // But this behavior is not consistent with just setting the value on Chrome.
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`);
+ // Same as Chrome
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since the input element's type is changed`);
+ }
+ input.type = "text";
+ input.removeEventListener("beforeinput", recordEvent);
+ input.removeEventListener("input", recordEvent);
+
+ description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText" and destroying the frame ${condition}: `;
+ input.value = "abc";
+ if (!Reset()) {
+ return;
+ }
+ input.addEventListener("beforeinput", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
+ is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
+ is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
+ input.addEventListener("beforeinput", recordEvent);
+ input.addEventListener("input", recordEvent);
+ input.value = "hig";
+ input.style.display = "none";
+ if (aPreventDefaultOfBeforeInput) {
+ aEvent.preventDefault();
+ }
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("def");
+ is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
+ if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since "beforeinput" was canceled`);
+ } else {
+ // XXX This result is same as Chrome (verified with spellchecker).
+ // But this behavior is not consistent with just setting the value on Chrome.
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`);
+ // Different from Chrome
+ is(inputEvents.length, 1,
+ `${description}"input" event should be fired even if the frame of target is destroyed`);
+ if (inputEvents.length) {
+ is(inputEvents[0].inputType,
+ "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ is(inputEvents[0].data, "def",
+ `${description}data of "input" event should be the value specified by setUserInput()`);
+ }
+ }
+ input.style.display = "inline";
+ input.removeEventListener("beforeinput", recordEvent);
+ input.removeEventListener("input", recordEvent);
+
+ if (aWithEditor) {
+ return;
+ }
+
+ description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText and create editor" ${condition}: `;
+ input.value = "abc";
+ if (!Reset()) {
+ return;
+ }
+ input.addEventListener("beforeinput", (aEvent) => {
+ is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`);
+ is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`);
+ is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`);
+ input.addEventListener("beforeinput", recordEvent);
+ input.addEventListener("input", recordEvent);
+ input.value = "hig";
+ input.focus();
+ if (aPreventDefaultOfBeforeInput) {
+ aEvent.preventDefault();
+ }
+ }, {once: true});
+ SpecialPowers.wrap(input).setUserInput("def");
+ is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`);
+ if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) {
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`);
+ is(inputEvents.length, 0,
+ `${description}"input" event shouldn't be fired since "beforeinput" was canceled`);
+ } else {
+ // XXX This result is different from Chrome (verified with spellchecker).
+ // Chrome inserts the new text to current value and selected range.
+ // It might be reasonable, but we don't touch this for now since it
+ // requires a lot of changes.
+ is(input.value, "hig",
+ `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`);
+ is(inputEvents.length, 1, `${description}"input" event should be fired`);
+ if (inputEvents.length) {
+ is(inputEvents[0].inputType,
+ "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`);
+ is(inputEvents[0].data, "def",
+ `${description}data of "input" event should be the value specified by setUserInput()`);
+ }
+ }
+ input.removeEventListener("beforeinput", recordEvent);
+ input.removeEventListener("input", recordEvent);
+ }
+ // testSettingValueFromBeforeInput(true, true);
+ // testSettingValueFromBeforeInput(true, false);
+ testSettingValueFromBeforeInput(false, true);
+ testSettingValueFromBeforeInput(false, false);
+
+ SimpleTest.finish();
+});
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html b/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html
new file mode 100644
index 0000000000..79a0f3d15a
--- /dev/null
+++ b/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=829606
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 829606</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 829606 **/
+ /*
+ * This test checks that setting .value on an text field (input or textarea)
+ * doesn't scroll the field to its beginning.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ var gTestRunner = null;
+
+ async function test(aElementName)
+ {
+ var element = document.getElementsByTagName(aElementName)[0];
+ element.focus();
+
+ var baseSnapshot = await snapshotWindow(window);
+
+ // This is a sanity check.
+ var s2 = await snapshotWindow(window);
+ var results = compareSnapshots(baseSnapshot, await snapshotWindow(window), true);
+ ok(results[0], "sanity check: screenshots should be the same");
+
+ element.selectionStart = element.selectionEnd = element.value.length;
+
+ setTimeout(function() {
+ sendString('f');
+
+ requestAnimationFrame(async function() {
+ var selectionAtTheEndSnapshot = await snapshotWindow(window);
+ results = compareSnapshots(baseSnapshot, selectionAtTheEndSnapshot, false);
+ ok(results[0], "after appending a character, string should have changed");
+
+ // Re-setting value shouldn't change anything.
+ // eslint-disable-next-line no-self-assign
+ element.value = element.value;
+ var tmpSnapshot = await snapshotWindow(window);
+
+ results = compareSnapshots(baseSnapshot, tmpSnapshot, false);
+ ok(results[0], "re-setting the value should change nothing");
+
+ results = compareSnapshots(selectionAtTheEndSnapshot, tmpSnapshot, true);
+ ok(results[0], "re-setting the value should change nothing");
+
+ element.selectionStart = element.selectionEnd = 0;
+ element.blur();
+
+ gTestRunner.next();
+ });
+ }, 0);
+ }
+
+ // This test checks that when a textarea has a long list of values and the
+ // textarea's value is then changed, the values are shown correctly.
+ async function testCorrectUpdateOnScroll()
+ {
+ var textarea = document.createElement('textarea');
+ textarea.rows = 5;
+ textarea.cols = 10;
+ textarea.value = 'a\nb\nc\nd';
+ document.getElementById('content').appendChild(textarea);
+
+ var baseSnapshot = await snapshotWindow(window);
+
+ textarea.value = '1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n';
+ textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
+
+ var fullSnapshot = await snapshotWindow(window);
+ var results = compareSnapshots(baseSnapshot, fullSnapshot, false);
+ ok(results[0], "sanity check: screenshots should not be the same");
+
+ textarea.value = 'a\nb\nc\nd';
+
+ var tmpSnapshot = await snapshotWindow(window);
+ results = compareSnapshots(baseSnapshot, tmpSnapshot, true);
+ ok(results[0], "textarea view should look like the beginning");
+
+ setTimeout(function() {
+ gTestRunner.next();
+ }, 0);
+ }
+
+ function* runTest()
+ {
+ test('input');
+ yield undefined;
+ test('textarea');
+ yield undefined;
+ testCorrectUpdateOnScroll();
+ yield undefined;
+ SimpleTest.finish();
+ }
+
+ gTestRunner = runTest();
+
+ SimpleTest.waitForFocus(function() {
+ gTestRunner.next();
+ });;
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=829606">Mozilla Bug 829606</a>
+<p id="display"></p>
+<div id="content">
+ <textarea rows='1' cols='5' style='-moz-appearance:none;'>this is a \n long text</textarea>
+ <input size='5' value="this is a very long text" style='-moz-appearance:none;'>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_time_key_events.html b/dom/html/test/forms/test_input_time_key_events.html
new file mode 100644
index 0000000000..c738816653
--- /dev/null
+++ b/dom/html/test/forms/test_input_time_key_events.html
@@ -0,0 +1,221 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1288591
+-->
+<head>
+ <title>Test key events for time control</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input" type="time">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+var testData = [
+ /**
+ * keys: keys to send to the input element.
+ * initialVal: initial value set to the input element.
+ * expectedVal: expected value of the input element after sending the keys.
+ */
+ {
+ // Type 16 in the hour field will automatically change time to 4PM in 12-hour clock
+ keys: ["16"],
+ initialVal: "01:00",
+ expectedVal: "16:00"
+ },
+ {
+ // Type 00 in hour field will automatically convert to 12AM in 12-hour clock
+ keys: ["00"],
+ initialVal: "03:00",
+ expectedVal: "00:00"
+ },
+ {
+ // Type hour > 23 such as 24 will automatically convert to 2
+ keys: ["24"],
+ initialVal: "04:00",
+ expectedVal: "02:00"
+ },
+ {
+ // Type 1030 and select AM.
+ keys: ["1030"],
+ initialVal: "",
+ expectedVal: "10:30"
+ },
+ {
+ // Type 3 in the hour field will automatically advance to the minute field.
+ keys: ["330"],
+ initialVal: "",
+ expectedVal: "03:30"
+ },
+ {
+ // Type 5 in the hour field will automatically advance to the minute field.
+ // Type 7 in the minute field will automatically advance to the AM/PM field.
+ keys: ["57"],
+ initialVal: "",
+ expectedVal: "05:07"
+ },
+ {
+ // Advance to AM/PM field and change to PM.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_ArrowDown"],
+ initialVal: "10:30",
+ expectedVal: "22:30"
+ },
+ {
+ // Right key should do the same thing as TAB key.
+ keys: ["KEY_ArrowRight", "KEY_ArrowRight", "KEY_ArrowDown"],
+ initialVal: "10:30",
+ expectedVal: "22:30"
+ },
+ {
+ // Advance to minute field then back to hour field and decrement.
+ keys: ["KEY_ArrowRight", "KEY_ArrowLeft", "KEY_ArrowDown"],
+ initialVal: "10:30",
+ expectedVal: "09:30"
+ },
+ {
+ // Focus starts on the first field, hour in this case, and increment.
+ keys: ["KEY_ArrowUp"],
+ initialVal: "16:00",
+ expectedVal: "17:00"
+ },
+ {
+ // Advance to minute field and decrement.
+ keys: ["KEY_Tab", "KEY_ArrowDown"],
+ initialVal: "16:00",
+ expectedVal: "16:59"
+ },
+ {
+ // Advance to minute field and increment.
+ keys: ["KEY_Tab", "KEY_ArrowUp"],
+ initialVal: "16:59",
+ expectedVal: "16:00"
+ },
+ {
+ // PageUp on hour field increments hour by 3.
+ keys: ["KEY_PageUp"],
+ initialVal: "05:00",
+ expectedVal: "08:00"
+ },
+ {
+ // PageDown on hour field decrements hour by 3.
+ keys: ["KEY_PageDown"],
+ initialVal: "05:00",
+ expectedVal: "02:00"
+ },
+ {
+ // PageUp on minute field increments minute by 10.
+ keys: ["KEY_Tab", "KEY_PageUp"],
+ initialVal: "14:00",
+ expectedVal: "14:10"
+ },
+ {
+ // PageDown on minute field decrements minute by 10.
+ keys: ["KEY_Tab", "KEY_PageDown"],
+ initialVal: "14:00",
+ expectedVal: "14:50"
+ },
+ {
+ // Home key on hour field sets it to the minimum hour, which is 1 in 12-hour
+ // clock.
+ keys: ["KEY_Home"],
+ initialVal: "03:10",
+ expectedVal: "01:10"
+ },
+ {
+ // End key on hour field sets it to the maximum hour, which is 12PM in 12-hour
+ // clock.
+ keys: ["KEY_End"],
+ initialVal: "03:10",
+ expectedVal: "12:10"
+ },
+ {
+ // Home key on minute field sets it to the minimum minute, which is 0.
+ keys: ["KEY_Tab", "KEY_Home"],
+ initialVal: "19:30",
+ expectedVal: "19:00"
+ },
+ {
+ // End key on minute field sets it to the minimum minute, which is 59.
+ keys: ["KEY_Tab", "KEY_End"],
+ initialVal: "19:30",
+ expectedVal: "19:59"
+ },
+ // Second field will show up when needed.
+ {
+ // PageUp on second field increments second by 10.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_PageUp"],
+ initialVal: "08:10:10",
+ expectedVal: "08:10:20"
+ },
+ {
+ // PageDown on second field increments second by 10.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_PageDown"],
+ initialVal: "08:10:10",
+ expectedVal: "08:10:00"
+ },
+ {
+ // Home key on second field sets it to the minimum second, which is 0.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_Home"],
+ initialVal: "16:00:30",
+ expectedVal: "16:00:00"
+ },
+ {
+ // End key on second field sets it to the minimum second, which is 59.
+ keys: ["KEY_Tab", "KEY_Tab", "KEY_End"],
+ initialVal: "16:00:30",
+ expectedVal: "16:00:59"
+ },
+ {
+ // Incomplete value maps to empty .value.
+ keys: ["1"],
+ initialVal: "",
+ expectedVal: ""
+ },
+];
+
+function sendKeys(aKeys, aElem) {
+ for (let i = 0; i < aKeys.length; i++) {
+ // Force layout flush between keys to ensure focus is correct.
+ // This shouldn't be necessary; bug 1450219 tracks this.
+ aElem.clientTop;
+ let key = aKeys[i];
+ if (key.startsWith("KEY_")) {
+ synthesizeKey(key);
+ } else {
+ sendString(key);
+ }
+ }
+}
+
+function test() {
+ var elem = document.getElementById("input");
+
+ for (let { keys, initialVal, expectedVal } of testData) {
+ elem.focus();
+ elem.value = initialVal;
+ sendKeys(keys, elem);
+ is(elem.value, expectedVal,
+ "Test with " + keys + ", result should be " + expectedVal);
+ elem.value = "";
+ elem.blur();
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_time_sec_millisec_field.html b/dom/html/test/forms/test_input_time_sec_millisec_field.html
new file mode 100644
index 0000000000..71db4942a9
--- /dev/null
+++ b/dom/html/test/forms/test_input_time_sec_millisec_field.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1374967
+-->
+<head>
+ <title>Test second and millisecond fields in input type=time</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <meta charset="UTF-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1374967">Mozilla Bug 1374967</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input1" type="time">
+ <input id="input2" type="time" value="12:30:40">
+ <input id="input3" type="time" value="12:30:40.567">
+ <input id="input4" type="time" step="1">
+ <input id="input5" type="time" step="61">
+ <input id="input6" type="time" step="120">
+ <input id="input7" type="time" step="0.01">
+ <input id="input8" type="time" step="0.001">
+ <input id="input9" type="time" step="1.001">
+ <input id="input10" type="time" min="01:30:05">
+ <input id="input11" type="time" min="01:30:05.100">
+ <input id="dummy">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ test();
+ SimpleTest.finish();
+});
+
+const NUM_OF_FIELDS_DEFAULT = 3;
+const NUM_OF_FIELDS_WITH_SECOND = NUM_OF_FIELDS_DEFAULT + 1;
+const NUM_OF_FIELDS_WITH_MILLISEC = NUM_OF_FIELDS_WITH_SECOND + 1;
+
+function countNumberOfFields(aElement) {
+ is(aElement.type, "time", "Input element type should be 'time'");
+
+ let inputRect = aElement.getBoundingClientRect();
+ let firstField_X = 15;
+ let firstField_Y = inputRect.height / 2;
+
+ // Make sure to start on the first field.
+ synthesizeMouse(aElement, firstField_X, firstField_Y, {});
+ is(document.activeElement, aElement, "Input element should be focused");
+
+ let n = 0;
+ while (document.activeElement == aElement) {
+ n++;
+ synthesizeKey("KEY_Tab");
+ }
+
+ return n;
+}
+
+function test() {
+ // Normal input time element.
+ let elem = document.getElementById("input1");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_DEFAULT, "Default input time");
+
+ // Dynamically changing the value with second part.
+ elem.value = "10:20:30";
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time after changing value with second part");
+
+ // Dynamically changing the step to 1 millisecond.
+ elem.step = "0.001";
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time after changing step to 1 millisecond");
+
+ // Input time with value with second part.
+ elem = document.getElementById("input2");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time with value with second part");
+
+ // Input time with value with second and millisecond part.
+ elem = document.getElementById("input3");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with value with second and millisecond part");
+
+ // Input time with step set as 1 second.
+ elem = document.getElementById("input4");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time with step set as 1 second");
+
+ // Input time with step set as 61 seconds.
+ elem = document.getElementById("input5");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time with step set as 61 seconds");
+
+ // Input time with step set as 2 minutes.
+ elem = document.getElementById("input6");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_DEFAULT,
+ "Input time with step set as 2 minutes");
+
+ // Input time with step set as 10 milliseconds.
+ elem = document.getElementById("input7");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with step set as 10 milliseconds");
+
+ // Input time with step set as 100 milliseconds.
+ elem = document.getElementById("input8");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with step set as 100 milliseconds");
+
+ // Input time with step set as 1001 milliseconds.
+ elem = document.getElementById("input9");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with step set as 1001 milliseconds");
+
+ // Input time with min with second part and default step (60 seconds). Note
+ // that step base is min, when there is a min.
+ elem = document.getElementById("input10");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND,
+ "Input time with min with second part");
+
+ // Input time with min with second and millisecond part and default step (60
+ // seconds). Note that step base is min, when there is a min.
+ elem = document.getElementById("input11");
+ is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC,
+ "Input time with min with second and millisecond part");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_types_pref.html b/dom/html/test/forms/test_input_types_pref.html
new file mode 100644
index 0000000000..1222e88a86
--- /dev/null
+++ b/dom/html/test/forms/test_input_types_pref.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=764481
+-->
+<head>
+ <title>Test for Bug 764481</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=764481">Mozilla Bug 764481</a>
+<p id="display"></p>
+<div id="content" style="display: none" >
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+ var input = document.createElement("input");
+
+ var testData = [
+ {
+ prefs: [["dom.forms.datetime.others", false]],
+ inputType: "month",
+ expectedType: "text"
+ }, {
+ prefs: [["dom.forms.datetime.others", false]],
+ inputType: "month",
+ expectedType: "text"
+ }, {
+ prefs: [["dom.forms.datetime.others", true]],
+ inputType: "month",
+ expectedType: "month"
+ }, {
+ prefs: [["dom.forms.datetime.others", false]],
+ inputType: "week",
+ expectedType: "text"
+ }, {
+ prefs: [["dom.forms.datetime.others", false]],
+ inputType: "week",
+ expectedType: "text"
+ }, {
+ prefs: [["dom.forms.datetime.others", true]],
+ inputType: "week",
+ expectedType: "week"
+ }
+ ];
+
+ function testInputTypePreference(aData) {
+ return SpecialPowers.pushPrefEnv({'set': aData.prefs})
+ .then(() => {
+ // Change the type of input to text and then back to the tested input type,
+ // so that HTMLInputElement::ParseAttribute gets called with the pref enabled.
+ input.type = "text";
+ input.type = aData.inputType;
+ is(input.type, aData.expectedType, "input type should be '" +
+ aData.expectedType + "'' when pref " + aData.prefs + " is set");
+ is(input.getAttribute('type'), aData.inputType,
+ "input 'type' attribute should not change");
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ let promise = Promise.resolve();
+ for (let i = 0; i < testData.length; i++) {
+ let data = testData[i];
+ promise = promise.then(() => testInputTypePreference(data));
+ }
+
+ promise.catch(error => ok(false, "Promise reject: " + error))
+ .then(() => SimpleTest.finish());
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_typing_sanitization.html b/dom/html/test/forms/test_input_typing_sanitization.html
new file mode 100644
index 0000000000..fef0ebed06
--- /dev/null
+++ b/dom/html/test/forms/test_input_typing_sanitization.html
@@ -0,0 +1,217 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=765772
+-->
+<head>
+ <title>Test for Bug 765772</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug 765772</a>
+<p id="display"></p>
+<iframe name="submit_frame" style="visibility: hidden;"></iframe>
+<div id="content">
+ <form id='f' target="submit_frame" action="foo">
+ <input name=i id="i" step='any' >
+ </form>
+</div>
+<pre id="test">
+<script>
+
+/*
+ * This test checks that when a user types in some input types, it will not be
+ * in a state where the value will be un-sanitized and usable (by a script).
+ */
+
+var input = document.getElementById('i');
+var form = document.getElementById('f');
+var submitFrame = document.getElementsByTagName('iframe')[0];
+var testData = [];
+var gCurrentTest = null;
+var gValidData = [];
+var gInvalidData = [];
+
+function submitForm() {
+ form.submit();
+}
+
+function sendKeyEventToSubmitForm() {
+ sendKey("return");
+}
+
+function urlify(aStr) {
+ return aStr.replace(/:/g, '%3A');
+}
+
+function runTestsForNextInputType()
+{
+ let {done} = testRunner.next();
+ if (done) {
+ SimpleTest.finish();
+ }
+}
+
+function checkValueSubmittedIsValid()
+{
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?i=${urlify(gValidData[valueIndex++])}`,
+ "The submitted value should not have been sanitized");
+
+ input.value = "";
+
+ if (valueIndex >= gValidData.length) {
+ if (gCurrentTest.canHaveBadInputValidityState) {
+ // Don't run the submission tests on the invalid input if submission
+ // will be blocked by invalid input.
+ runTestsForNextInputType();
+ return;
+ }
+ valueIndex = 0;
+ submitFrame.onload = checkValueSubmittedIsInvalid;
+ testData = gInvalidData;
+ }
+ testSubmissions();
+}
+
+function checkValueSubmittedIsInvalid()
+{
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?i=`,
+ "The submitted value should have been sanitized");
+
+ valueIndex++;
+ input.value = "";
+
+ if (valueIndex >= gInvalidData.length) {
+ if (submitMethod == sendKeyEventToSubmitForm) {
+ runTestsForNextInputType();
+ return;
+ }
+ valueIndex = 0;
+ submitMethod = sendKeyEventToSubmitForm;
+ submitFrame.onload = checkValueSubmittedIsValid;
+ testData = gValidData;
+ }
+ testSubmissions();
+}
+
+function testSubmissions() {
+ input.focus();
+ sendString(testData[valueIndex]);
+ submitMethod();
+}
+
+var valueIndex = 0;
+var submitMethod = submitForm;
+
+SimpleTest.waitForExplicitFinish();
+
+function* runTest()
+{
+ SimpleTest.requestLongerTimeout(4);
+
+ var data = [
+ {
+ type: 'number',
+ canHaveBadInputValidityState: true,
+ validData: [
+ "42",
+ "-42", // should work for negative values
+ "42.1234",
+ "123.123456789123", // double precision
+ "1e2", // e should be usable
+ "2e1",
+ "1e-1", // value after e can be negative
+ "1E2", // E can be used instead of e
+ ],
+ invalidData: [
+ "e",
+ "e2",
+ "1e0.1",
+ "foo",
+ "42,13", // comma can't be used as a decimal separator
+ ]
+ },
+ {
+ type: 'month',
+ validData: [
+ '0001-01',
+ '2012-12',
+ '100000-01',
+ ],
+ invalidData: [
+ '1-01',
+ '-',
+ 'december',
+ '2012-dec',
+ '2012/12',
+ '2012-99',
+ '2012-1',
+ ]
+ },
+ {
+ type: 'week',
+ validData: [
+ '0001-W01',
+ '1970-W53',
+ '100000-W52',
+ '2016-W30',
+ ],
+ invalidData: [
+ '1-W01',
+ 'week',
+ '2016-30',
+ '2010-W80',
+ '2000/W30',
+ '1985-W00',
+ '1000-W'
+ ]
+ },
+ ];
+
+ for (test of data) {
+ gCurrentTest = test;
+
+ input.type = test.type;
+ gValidData = test.validData;
+ gInvalidData = test.invalidData;
+
+ for (data of gValidData) {
+ input.value = "";
+ input.focus();
+ sendString(data);
+ input.blur();
+ is(input.value, data, "valid user input should not be sanitized");
+ }
+
+ for (data of gInvalidData) {
+ input.value = "";
+ input.focus();
+ sendString(data);
+ input.blur();
+ is(input.value, "", "invalid user input should be sanitized");
+ }
+
+ input.value = '';
+
+ testData = gValidData;
+ valueIndex = 0;
+ submitFrame.onload = checkValueSubmittedIsValid;
+ testSubmissions();
+ yield undefined;
+ }
+}
+
+var testRunner = runTest();
+
+addLoadEvent(function () {
+ testRunner.next();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_untrusted_key_events.html b/dom/html/test/forms/test_input_untrusted_key_events.html
new file mode 100644
index 0000000000..78e35f525f
--- /dev/null
+++ b/dom/html/test/forms/test_input_untrusted_key_events.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for untrusted DOM KeyboardEvent on input element</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+ <input id="input">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runNextTest, window);
+
+const kTests = [
+ { type: "text", value: "foo", key: "b", expectedNewValue: "foo" },
+ { type: "number", value: "123", key: "4", expectedNewValue: "123" },
+ { type: "number", value: "123", key: KeyEvent.DOM_VK_UP, expectedNewValue: "123" },
+ { type: "number", value: "123", key: KeyEvent.DOM_VK_DOWN, expectedNewValue: "123" },
+];
+
+function sendUntrustedKeyEvent(eventType, keyCode, target) {
+ var evt = new KeyboardEvent(eventType, {
+ bubbles: true,
+ cancelable: true,
+ view: document.defaultView,
+ keyCode,
+ charCode: 0,
+ });
+ target.dispatchEvent(evt);
+}
+
+var input = document.getElementById("input");
+
+var gotEvents = {};
+
+function handleEvent(event) {
+ gotEvents[event.type] = true;
+}
+
+input.addEventListener("keydown", handleEvent);
+input.addEventListener("keyup", handleEvent);
+input.addEventListener("keypress", handleEvent);
+
+var previousTest = null;
+
+function runNextTest() {
+ if (previousTest) {
+ var msg = "For <input " + "type=" + previousTest.type + ">, ";
+ is(gotEvents.keydown, true, msg + "checking got keydown");
+ is(gotEvents.keyup, true, msg + "checking got keyup");
+ is(gotEvents.keypress, true, msg + "checking got keypress");
+ is(input.value, previousTest.expectedNewValue, msg + "checking element " +
+ " after being sent '" + previousTest.key + "' key events");
+ }
+
+ // reset flags
+ gotEvents.keydown = false;
+ gotEvents.keyup = false;
+ gotEvents.keypress = false;
+
+
+ var test = kTests.shift();
+ if (!test) {
+ SimpleTest.finish();
+ return; // We're all done
+ }
+
+ input.type = test.type;
+ input.focus(); // make sure we still have focus after type change
+ input.value = test.value;
+
+ sendUntrustedKeyEvent("keydown", test.key, input);
+ sendUntrustedKeyEvent("keyup", test.key, input);
+ sendUntrustedKeyEvent("keypress", test.key, input);
+
+ previousTest = test;
+
+ SimpleTest.executeSoon(runNextTest);
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_input_url.html b/dom/html/test/forms/test_input_url.html
new file mode 100644
index 0000000000..3cdf1070bb
--- /dev/null
+++ b/dom/html/test/forms/test_input_url.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Tests for &lt;input type='url'&gt; validity</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <input type='url' id='i' oninvalid='invalidEventHandler(event);'>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Tests for <input type='url'> validity **/
+
+// More checks are done in test_bug551670.html.
+
+var gInvalid = false;
+
+function invalidEventHandler(e)
+{
+ is(e.type, "invalid", "Invalid event type should be invalid");
+ gInvalid = true;
+}
+
+function checkValidURL(element)
+{
+ info(`Checking ${element.value}\n`);
+ gInvalid = false;
+ ok(!element.validity.typeMismatch,
+ "Element should not suffer from type mismatch");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "Element should be valid");
+ ok(!gInvalid, "The invalid event should not have been thrown");
+ is(element.validationMessage, '',
+ "Validation message should be the empty string");
+ ok(element.matches(":valid"), ":valid pseudo-class should apply");
+}
+
+function checkInvalidURL(element)
+{
+ gInvalid = false;
+ ok(element.validity.typeMismatch,
+ "Element should suffer from type mismatch");
+ ok(!element.validity.valid, "Element should not be valid");
+ ok(!element.checkValidity(), "Element should not be valid");
+ ok(gInvalid, "The invalid event should have been thrown");
+ is(element.validationMessage, "Please enter a URL.",
+ "Validation message should be related to invalid URL");
+ ok(element.matches(":invalid"),
+ ":invalid pseudo-class should apply");
+}
+
+var url = document.getElementById('i');
+
+var values = [
+ // [ value, validity ]
+ // The empty string should be considered as valid.
+ [ "", true ],
+ [ "foo", false ],
+ [ "http://mozilla.com/", true ],
+ [ "http://mozilla.com", true ],
+ [ "http://mozil\nla\r.com/", true ],
+ [ " http://mozilla.com/ ", true ],
+ [ "\r http://mozilla.com/ \n", true ],
+ [ "file:///usr/bin/tulip", true ],
+ [ "../../bar.html", false ],
+ [ "http://mozillá.org", true ],
+ [ "https://mózillä.org", true ],
+ [ "http://mózillä.órg", true ],
+ [ "ht://mózillä.órg", true ],
+ [ "httŭ://mózillä.órg", false ],
+ [ "chrome://bookmarks", true ],
+];
+
+values.forEach(function([value, valid]) {
+ url.value = value;
+
+ if (valid) {
+ checkValidURL(url);
+ } else {
+ checkInvalidURL(url);
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_interactive_content_in_label.html b/dom/html/test/forms/test_interactive_content_in_label.html
new file mode 100644
index 0000000000..b8d9c81d51
--- /dev/null
+++ b/dom/html/test/forms/test_interactive_content_in_label.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=229925
+-->
+<head>
+ <title>Test for Bug 229925</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=229925">Mozilla Bug 229925</a>
+<p id="display"></p>
+<form action="#">
+ <label>
+ <span id="text">label</span>
+ <input type="button" id="target" value="target">
+
+ <a class="yes" href="#">a</a>
+ <audio class="yes" controls></audio>
+ <button class="yes">button</button>
+ <details class="yes">details</details>
+ <embed class="yes">embed</embed>
+ <iframe class="yes" src="data:text/plain," style="width: 16px; height: 16px;"></iframe>
+ <img class="yes" src="data:image/png," usemap="#map">
+ <input class="yes" type="text" size="4">
+ <keygen class="no">
+ <label class="yes">label</label>
+ <object class="yes" usemap="#map">object</object>
+ <select class="yes"><option>select</option></select>
+ <textarea class="yes" cols="1" rows="1"></textarea>
+ <video class="yes" controls></video>
+
+ <!-- Tests related to shadow tree. -->
+ <div id="root1"> <!-- content will be added by script below. --> </div>
+ <button><div id="root2"> <!-- content will be added by script below. --> </div></button>
+
+ <a class="no">a</a>
+ <audio class="no"></audio>
+ <img class="no" src="data:image/png,">
+ <input class="no" type="hidden">
+ <object class="no">object</object>
+ <video class="no"></video>
+
+ <span class="no" tabindex="1">tabindex</span>
+ <audio class="no" tabindex="1"></audio>
+ <img class="no" src="data:image/png," tabindex="1">
+ <input class="no" type="hidden" tabindex="1">
+ <object class="no" tabindex="1">object</object>
+ <video class="no" tabindex="1"></video>
+ </label>
+</form>
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 229925 **/
+
+var target = document.getElementById("target");
+
+var yes_nodes = Array.from(document.getElementsByClassName("yes"));
+
+var root1 = document.getElementById("root1");
+root1.attachShadow({ mode: "open" }).innerHTML = "<button class=yes>button in shadow tree</button>";
+var root2 = document.getElementById("root2");
+root2.attachShadow({ mode: "open" }).innerHTML = "<div class=yes>text in shadow tree</div>";
+var yes_nodes_in_shadow_tree =
+ Array.from(root1.shadowRoot.querySelectorAll(".yes")).concat(
+ Array.from(root2.shadowRoot.querySelectorAll(".yes")));
+
+var no_nodes = Array.from(document.getElementsByClassName("no"));
+
+var target_clicked = false;
+target.addEventListener("click", function() {
+ target_clicked = true;
+});
+
+var node;
+for (node of yes_nodes) {
+ target_clicked = false;
+ node.click();
+ is(target_clicked, false, "mouse click on interactive content " + node.nodeName + " shouldn't dispatch event to label target");
+}
+
+for (node of yes_nodes_in_shadow_tree) {
+ target_clicked = false;
+ node.click();
+ is(target_clicked, false, "mouse click on content in shadow tree " + node.nodeName + " shouldn't dispatch event to label target");
+}
+
+for (node of no_nodes) {
+ target_clicked = false;
+ node.click();
+ is(target_clicked, true, "mouse click on non interactive content " + node.nodeName + " should dispatch event to label target");
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_interactive_content_in_summary.html b/dom/html/test/forms/test_interactive_content_in_summary.html
new file mode 100644
index 0000000000..f8bac77d89
--- /dev/null
+++ b/dom/html/test/forms/test_interactive_content_in_summary.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1524893
+-->
+<head>
+ <title>Test for Bug 1524893</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1524893">Mozilla Bug 1524893</a>
+
+<details id="details">
+ <summary>
+ <a class="yes" href="#">a</a>
+ <audio class="yes" controls></audio>
+ <button class="yes">button</button>
+ <details class="yes">details</details>
+ <embed class="yes">embed</embed>
+ <iframe class="yes" src="data:text/plain," style="width: 16px; height: 16px;"></iframe>
+ <img class="yes" src="data:image/png," usemap="#map">
+ <input class="yes" type="text" size="4">
+ <keygen class="no">
+ <label class="yes">label</label>
+ <object class="yes" usemap="#map">object</object>
+ <select class="yes"><option>select</option></select>
+ <textarea class="yes" cols="1" rows="1"></textarea>
+ <video class="yes" controls></video>
+
+ <!-- Tests related to shadow tree. -->
+ <div id="root1"> <!-- content will be added by script below. --> </div>
+ <button><div id="root2"> <!-- content will be added by script below. --> </div></button>
+
+ <a class="no">a</a>
+ <audio class="no"></audio>
+ <img class="no" src="data:image/png,">
+ <input class="no" type="hidden">
+ <object class="no">object</object>
+ <video class="no"></video>
+
+ <span class="no" tabindex="1">tabindex</span>
+ <audio class="no" tabindex="1"></audio>
+ <img class="no" src="data:image/png," tabindex="1">
+ <input class="no" type="hidden" tabindex="1">
+ <object class="no" tabindex="1">object</object>
+ <video class="no" tabindex="1"></video>
+ </summary>
+ <div>This is details</div>
+</details>
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1524893 **/
+
+var details = document.getElementById("details");
+
+var yes_nodes = Array.from(document.getElementsByClassName("yes"));
+
+var root1 = document.getElementById("root1");
+root1.attachShadow({ mode: "open" }).innerHTML = "<button class=yes>button in shadow tree</button>";
+var root2 = document.getElementById("root2");
+root2.attachShadow({ mode: "open" }).innerHTML = "<div class=yes>text in shadow tree</div>";
+var yes_nodes_in_shadow_tree =
+ Array.from(root1.shadowRoot.querySelectorAll(".yes")).concat(
+ Array.from(root2.shadowRoot.querySelectorAll(".yes")));
+
+var no_nodes = Array.from(document.getElementsByClassName("no"));
+
+var node;
+for (node of yes_nodes) {
+ details.removeAttribute('open');
+ node.click();
+ ok(!details.hasAttribute('open'),
+ "mouse click on interactive content " + node.nodeName + " shouldn't not open details");
+}
+
+for (node of yes_nodes_in_shadow_tree) {
+ details.removeAttribute('open');
+ node.click();
+ ok(!details.hasAttribute('open'),
+ "mouse click on content in shadow tree " + node.nodeName + " shouldn't open details");
+}
+
+for (node of no_nodes) {
+ details.removeAttribute('open');
+ node.click();
+ ok(details.hasAttribute('open'),
+ "mouse click on non interactive content " + node.nodeName + " should open details");
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_label_control_attribute.html b/dom/html/test/forms/test_label_control_attribute.html
new file mode 100644
index 0000000000..efc04cd787
--- /dev/null
+++ b/dom/html/test/forms/test_label_control_attribute.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=562932
+-->
+<head>
+ <title>Test for Bug 562932</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=562932">Mozilla Bug 562932</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <!-- No @for, we have to check the content -->
+ <label id='l1'><input id='i1'></label>
+ <label id='l2'><input id='i2'><input></label>
+ <label id='l3'></label>
+ <label id='l4a'><fieldset id='f'>foo</fieldset></label>
+ <label id='l4b'><label id='l4c'><input id='i3'></label></label>
+ <label id='l4d'><label id='l4e'><input id='i3b'></label><input></label>
+
+ <!-- With @for, we do no check the content -->
+ <label id='l5' for='i1'></label>
+ <label id='l6' for='i4'></label>
+ <label id='l7' for='i4'><input></label>
+ <label id='l8' for='i1 i2'></label>
+ <label id='l9' for='i1 i2'><input></label>
+ <label id='l10' for='f'></label>
+ <label id='l11' for='i4'></label>
+ <label id='l12' for='i5'></label>
+ <label id='l13' for=''><input></label>
+ <!-- <label id='l14'> is created in script -->
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 562932 **/
+
+function checkControl(aLabelId, aElementId, aMsg)
+{
+ var element = null;
+
+ if (aElementId != null) {
+ element = document.getElementById(aElementId);
+ }
+
+ is(document.getElementById(aLabelId).control, element, aMsg);
+}
+
+ok('control' in document.createElement('label'),
+ "label element should have a control IDL attribute");
+
+checkControl('l1', 'i1', "label control should be the first form element");
+checkControl('l2', 'i2', "label control should be the first form element");
+checkControl('l3', null, "label control should be null when there is no child");
+checkControl('l4a', null, "label control should be null when there is no \
+ labelable form element child");
+checkControl('l4b', 'i3', "label control should be the first labelable element \
+ in tree order");
+checkControl('l4c', 'i3', "label control should be the first labelable element \
+ in tree order");
+checkControl('l4d', 'i3b', "label control should be the first labelable element \
+ in tree order");
+checkControl('l4e', 'i3b', "label control should be the first labelable element \
+ in tree order");
+checkControl('l5', 'i1', "label control should be the id in @for");
+checkControl('l6', null,
+ "label control should be null if the id in @for is not valid");
+checkControl('l7', null,
+ "label control should be null if the id in @for is not valid");
+checkControl('l8', null,
+ "label control should be null if there are more than one id in @for");
+checkControl('l9', null,
+ "label control should be null if there are more than one id in @for");
+checkControl('l10', null, "label control should be null if the id in @for \
+ is not an id from a labelable form element");
+
+var inputOutOfDocument = document.createElement('input');
+inputOutOfDocument.id = 'i4';
+checkControl('l11', null, "label control should be null if the id in @for \
+ is not an id from an element in the document");
+
+var inputInDocument = document.createElement('input');
+inputInDocument.id = 'i5';
+document.getElementById('content').appendChild(inputInDocument);
+checkControl('l12', 'i5', "label control should be the id in @for");
+
+checkControl('l13', null, "label control should be null if the id in @for \
+ is empty");
+
+var labelOutOfDocument = document.createElement('label');
+labelOutOfDocument.htmlFor = 'i1';
+is(labelOutOfDocument.control, null, "out of document label shouldn't \
+ labelize a form control");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_label_input_controls.html b/dom/html/test/forms/test_label_input_controls.html
new file mode 100644
index 0000000000..fe9410b608
--- /dev/null
+++ b/dom/html/test/forms/test_label_input_controls.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=597650
+-->
+<head>
+ <title>Test for Bug 597650</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597650">Mozilla Bug 597650</a>
+ <p id="display"></p>
+ <div id="content">
+ <label id="l">
+ <input id="h"></input>
+ <input type="text" id="i"></input>
+ </label>
+ <label id="lh" for="h"></label>
+ </div>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ /** Test for Bug 597650 **/
+ label = document.getElementById("l");
+ labelForH = document.getElementById("lh");
+ inputI = document.getElementById("i");
+ inputH = document.getElementById("h");
+
+ var labelableTypes = ["text", "search", "tel", "url", "email", "password",
+ "datetime", "date", "month", "week", "time",
+ "number", "range", "color", "checkbox", "radio",
+ "file", "submit", "image", "reset", "button"];
+ var nonLabelableTypes = ["hidden"];
+
+ for (var i in labelableTypes) {
+ test(labelableTypes[i], true);
+ }
+
+ for (var i in nonLabelableTypes) {
+ test(nonLabelableTypes[i], false);
+ }
+
+ function test(type, isLabelable) {
+ inputH.type = type;
+ if (isLabelable) {
+ testControl(label, inputH, type, true);
+ testControl(labelForH, inputH, type, true);
+ } else {
+ testControl(label, inputI, type, false);
+ testControl(labelForH, null, type, false);
+
+ inputH.type = "text";
+ testControl(label, inputH, "text", true);
+ testControl(labelForH, inputH, "text", true);
+
+ inputH.type = type;
+ testControl(label, inputI, type, false);
+ testControl(labelForH, null, type, false);
+
+ label.removeChild(inputH);
+ testControl(label, inputI, "text", true);
+
+ var element = document.createElement('input');
+ element.type = type;
+ label.insertBefore(element, inputI);
+ testControl(label, inputI, "text", true);
+ }
+ }
+
+ function testControl(label, control, type, labelable) {
+ if (labelable) {
+ is(label.control, control, "Input controls of type " + type
+ + " should be labeled");
+ } else {
+ is(label.control, control, "Input controls of type " + type
+ + " should be ignored by <label>");
+ }
+ }
+ </script>
+ </pre>
+ </body>
+</html>
+
diff --git a/dom/html/test/forms/test_max_attribute.html b/dom/html/test/forms/test_max_attribute.html
new file mode 100644
index 0000000000..f6e9c9bd8e
--- /dev/null
+++ b/dom/html/test/forms/test_max_attribute.html
@@ -0,0 +1,473 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=635499
+-->
+<head>
+ <title>Test for Bug 635499</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635499">Mozilla Bug 635499</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 635499 **/
+
+var data = [
+ { type: 'hidden', apply: false },
+ { type: 'text', apply: false },
+ { type: 'search', apply: false },
+ { type: 'tel', apply: false },
+ { type: 'url', apply: false },
+ { type: 'email', apply: false },
+ { type: 'password', apply: false },
+ { type: 'date', apply: true },
+ { type: 'month', apply: true },
+ { type: 'week', apply: true },
+ { type: 'time', apply: true },
+ { type: 'datetime-local', apply: true },
+ { type: 'number', apply: true },
+ { type: 'range', apply: true },
+ { type: 'color', apply: false },
+ { type: 'checkbox', apply: false },
+ { type: 'radio', apply: false },
+ { type: 'file', apply: false },
+ { type: 'submit', apply: false },
+ { type: 'image', apply: false },
+ { type: 'reset', apply: false },
+ { type: 'button', apply: false },
+];
+
+var input = document.createElement("input");
+document.getElementById('content').appendChild(input);
+
+/**
+ * @aValidity - boolean indicating whether the element is expected to be valid
+ * (aElement.validity.valid is true) or not. The value passed is ignored and
+ * overridden with true if aApply is false.
+ * @aApply - boolean indicating whether the min/max attributes apply to this
+ * element type.
+ * @aRangeApply - A boolean that's set to true if the current input type is a
+ * "[candidate] for constraint validation" and it "[has] range limitations"
+ * per http://www.whatwg.org/specs/web-apps/current-work/multipage/selectors.html#selector-in-range
+ * (in other words, one of the pseudo classes :in-range and :out-of-range
+ * should apply (which, depends on aValidity)).
+ * Else (neither :in-range or :out-of-range should match) set to false.
+ */
+function checkValidity(aElement, aValidity, aApply, aRangeApply)
+{
+ aValidity = aApply ? aValidity : true;
+
+ is(aElement.validity.valid, aValidity,
+ "element validity should be " + aValidity);
+ is(aElement.validity.rangeOverflow, !aValidity,
+ "element overflow status should be " + !aValidity);
+ var overflowMsg =
+ (aElement.type == "date" || aElement.type == "time" ||
+ aElement.type == "month" || aElement.type == "week" ||
+ aElement.type == "datetime-local") ?
+ ("Please select a value that is no later than " + aElement.max + ".") :
+ ("Please select a value that is no more than " + aElement.max + ".");
+ is(aElement.validationMessage,
+ aValidity ? "" : overflowMsg, "Checking range overflow validation message");
+
+ is(aElement.matches(":valid"), aElement.willValidate && aValidity,
+ (aElement.willValidate && aValidity) ? ":valid should apply" : "valid shouldn't apply");
+ is(aElement.matches(":invalid"), aElement.willValidate && !aValidity,
+ (aElement.wil && aValidity) ? ":invalid shouldn't apply" : "valid should apply");
+
+ if (!aRangeApply) {
+ ok(!aElement.matches(":in-range"), ":in-range should not match");
+ ok(!aElement.matches(":out-of-range"),
+ ":out-of-range should not match");
+ } else {
+ is(aElement.matches(":in-range"), aValidity,
+ ":in-range matches status should be " + aValidity);
+ is(aElement.matches(":out-of-range"), !aValidity,
+ ":out-of-range matches status should be " + !aValidity);
+ }
+}
+
+for (var test of data) {
+ input.type = test.type;
+ var apply = test.apply;
+
+ // The element should be valid. Range should not apply when @min and @max are
+ // undefined, except if the input type is 'range' (since that type has a
+ // default minimum and maximum).
+ if (input.type == 'range') {
+ checkValidity(input, true, apply, true);
+ } else {
+ checkValidity(input, true, apply, false);
+ }
+ checkValidity(input, true, apply, test.type == 'range');
+
+ switch (input.type) {
+ case 'hidden':
+ case 'text':
+ case 'search':
+ case 'password':
+ case 'url':
+ case 'tel':
+ case 'email':
+ case 'number':
+ case 'checkbox':
+ case 'radio':
+ case 'file':
+ case 'submit':
+ case 'reset':
+ case 'button':
+ case 'image':
+ case 'color':
+ input.max = '-1';
+ break;
+ case 'date':
+ input.max = '2012-06-27';
+ break;
+ case 'time':
+ input.max = '02:20';
+ break;
+ case 'range':
+ // range is special, since setting max to -1 will make it invalid since
+ // it's default would then be 0, meaning it suffers from overflow.
+ input.max = '-1';
+ checkValidity(input, false, apply, apply);
+ // Now make it something that won't cause an error below:
+ input.max = '10';
+ break;
+ case 'month':
+ input.max = '2016-12';
+ break;
+ case 'week':
+ input.max = '2016-W39';
+ break;
+ case 'datetime-local':
+ input.max = '2016-12-31T23:59:59';
+ break;
+ default:
+ ok(false, 'please, add a case for this new type (' + input.type + ')');
+ }
+
+ checkValidity(input, true, apply, apply);
+
+ switch (input.type) {
+ case 'text':
+ case 'hidden':
+ case 'search':
+ case 'password':
+ case 'tel':
+ case 'radio':
+ case 'checkbox':
+ case 'reset':
+ case 'button':
+ case 'submit':
+ case 'image':
+ input.value = '0';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'url':
+ input.value = 'http://mozilla.org';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'email':
+ input.value = 'foo@bar.com';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'file':
+ var file = new File([''], '635499_file');
+
+ SpecialPowers.wrap(input).mozSetFileArray([file]);
+ checkValidity(input, true, apply, apply);
+
+ break;
+ case 'date':
+ input.max = '2012-06-27';
+ input.value = '2012-06-26';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-06-27';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-06-28';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '2012-06-30';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-07-05';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20120-01-01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '0050-01-01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '0049-01-01';
+ checkValidity(input, true, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ case 'number':
+ input.max = '2';
+ input.value = '1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '3';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '5';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '42';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ // Check that we correctly convert input.max to a double in validationMessage.
+ if (input.type == 'number') {
+ input.max = "4.333333333333333333333333333333333331";
+ input.value = "5";
+ is(input.validationMessage,
+ "Please select a value that is no more than 4.33333333333333.",
+ "validation message");
+ }
+
+ break;
+ case 'range':
+ input.max = '2';
+ input.value = '1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '3';
+ checkValidity(input, true, apply, apply);
+
+ is(input.value, input.max, "the value should have been set to max");
+
+ input.max = '5';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '42';
+ checkValidity(input, true, apply, apply);
+
+ is(input.value, input.max, "the value should have been set to max");
+
+ input.max = '';
+ checkValidity(input, true, apply, apply);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ // Check that we correctly convert input.max to a double in validationMessage.
+ input.step = 'any';
+ input.min = 5;
+ input.max = 0.6666666666666666;
+ input.value = 1;
+ is(input.validationMessage,
+ "Please select a value that is no more than 0.666666666666667.",
+ "validation message")
+
+ break;
+ case 'time':
+ // Don't worry about that.
+ input.step = 'any';
+
+ input.max = '10:10';
+ input.value = '10:09';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:10';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:10:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:10:00.000';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:11';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '10:10:00.001';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '01:00:00.01';
+ input.value = '01:00:00.001';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '01:00:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '01:00:00.1';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ case 'month':
+ input.value = '2016-06';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-12';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '2017-07';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-12';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20160-01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '0050-01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '0049-12';
+ checkValidity(input, true, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ case 'week':
+ input.value = '2016-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-W39';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '2017-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-W52';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2100-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '0050-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '0049-W52';
+ checkValidity(input, true, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ case 'datetime-local':
+ input.value = '2016-01-01T12:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-12-31T23:59:59';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-12-31T23:59:59.123';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '2017-01-01T10:00';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '2017-01-01T10:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-01-01T10:00:30';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01-01T12:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2100-01-01T12:00';
+ checkValidity(input, false, apply, apply);
+
+ input.max = '0050-12-31T23:59:59.999';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '0050-12-31T23:59:59';
+ checkValidity(input, true, apply, apply);
+
+ input.max = '';
+ checkValidity(input, true, apply, false);
+
+ input.max = 'foo';
+ checkValidity(input, true, apply, false);
+
+ break;
+ }
+
+ // Cleaning up,
+ input.removeAttribute('max');
+ input.value = '';
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_maxlength_attribute.html b/dom/html/test/forms/test_maxlength_attribute.html
new file mode 100644
index 0000000000..bd76e277e5
--- /dev/null
+++ b/dom/html/test/forms/test_maxlength_attribute.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345624
+-->
+<head>
+ <title>Test for Bug 345624</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input, textarea { background-color: rgb(0,0,0) !important; }
+ :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; }
+ :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345624">Mozilla Bug 345624</a>
+<p id="display"></p>
+<div id="content">
+ <input id='i'>
+ <textarea id='t'></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345624 **/
+
+/**
+ * This test is checking only tooLong related features
+ * related to constraint validation.
+ */
+
+function checkTooLongValidity(element)
+{
+ element.value = "foo";
+ ok(!element.validity.tooLong,
+ "Element should not be too long when maxlength is not set");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.maxLength = 1;
+ ok(!element.validity.tooLong,
+ "Element should not be too long unless the user edits it");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.focus();
+
+ synthesizeKey("KEY_Backspace");
+ is(element.value, "fo", "value should have changed");
+ ok(element.validity.tooLong,
+ "Element should be too long after a user edit that does not make it short enough");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ ok(!element.validity.valid, "Element should be invalid");
+ ok(!element.checkValidity(), "The element should not be valid");
+ is(element.validationMessage,
+ "Please shorten this text to 1 characters or less (you are currently using 2 characters).",
+ "The validation message text is not correct");
+
+ synthesizeKey("KEY_Backspace");
+ is(element.value, "f", "value should have changed");
+ ok(!element.validity.tooLong,
+ "Element should not be too long after a user edit makes it short enough");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+
+ element.maxLength = 2;
+ ok(!element.validity.tooLong,
+ "Element should remain valid if maxlength changes but maxlength > length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+
+ element.maxLength = 1;
+ ok(!element.validity.tooLong,
+ "Element should remain valid if maxlength changes but maxlength = length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.maxLength = 0;
+ ok(element.validity.tooLong,
+ "Element should become invalid if maxlength changes and maxlength < length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ ok(!element.validity.valid, "Element should be invalid");
+ ok(!element.checkValidity(), "The element should not be valid");
+ is(element.validationMessage,
+ "Please shorten this text to 0 characters or less (you are currently using 1 characters).",
+ "The validation message text is not correct");
+
+ element.maxLength = 1;
+ ok(!element.validity.tooLong,
+ "Element should become valid if maxlength changes and maxlength = length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.value = "test";
+ ok(!element.validity.tooLong,
+ "Element should stay valid after programmatic edit (even if value is too long)");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.setCustomValidity("custom message");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ is(element.validationMessage, "custom message",
+ "Custom message should be shown instead of too long one");
+}
+
+checkTooLongValidity(document.getElementById('i'));
+checkTooLongValidity(document.getElementById('t'));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_meter_element.html b/dom/html/test/forms/test_meter_element.html
new file mode 100644
index 0000000000..5e1073d53d
--- /dev/null
+++ b/dom/html/test/forms/test_meter_element.html
@@ -0,0 +1,376 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=657938
+-->
+<head>
+ <title>Test for <meter></title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=657938">Mozilla Bug 657938</a>
+<p id="display"></p>
+<iframe name="submit_frame" style="visibility: hidden;"></iframe>
+<div id="content" style="visibility: hidden;">
+ <form id='f' method='get' target='submit_frame' action='foo'>
+ <meter id='m' value=0.5></meter>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for <meter> **/
+
+function checkFormIDLAttribute(aElement)
+{
+ is('form' in aElement, false, "<meter> shouldn't have a form attribute");
+}
+
+function checkAttribute(aElement, aAttribute, aNewValue, aExpectedValueForIDL)
+{
+ var expectedValueForIDL = aNewValue;
+ var expectedValueForContent = String(aNewValue);
+
+ if (aExpectedValueForIDL !== undefined) {
+ expectedValueForIDL = aExpectedValueForIDL;
+ }
+
+ if (aNewValue != null) {
+ aElement.setAttribute(aAttribute, aNewValue);
+ is(aElement.getAttribute(aAttribute), expectedValueForContent,
+ aAttribute + " content attribute should be " + expectedValueForContent);
+ is(aElement[aAttribute], expectedValueForIDL,
+ aAttribute + " IDL attribute should be " + expectedValueForIDL);
+
+ if (parseFloat(aNewValue) == aNewValue) {
+ aElement[aAttribute] = aNewValue;
+ is(aElement.getAttribute(aAttribute), expectedValueForContent,
+ aAttribute + " content attribute should be " + expectedValueForContent);
+ is(aElement[aAttribute], parseFloat(expectedValueForIDL),
+ aAttribute + " IDL attribute should be " + parseFloat(expectedValueForIDL));
+ }
+ } else {
+ aElement.removeAttribute(aAttribute);
+ is(aElement.getAttribute(aAttribute), null,
+ aAttribute + " content attribute should be null");
+ is(aElement[aAttribute], expectedValueForIDL,
+ aAttribute + " IDL attribute should be " + expectedValueForIDL);
+ }
+}
+
+function checkValueAttribute()
+{
+ var tests = [
+ // value has to be a valid float, its default value is 0.0 otherwise.
+ [ null, 0.0 ],
+ [ 'foo', 0.0 ],
+ // If value < 0.0, 0.0 is used instead.
+ [ -1.0, 0.0 ],
+ // If value >= max, max is used instead (max default value is 1.0).
+ [ 2.0, 1.0 ],
+ [ 1.0, 0.5, 0.5 ],
+ [ 10.0, 5.0, 5.0 ],
+ [ 13.37, 13.37, 42.0 ],
+ // If value <= min, min is used instead (min default value is 0.0).
+ [ 0.5, 1.0, 10.0 ,1.0 ],
+ [ 10.0, 13.37, 42.0 , 13.37],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('max', test[2]);
+ }
+
+ if (test[3]) {
+ element.setAttribute('min', test[3]);
+ }
+
+ checkAttribute(element, 'value', test[0], test[1]);
+
+ element.removeAttribute('max');
+ element.removeAttribute('min');
+ }
+}
+
+function checkMinAttribute()
+{
+ var tests = [
+ // min default value is 0.0.
+ [ null, 0.0 ],
+ [ 'foo', 0.0 ],
+ // Regular reflection.
+ [ 0.5 ],
+ [ 1.0 ],
+ [ 2.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ checkAttribute(element, 'min', test[0], test[1]);
+ }
+}
+
+function checkMaxAttribute()
+{
+ var tests = [
+ // max default value is 1.0.
+ [ null, 1.0 ],
+ [ 'foo', 1.0 ],
+ // If value <= min, min is used instead.
+ [ -1.0, 0.0 ],
+ [ 0.0, 0.5, 0.5 ],
+ [ 10.0, 15.0, 15.0 ],
+ [ 42, 42, 13.37 ],
+ // Regular reflection.
+ [ 0.5 ],
+ [ 1.0 ],
+ [ 2.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('min', test[2]);
+ }
+
+ checkAttribute(element, 'max', test[0], test[1]);
+
+ element.removeAttribute('min');
+ }
+}
+
+function checkLowAttribute()
+{
+ var tests = [
+ // low default value is min (min default value is 0.0).
+ [ null, 0.0 ],
+ [ 'foo', 0.0 ],
+ [ 'foo', 1.0, 1.0],
+ // If low <= min, min is used instead.
+ [ -1.0, 0.0 ],
+ [ 0.0, 0.5, 0.5 ],
+ [ 10.0, 15.0, 15.0, 42.0 ],
+ [ 42.0, 42.0, 13.37, 100.0 ],
+ // If low >= max, max is used instead.
+ [ 2.0, 1.0 ],
+ [ 10.0, 5.0 , 0.5, 5.0 ],
+ [ 13.37, 13.37, 0.0, 42.0 ],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('min', test[2]);
+ }
+ if (test[3]) {
+ element.setAttribute('max', test[3]);
+ }
+
+ checkAttribute(element, 'low', test[0], test[1]);
+
+ element.removeAttribute('min');
+ element.removeAttribute('max');
+ }
+}
+
+function checkHighAttribute()
+{
+ var tests = [
+ // high default value is max (max default value is 1.0).
+ [ null, 1.0 ],
+ [ 'foo', 1.0 ],
+ [ 'foo', 42.0, 0.0, 42.0],
+ // If high <= min, min is used instead.
+ [ -1.0, 0.0 ],
+ [ 0.0, 0.5, 0.5 ],
+ [ 10.0, 15.0, 15.0, 42.0 ],
+ [ 42.0, 42.0, 13.37, 100.0 ],
+ // If high >= max, max is used instead.
+ [ 2.0, 1.0 ],
+ [ 10.0, 5.0 , 0.5, 5.0 ],
+ [ 13.37, 13.37, 0.0, 42.0 ],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('min', test[2]);
+ }
+ if (test[3]) {
+ element.setAttribute('max', test[3]);
+ }
+
+ checkAttribute(element, 'high', test[0], test[1]);
+
+ element.removeAttribute('min');
+ element.removeAttribute('max');
+ }
+}
+
+function checkOptimumAttribute()
+{
+ var tests = [
+ // opt default value is (max-min)/2 (thus default value is 0.5).
+ [ null, 0.5 ],
+ [ 'foo', 0.5 ],
+ [ 'foo', 2.0, 1.0, 3.0],
+ // If opt <= min, min is used instead.
+ [ -1.0, 0.0 ],
+ [ 0.0, 0.5, 0.5 ],
+ [ 10.0, 15.0, 15.0, 42.0 ],
+ [ 42.0, 42.0, 13.37, 100.0 ],
+ // If opt >= max, max is used instead.
+ [ 2.0, 1.0 ],
+ [ 10.0, 5.0 , 0.5, 5.0 ],
+ [ 13.37, 13.37, 0.0, 42.0 ],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('meter');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('min', test[2]);
+ }
+ if (test[3]) {
+ element.setAttribute('max', test[3]);
+ }
+
+ checkAttribute(element, 'optimum', test[0], test[1]);
+
+ element.removeAttribute('min');
+ element.removeAttribute('max');
+ }
+}
+
+function checkFormListedElement(aElement)
+{
+ is(document.forms[0].elements.length, 0, "the form should have no element");
+}
+
+function checkLabelable(aElement)
+{
+ var content = document.getElementById('content');
+ var label = document.createElement('label');
+
+ content.appendChild(label);
+ label.appendChild(aElement);
+ is(label.control, aElement, "meter should be labelable");
+
+ // Cleaning-up.
+ content.removeChild(label);
+ content.appendChild(aElement);
+}
+
+function checkNotResetableAndFormSubmission(aElement)
+{
+ // Creating an input element to check the submission worked.
+ var form = document.forms[0];
+ var input = document.createElement('input');
+
+ input.name = 'a';
+ input.value = 'tulip';
+ form.appendChild(input);
+
+ // Setting values.
+ aElement.value = 42.0;
+ aElement.max = 100.0;
+
+ document.getElementsByName('submit_frame')[0].addEventListener("load", function() {
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?a=tulip`,
+ "The meter element value should not be submitted");
+
+ checkNotResetable();
+ }, {once: true});
+
+ form.submit();
+}
+
+function checkNotResetable()
+{
+ // Try to reset the form.
+ var form = document.forms[0];
+ var element = document.getElementById('m');
+
+ element.value = 3.0;
+ element.max = 42.0;
+
+ form.reset();
+
+ SimpleTest.executeSoon(function() {
+ is(element.value, 3.0, "meter.value should not have changed");
+ is(element.max, 42.0, "meter.max should not have changed");
+
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var m = document.getElementById('m');
+
+ok(m instanceof HTMLMeterElement,
+ "The meter element should be instance of HTMLMeterElement");
+is(m.constructor, HTMLMeterElement,
+ "The meter element constructor should be HTMLMeterElement");
+
+// There is no such attribute.
+checkFormIDLAttribute(m);
+
+checkValueAttribute();
+
+checkMinAttribute();
+
+checkMaxAttribute();
+
+checkLowAttribute();
+
+checkHighAttribute();
+
+checkOptimumAttribute();
+
+checkFormListedElement(m);
+
+checkLabelable(m);
+
+checkNotResetableAndFormSubmission(m);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_meter_pseudo-classes.html b/dom/html/test/forms/test_meter_pseudo-classes.html
new file mode 100644
index 0000000000..e317a58405
--- /dev/null
+++ b/dom/html/test/forms/test_meter_pseudo-classes.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=660238
+-->
+<head>
+ <title>Test for Bug 660238</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=770238">Mozilla Bug 660238</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 660238 **/
+
+function checkOptimum(aElement, aValue, aOptimum, expectedResult)
+{
+ var errorString = expectedResult
+ ? "value attribute should be in the optimum region"
+ : "value attribute should not be in the optimum region";
+
+ aElement.setAttribute('value', aValue);
+ aElement.setAttribute('optimum', aOptimum);
+ is(aElement.matches(":-moz-meter-optimum"),
+ expectedResult, errorString);
+}
+
+function checkSubOptimum(aElement, aValue, aOptimum, expectedResult)
+{
+ var errorString = "value attribute should be in the suboptimal region";
+ if (!expectedResult) {
+ errorString = "value attribute should not be in the suboptimal region";
+ }
+ aElement.setAttribute('value', aValue);
+ aElement.setAttribute('optimum', aOptimum);
+ is(aElement.matches(":-moz-meter-sub-optimum"),
+ expectedResult, errorString);
+}
+
+function checkSubSubOptimum(aElement, aValue, aOptimum, expectedResult)
+{
+ var errorString = "value attribute should be in the sub-suboptimal region";
+ if (!expectedResult) {
+ errorString = "value attribute should not be in the sub-suboptimal region";
+ }
+ aElement.setAttribute('value', aValue);
+ aElement.setAttribute('optimum', aOptimum);
+ is(aElement.matches(":-moz-meter-sub-sub-optimum"),
+ expectedResult, errorString);
+}
+
+function checkMozMatchesSelector()
+{
+ var element = document.createElement('meter');
+ // all tests realised with default values for min and max (0 and 1)
+ // low = 0.3 and high = 0.7
+ element.setAttribute('low', 0.3);
+ element.setAttribute('high', 0.7);
+
+ var tests = [
+ /*
+ * optimum = 0.0 =>
+ * optimum region = [ 0.0, 0.3 [
+ * suboptimal region = [ 0.3, 0.7 ]
+ * sub-suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.0, true, false, false ],
+ [ 0.1, 0.0, true, false, false ],
+ [ 0.3, 0.0, false, true, false ],
+ [ 0.5, 0.0, false, true, false ],
+ [ 0.7, 0.0, false, true, false ],
+ [ 0.8, 0.0, false, false, true ],
+ [ 1.0, 0.0, false, false, true ],
+ /*
+ * optimum = 0.1 =>
+ * optimum region = [ 0.0, 0.3 [
+ * suboptimal region = [ 0.3, 0.7 ]
+ * sub-suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.1, true, false, false ],
+ [ 0.1, 0.1, true, false, false ],
+ [ 0.3, 0.1, false, true, false ],
+ [ 0.5, 0.1, false, true, false ],
+ [ 0.7, 0.1, false, true, false ],
+ [ 0.8, 0.1, false, false, true ],
+ [ 1.0, 0.1, false, false, true ],
+ /*
+ * optimum = 0.3 =>
+ * suboptimal region = [ 0.0, 0.3 [
+ * optimum region = [ 0.3, 0.7 ]
+ * suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.3, false, true, false ],
+ [ 0.1, 0.3, false, true, false ],
+ [ 0.3, 0.3, true, false, false ],
+ [ 0.5, 0.3, true, false, false ],
+ [ 0.7, 0.3, true, false, false ],
+ [ 0.8, 0.3, false, true, false ],
+ [ 1.0, 0.3, false, true, false ],
+ /*
+ * optimum = 0.5 =>
+ * suboptimal region = [ 0.0, 0.3 [
+ * optimum region = [ 0.3, 0.7 ]
+ * suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.5, false, true, false ],
+ [ 0.1, 0.5, false, true, false ],
+ [ 0.3, 0.5, true, false, false ],
+ [ 0.5, 0.5, true, false, false ],
+ [ 0.7, 0.5, true, false, false ],
+ [ 0.8, 0.5, false, true, false ],
+ [ 1.0, 0.5, false, true, false ],
+ /*
+ * optimum = 0.7 =>
+ * suboptimal region = [ 0.0, 0.3 [
+ * optimum region = [ 0.3, 0.7 ]
+ * suboptimal region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.7, false, true, false ],
+ [ 0.1, 0.7, false, true, false ],
+ [ 0.3, 0.7, true, false, false ],
+ [ 0.5, 0.7, true, false, false ],
+ [ 0.7, 0.7, true, false, false ],
+ [ 0.8, 0.7, false, true, false ],
+ [ 1.0, 0.7, false, true, false ],
+ /*
+ * optimum = 0.8 =>
+ * sub-suboptimal region = [ 0.0, 0.3 [
+ * suboptimal region = [ 0.3, 0.7 ]
+ * optimum region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 0.8, false, false, true ],
+ [ 0.1, 0.8, false, false, true ],
+ [ 0.3, 0.8, false, true, false ],
+ [ 0.5, 0.8, false, true, false ],
+ [ 0.7, 0.8, false, true, false ],
+ [ 0.8, 0.8, true, false, false ],
+ [ 1.0, 0.8, true, false, false ],
+ /*
+ * optimum = 1.0 =>
+ * sub-suboptimal region = [ 0.0, 0.3 [
+ * suboptimal region = [ 0.3, 0.7 ]
+ * optimum region = ] 0.7, 1.0 ]
+ */
+ [ 0.0, 1.0, false, false, true ],
+ [ 0.1, 1.0, false, false, true ],
+ [ 0.3, 1.0, false, true, false ],
+ [ 0.5, 1.0, false, true, false ],
+ [ 0.7, 1.0, false, true, false ],
+ [ 0.8, 1.0, true, false, false ],
+ [ 1.0, 1.0, true, false, false ],
+ ];
+
+ for (var test of tests) {
+ checkOptimum(element, test[0], test[1], test[2]);
+ checkSubOptimum(element, test[0], test[1], test[3]);
+ checkSubSubOptimum(element, test[0], test[1], test[4]);
+ }
+}
+
+checkMozMatchesSelector();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_min_attribute.html b/dom/html/test/forms/test_min_attribute.html
new file mode 100644
index 0000000000..a603a37d29
--- /dev/null
+++ b/dom/html/test/forms/test_min_attribute.html
@@ -0,0 +1,473 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=635553
+-->
+<head>
+ <title>Test for Bug 635553</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635499">Mozilla Bug 635499</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 635553 **/
+
+var data = [
+ { type: 'hidden', apply: false },
+ { type: 'text', apply: false },
+ { type: 'search', apply: false },
+ { type: 'tel', apply: false },
+ { type: 'url', apply: false },
+ { type: 'email', apply: false },
+ { type: 'password', apply: false },
+ { type: 'date', apply: true },
+ { type: 'month', apply: true },
+ { type: 'week', apply: true },
+ { type: 'time', apply: true },
+ { type: 'datetime-local', apply: true },
+ { type: 'number', apply: true },
+ { type: 'range', apply: true },
+ { type: 'color', apply: false },
+ { type: 'checkbox', apply: false },
+ { type: 'radio', apply: false },
+ { type: 'file', apply: false },
+ { type: 'submit', apply: false },
+ { type: 'image', apply: false },
+ { type: 'reset', apply: false },
+ { type: 'button', apply: false },
+];
+
+var input = document.createElement("input");
+document.getElementById('content').appendChild(input);
+
+/**
+ * @aValidity - boolean indicating whether the element is expected to be valid
+ * (aElement.validity.valid is true) or not. The value passed is ignored and
+ * overridden with true if aApply is false.
+ * @aApply - boolean indicating whether the min/max attributes apply to this
+ * element type.
+ * @aRangeApply - A boolean that's set to true if the current input type is a
+ * "[candidate] for constraint validation" and it "[has] range limitations"
+ * per http://www.whatwg.org/specs/web-apps/current-work/multipage/selectors.html#selector-in-range
+ * (in other words, one of the pseudo classes :in-range and :out-of-range
+ * should apply (which, depends on aValidity)).
+ * Else (neither :in-range or :out-of-range should match) set to false.
+ */
+function checkValidity(aElement, aValidity, aApply, aRangeApply)
+{
+ aValidity = aApply ? aValidity : true;
+
+ is(aElement.validity.valid, aValidity,
+ "element validity should be " + aValidity);
+ is(aElement.validity.rangeUnderflow, !aValidity,
+ "element underflow status should be " + !aValidity);
+ var underflowMsg =
+ (aElement.type == "date" || aElement.type == "time" ||
+ aElement.type == "month" || aElement.type == "week" ||
+ aElement.type == "datetime-local") ?
+ ("Please select a value that is no earlier than " + aElement.min + ".") :
+ ("Please select a value that is no less than " + aElement.min + ".");
+ is(aElement.validationMessage,
+ aValidity ? "" : underflowMsg, "Checking range underflow validation message");
+
+ is(aElement.matches(":valid"), aElement.willValidate && aValidity,
+ (aElement.willValidate && aValidity) ? ":valid should apply" : "valid shouldn't apply");
+ is(aElement.matches(":invalid"), aElement.willValidate && !aValidity,
+ (aElement.wil && aValidity) ? ":invalid shouldn't apply" : "valid should apply");
+
+ if (!aRangeApply) {
+ ok(!aElement.matches(":in-range"), ":in-range should not match");
+ ok(!aElement.matches(":out-of-range"),
+ ":out-of-range should not match");
+ } else {
+ is(aElement.matches(":in-range"), aValidity,
+ ":in-range matches status should be " + aValidity);
+ is(aElement.matches(":out-of-range"), !aValidity,
+ ":out-of-range matches status should be " + !aValidity);
+ }
+}
+
+for (var test of data) {
+ input.type = test.type;
+ var apply = test.apply;
+
+ if (test.todo) {
+ todo_is(input.type, test.type, test.type + " isn't implemented yet");
+ continue;
+ }
+
+ // The element should be valid. Range should not apply when @min and @max are
+ // undefined, except if the input type is 'range' (since that type has a
+ // default minimum and maximum).
+ if (input.type == 'range') {
+ checkValidity(input, true, apply, true);
+ } else {
+ checkValidity(input, true, apply, false);
+ }
+
+ switch (input.type) {
+ case 'hidden':
+ case 'text':
+ case 'search':
+ case 'password':
+ case 'url':
+ case 'tel':
+ case 'email':
+ case 'number':
+ case 'checkbox':
+ case 'radio':
+ case 'file':
+ case 'submit':
+ case 'reset':
+ case 'button':
+ case 'image':
+ case 'color':
+ input.min = '999';
+ break;
+ case 'date':
+ input.min = '2012-06-27';
+ break;
+ case 'time':
+ input.min = '20:20';
+ break;
+ case 'range':
+ // range is special, since setting min to 999 will make it invalid since
+ // it's default maximum is 100, its value would be 999, and it would
+ // suffer from overflow.
+ break;
+ case 'month':
+ input.min = '2016-06';
+ break;
+ case 'week':
+ input.min = '2016-W39';
+ break;
+ case 'datetime-local':
+ input.min = '2017-01-01T00:00';
+ break;
+ default:
+ ok(false, 'please, add a case for this new type (' + input.type + ')');
+ }
+
+ // The element should still be valid and range should apply if it can.
+ checkValidity(input, true, apply, apply);
+
+ switch (input.type) {
+ case 'text':
+ case 'hidden':
+ case 'search':
+ case 'password':
+ case 'tel':
+ case 'radio':
+ case 'checkbox':
+ case 'reset':
+ case 'button':
+ case 'submit':
+ case 'image':
+ case 'color':
+ input.value = '0';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'url':
+ input.value = 'http://mozilla.org';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'email':
+ input.value = 'foo@bar.com';
+ checkValidity(input, true, apply, apply);
+ break;
+ case 'file':
+ var file = new File([''], '635499_file');
+
+ SpecialPowers.wrap(input).mozSetFileArray([file]);
+ checkValidity(input, true, apply, apply);
+
+ break;
+ case 'date':
+ input.value = '2012-06-28';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-06-27';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-06-26';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '2012-02-29';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2012-02-28';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01-01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '20120-01-01';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '0050-01-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0049-01-01';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ case 'number':
+ input.min = '0';
+ input.value = '1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '-1';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '-1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '-42';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+
+ // Check that we correctly convert input.min to a double in
+ // validationMessage.
+ input.min = "4.333333333333333333333333333333333331";
+ input.value = "2";
+ is(input.validationMessage,
+ "Please select a value that is no less than 4.33333333333333.",
+ "validation message");
+ break;
+ case 'range':
+ input.min = '0';
+ input.value = '1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '-1';
+ checkValidity(input, true, apply, apply);
+
+ is(input.value, input.min, "the value should have been set to min");
+
+ input.min = '-1';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '-42';
+ checkValidity(input, true, apply, apply);
+
+ is(input.value, input.min, "the value should have been set to min");
+
+ input.min = '';
+ checkValidity(input, true, apply, true);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, true);
+
+ // We don't check the conversion of input.min to a double in
+ // validationMessage for 'range' since range will always clamp the value
+ // up to at least the minimum (so we will never see the min in a
+ // validationMessage).
+
+ break;
+ case 'time':
+ // Don't worry about that.
+ input.step = 'any';
+
+ input.min = '20:20';
+ input.value = '20:20:01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20:20:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '10:00';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '20:20:00.001';
+ input.value = '20:20';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '00:00';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '23:59';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20:20:01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20:20:00.01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '20:20:00.1';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '00:00:00';
+ input.value = '01:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '00:00:00.000';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ case 'month':
+ input.value = '2016-07';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-06';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-05';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '2016-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2015-12';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '10000-01';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '0010-01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0001-01';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ case 'week':
+ input.value = '2016-W40';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-W39';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-W38';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '2016-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2015-W53';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '10000-01';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '0010-W01';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0001-W01';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ case 'datetime-local':
+ input.value = '2017-12-31T23:59';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-01-01T00:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2017-01-01T00:00:00.123';
+ checkValidity(input, true, apply, apply);
+
+ input.value = 'foo';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2016-12-31T23:59';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '2016-01-01T00:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '2015-12-31T23:59';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '1000-01-01T00:00';
+ checkValidity(input, false, apply, apply);
+
+ input.value = '10000-01-01T00:00';
+ checkValidity(input, true, apply, apply);
+
+ input.min = '0010-01-01T12:00';
+ checkValidity(input, true, apply, apply);
+
+ input.value = '0010-01-01T10:00';
+ checkValidity(input, false, apply, apply);
+
+ input.min = '';
+ checkValidity(input, true, apply, false);
+
+ input.min = 'foo';
+ checkValidity(input, true, apply, false);
+ break;
+ default:
+ ok(false, 'write tests for ' + input.type);
+ }
+
+ // Cleaning up,
+ input.removeAttribute('min');
+ input.value = '';
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_minlength_attribute.html b/dom/html/test/forms/test_minlength_attribute.html
new file mode 100644
index 0000000000..154343a512
--- /dev/null
+++ b/dom/html/test/forms/test_minlength_attribute.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345624
+-->
+<head>
+ <title>Test for Bug 345624</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input, textarea { background-color: rgb(0,0,0) !important; }
+ :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; }
+ :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345624">Mozilla Bug 345624</a>
+<p id="display"></p>
+<div id="content">
+ <input id='i'>
+ <textarea id='t'></textarea>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345624 **/
+
+/**
+ * This test is checking only tooShort related features
+ * related to constraint validation.
+ */
+
+function checkTooShortValidity(element)
+{
+ element.value = "foo";
+ ok(!element.validity.tooShort,
+ "Element should not be too short when minlength is not set");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.minLength = 5;
+ ok(!element.validity.tooShort,
+ "Element should not be too short unless the user edits it");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.focus();
+
+ sendString("o");
+ is(element.value, "fooo", "value should have changed");
+ ok(element.validity.tooShort,
+ "Element should be too short after a user edit that does not make it short enough");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ ok(!element.validity.valid, "Element should be invalid");
+ ok(!element.checkValidity(), "The element should not be valid");
+ is(element.validationMessage,
+ "Please use at least 5 characters (you are currently using 4 characters).",
+ "The validation message text is not correct");
+
+ sendString("o");
+ is(element.value, "foooo", "value should have changed");
+ ok(!element.validity.tooShort,
+ "Element should not be too short after a user edit makes it long enough");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+
+ element.minLength = 2;
+ ok(!element.validity.tooShort,
+ "Element should remain valid if minlength changes but minlength < length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+
+ element.minLength = 1;
+ ok(!element.validity.tooShort,
+ "Element should remain valid if minlength changes but minlength = length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.minLength = 6;
+ ok(element.validity.tooShort,
+ "Element should become invalid if minlength changes and minlength > length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ ok(!element.validity.valid, "Element should be invalid");
+ ok(!element.checkValidity(), "The element should not be valid");
+ is(element.validationMessage,
+ "Please use at least 6 characters (you are currently using 5 characters).",
+ "The validation message text is not correct");
+
+ element.minLength = 5;
+ ok(!element.validity.tooShort,
+ "Element should become valid if minlength changes and minlength = length");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.value = "test";
+ ok(!element.validity.tooShort,
+ "Element should stay valid after programmatic edit (even if value is too short)");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "The element should be valid");
+
+ element.setCustomValidity("custom message");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ is(element.validationMessage, "custom message",
+ "Custom message should be shown instead of too short one");
+}
+
+checkTooShortValidity(document.getElementById('i'));
+checkTooShortValidity(document.getElementById('t'));
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_mozistextfield.html b/dom/html/test/forms/test_mozistextfield.html
new file mode 100644
index 0000000000..3f92a3d05d
--- /dev/null
+++ b/dom/html/test/forms/test_mozistextfield.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=565538
+-->
+<head>
+ <title>Test for Bug 565538</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=565538">Mozilla Bug 565538</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 565538 **/
+
+var gElementTestData = [
+/* element result */
+ ['input', true],
+ ['button', false],
+ ['fieldset', false],
+ ['label', false],
+ ['option', false],
+ ['optgroup', false],
+ ['output', false],
+ ['legend', false],
+ ['select', false],
+ ['textarea', false],
+ ['object', false],
+];
+
+var gInputTestData = [
+/* type result */
+ ['password', true],
+ ['tel', true],
+ ['text', true],
+ ['button', false],
+ ['checkbox', false],
+ ['file', false],
+ ['hidden', false],
+ ['reset', false],
+ ['image', false],
+ ['radio', false],
+ ['submit', false],
+ ['search', true],
+ ['email', true],
+ ['url', true],
+ ['number', false],
+ ['range', false],
+ ['date', false],
+ ['time', false],
+ ['color', false],
+ ['month', false],
+ ['week', false],
+ ['datetime-local', false],
+];
+
+function checkMozIsTextFieldDefined(aElement, aResult)
+{
+ var element = document.createElement(aElement);
+
+ var msg = "mozIsTextField should be "
+ if (aResult) {
+ msg += "defined";
+ } else {
+ msg += "undefined";
+ }
+
+ is('mozIsTextField' in element, aResult, msg);
+}
+
+function checkMozIsTextFieldValue(aInput, aResult)
+{
+ is(aInput.mozIsTextField(false), aResult,
+ "mozIsTextField(false) should return " + aResult);
+
+ if (aInput.type == 'password') {
+ ok(!aInput.mozIsTextField(true),
+ "mozIsTextField(true) should return false for password");
+ } else {
+ is(aInput.mozIsTextField(true), aResult,
+ "mozIsTextField(true) should return " + aResult);
+ }
+}
+
+function checkMozIsTextFieldValueTodo(aInput, aResult)
+{
+ todo_is(aInput.mozIsTextField(false), aResult,
+ "mozIsTextField(false) should return " + aResult);
+ todo_is(aInput.mozIsTextField(true), aResult,
+ "mozIsTextField(true) should return " + aResult);
+}
+
+// Check if the method is defined for the correct elements.
+for (data of gElementTestData) {
+ checkMozIsTextFieldDefined(data[0], data[1]);
+}
+
+// Check if the method returns the correct value.
+var input = document.createElement('input');
+for (data of gInputTestData) {
+ input.type = data[0];
+ checkMozIsTextFieldValue(input, data[1]);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_novalidate_attribute.html b/dom/html/test/forms/test_novalidate_attribute.html
new file mode 100644
index 0000000000..dcea207838
--- /dev/null
+++ b/dom/html/test/forms/test_novalidate_attribute.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=556013
+-->
+<head>
+ <title>Test for Bug 556013</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=556013">Mozilla Bug 556013</a>
+<p id="display"></p>
+<iframe style='width:50px; height: 50px;' name='t'></iframe>
+<div id="content">
+ <form target='t' action='data:text/html,' novalidate>
+ <input id='av' required>
+ <input id='a' type='submit'>
+ </form>
+ <form target='t' action='data:text/html,' novalidate>
+ <input id='bv' type='checkbox' required>
+ <button id='b' type='submit'></button>
+ </form>
+ <form target='t' action='data:text/html,' novalidate>
+ <input id='c' required>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 556013 **/
+
+/**
+ * novalidate should prevent form validation, thus not blocking form submission.
+ *
+ * NOTE: if the MozInvalidForm event doesn't get prevented default, the form
+ * submission will never be blocked and this test might be a false-positive but
+ * that should not be a problem. We will remove the check for MozInvalidForm
+ * event, see bug 587671.
+ */
+document.forms[0].addEventListener("submit", function(aEvent) {
+ ok(true, "novalidate has been correctly used for first form");
+ document.getElementById('b').click();
+}, {once: true});
+
+document.forms[1].addEventListener("submit", function(aEvent) {
+ ok(true, "novalidate has been correctly used for second form");
+ var c = document.getElementById('c');
+ c.focus();
+ synthesizeKey("KEY_Enter");
+}, {once: true});
+
+document.forms[2].addEventListener("submit", function(aEvent) {
+ ok(true, "novalidate has been correctly used for third form");
+ SimpleTest.executeSoon(SimpleTest.finish);
+}, {once: true});
+
+/**
+ * We have to be sure invalid events are not send too.
+ * They should be sent before the submit event so we can just create a test
+ * failure if we got one. All of them should be catched if sent.
+ * At worst, we got random green which isn't harmful.
+ */
+function invalidHandling(aEvent)
+{
+ aEvent.target.removeEventListener("invalid", invalidHandling);
+ ok(false, "invalid event should not be sent");
+}
+
+document.getElementById('av').addEventListener("invalid", invalidHandling);
+document.getElementById('bv').addEventListener("invalid", invalidHandling);
+document.getElementById('c').addEventListener("invalid", invalidHandling);
+
+SimpleTest.waitForExplicitFinish();
+
+// This is going to call all the tests (with a chain reaction).
+SimpleTest.waitForFocus(function() {
+ document.getElementById('a').click();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_option_disabled.html b/dom/html/test/forms/test_option_disabled.html
new file mode 100644
index 0000000000..421e4546be
--- /dev/null
+++ b/dom/html/test/forms/test_option_disabled.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=759666
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for HTMLOptionElement disabled attribute and pseudo-class</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=759666">Mozilla Bug 759666</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLOptionElement disabled attribute and pseudo-class **/
+
+var testCases = [
+ // Static checks.
+ { html: "<option></option>",
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<option disabled></option>",
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup><option></option></otpgroup>",
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup><option disabled></option></optgroup>",
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup disabled><option disabled></option></optgroup>",
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup disabled><option></option></optgroup>",
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup><optgroup disabled><option></option></optgroup></optgroup>",
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup disabled><optgroup><option></option></optgroup></optgroup>",
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+ result: { attr: "", idl: true, pseudo: true } },
+
+ // Dynamic checks: changing disable value.
+ { html: "<option></option>",
+ modifier(c) { c.querySelector('option').disabled = true; },
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<option disabled></option>",
+ modifier(c) { c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup><option></option></otpgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = true; },
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup><option disabled></option></optgroup>",
+ modifier(c) { c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><option disabled></option></optgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = false; },
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup disabled><option disabled></option></optgroup>",
+ modifier(c) { c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup disabled><option disabled></option></optgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><option></option></optgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup><optgroup disabled><option></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('optgroup[disabled]').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><optgroup><option></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('optgroup[disabled]').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('optgroup').disabled = false; },
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>",
+ modifier(c) { c.querySelector('option').disabled = c.querySelector('option').disabled = false; },
+ result: { attr: null, idl: false, pseudo: false } },
+
+ // Dynamic checks: moving option element.
+ { html: "<optgroup id='a'><option></option></optgroup><optgroup id='b'></optgroup>",
+ modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+ result: { attr: null, idl: false, pseudo: false } },
+ { html: "<optgroup id='a'><option disabled></option></optgroup><optgroup id='b'></optgroup>",
+ modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+ result: { attr: "", idl: true, pseudo: true } },
+ { html: "<optgroup id='a'><option></option></optgroup><optgroup disabled id='b'></optgroup>",
+ modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+ result: { attr: null, idl: false, pseudo: true } },
+ { html: "<optgroup disabled id='a'><option></option></optgroup><optgroup id='b'></optgroup>",
+ modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); },
+ result: { attr: null, idl: false, pseudo: false } },
+];
+
+var content = document.getElementById('content');
+
+testCases.forEach(function(testCase) {
+ var result = testCase.result;
+
+ content.innerHTML = testCase.html;
+
+ if (testCase.modifier !== undefined) {
+ testCase.modifier(content);
+ }
+
+ var option = content.querySelector('option');
+ is(option.getAttribute('disabled'), result.attr, "disabled content attribute value should be " + result.attr);
+ is(option.disabled, result.idl, "disabled idl attribute value should be " + result.idl);
+ is(option.matches(":disabled"), result.pseudo, ":disabled state should be " + result.pseudo);
+ is(option.matches(":enabled"), !result.pseudo, ":enabled state should be " + !result.pseudo);
+
+ content.innerHTML = "";
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_option_index_attribute.html b/dom/html/test/forms/test_option_index_attribute.html
new file mode 100644
index 0000000000..f15520e5e6
--- /dev/null
+++ b/dom/html/test/forms/test_option_index_attribute.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+See those bugs:
+https://bugzilla.mozilla.org/show_bug.cgi?id=720385
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for option.index</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=720385">Mozilla Bug 720385</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <datalist>
+ <option></option>
+ <option></option>
+ </datalist>
+ <select>
+ <option></option>
+ <foo>
+ <option></option>
+ <optgroup>
+ <option></option>
+ </optgroup>
+ <option></option>
+ </foo>
+ <option></option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 720385 **/
+
+var initialIndexes = [ 0, 0, 0, 1, 2, 3, 4 ];
+var options = document.getElementsByTagName('option');
+
+is(options.length, initialIndexes.length,
+ "Must have " + initialIndexes.length +" options");
+
+for (var i=0; i<options.length; ++i) {
+ is(options[i].index, initialIndexes[i], "test");
+}
+
+var o = document.createElement('option');
+is(o.index, 0, "option outside of a document have index=0");
+
+document.body.appendChild(o);
+is(o.index, 0, "option outside of a select have index=0");
+
+var datalist = document.getElementsByTagName('datalist')[0];
+
+datalist.appendChild(o);
+is(o.index, 0, "option outside of a select have index=0");
+
+datalist.removeChild(o);
+is(o.index, 0, "option outside of a select have index=0");
+
+var select = document.getElementsByTagName('select')[0];
+
+select.appendChild(o);
+is(o.index, 5, "option inside a select have an index");
+
+select.removeChild(select.options[0]);
+is(o.index, 4, "option inside a select have an index");
+
+select.insertBefore(o, select.options[0]);
+is(o.index, 0, "option inside a select have an index");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_option_text.html b/dom/html/test/forms/test_option_text.html
new file mode 100644
index 0000000000..3afe3e786a
--- /dev/null
+++ b/dom/html/test/forms/test_option_text.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>HTMLOptionElement.text</title>
+<link rel=author title=Ms2ger href="mailto:Ms2ger@gmail.com">
+<link rel=help href="http://www.whatwg.org/html/#dom-option-text">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id=log></div>
+<script>
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createElement("font"))
+ .appendChild(document.createTextNode(" font "));
+ assert_equals(option.text, "font");
+}, "option.text should recurse");
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createTextNode(" before "));
+ option.appendChild(document.createElement("script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" after "));
+ assert_equals(option.text, "before after");
+}, "option.text should not recurse into HTML script elements");
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createTextNode(" before "));
+ option.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" after "));
+ assert_equals(option.text, "before after");
+}, "option.text should not recurse into SVG script elements");
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createTextNode(" before "));
+ option.appendChild(document.createElementNS("http://www.w3.org/1998/Math/MathML", "script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" after "));
+ assert_equals(option.text, "before script after");
+}, "option.text should recurse into MathML script elements");
+test(function() {
+ var option = document.createElement("option");
+ option.appendChild(document.createTextNode(" before "));
+ option.appendChild(document.createElementNS(null, "script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" after "));
+ assert_equals(option.text, "before script after");
+}, "option.text should recurse into null script elements");
+test(function() {
+ var option = document.createElement("option");
+ var span = option.appendChild(document.createElement("span"));
+ span.appendChild(document.createTextNode(" Some "));
+ span.appendChild(document.createElement("script"))
+ .appendChild(document.createTextNode(" script "));
+ option.appendChild(document.createTextNode(" Text "));
+ assert_equals(option.text, "Some Text");
+}, "option.text should work if a child of the option ends with a script");
+</script>
diff --git a/dom/html/test/forms/test_output_element.html b/dom/html/test/forms/test_output_element.html
new file mode 100644
index 0000000000..ab11443d83
--- /dev/null
+++ b/dom/html/test/forms/test_output_element.html
@@ -0,0 +1,182 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=346485
+-->
+<head>
+ <title>Test for Bug 346485</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ frameLoaded = function() {
+ is(frames.submit_frame.location.href, "about:blank",
+ "Blank frame loaded");
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=346485">Mozilla Bug 346485</a>
+<p id="display"></p>
+<iframe name="submit_frame" onload="frameLoaded()" style="visibility: hidden;"></iframe>
+<div id="content" style="display: none">
+ <form id='f' method='get' target='submit_frame' action='foo'>
+ <input name='a' id='a'>
+ <input name='b' id='b'>
+ <output id='o' for='a b' name='output-name'>tulip</output>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 346485 **/
+
+function checkNameAttribute(element)
+{
+ is(element.name, "output-name", "Output name IDL attribute is not correct");
+ is(element.getAttribute('name'), "output-name",
+ "Output name content attribute is not correct");
+}
+
+function checkValueAndDefaultValueIDLAttribute(element)
+{
+ is(element.value, element.textContent,
+ "The value IDL attribute should act like the textContent IDL attribute");
+
+ element.value = "foo";
+ is(element.value, "foo", "Value should be 'foo'");
+
+ is(element.defaultValue, "tulip", "Default defaultValue is 'tulip'");
+
+ element.defaultValue = "bar";
+ is(element.defaultValue, "bar", "defaultValue should be 'bar'");
+
+ // More complex situation.
+ element.textContent = 'foo';
+ var b = document.createElement('b');
+ b.textContent = 'bar'
+ element.appendChild(b);
+ is(element.value, element.textContent,
+ "The value IDL attribute should act like the textContent IDL attribute");
+}
+
+function checkValueModeFlag(element)
+{
+ /**
+ * The value mode flag is the flag used to know if value should represent the
+ * textContent or the default value.
+ */
+ // value mode flag should be 'value'
+ isnot(element.defaultValue, element.value,
+ "When value is set, defaultValue keeps its value");
+
+ var f = document.getElementById('f');
+ f.reset();
+ // value mode flag should be 'default'
+ is(element.defaultValue, element.value, "When reset, defaultValue=value");
+ is(element.textContent, element.defaultValue,
+ "textContent should contain the defaultValue");
+}
+
+function checkDescendantChanged(element)
+{
+ /**
+ * Whenever a descendant is changed if the value mode flag is value,
+ * the default value should be the textContent value.
+ */
+ element.defaultValue = 'tulip';
+ element.value = 'foo';
+
+ // set value mode flag to 'default'
+ var f = document.getElementById('f');
+ f.reset();
+
+ is(element.textContent, element.defaultValue,
+ "textContent should contain the defaultValue");
+ element.textContent = "bar";
+ is(element.textContent, element.defaultValue,
+ "textContent should contain the defaultValue");
+}
+
+function checkFormIDLAttribute(element)
+{
+ is(element.form, document.getElementById('f'),
+ "form IDL attribute is invalid");
+}
+
+function checkHtmlForIDLAttribute(element)
+{
+ is(String(element.htmlFor), 'a b',
+ "htmlFor IDL attribute should reflect the for content attribute");
+
+ // DOMTokenList is tested in another bug so we just test assignation
+ element.htmlFor.value = 'a b c';
+ is(String(element.htmlFor), 'a b c', "htmlFor should have changed");
+}
+
+function submitForm()
+{
+ // Setting the values for the submit.
+ document.getElementById('o').value = 'foo';
+ document.getElementById('a').value = 'afield';
+ document.getElementById('b').value = 'bfield';
+
+ frameLoaded = checkFormSubmission;
+
+ // This will call checkFormSubmission() which is going to call ST.finish().
+ document.getElementById('f').submit();
+}
+
+function checkFormSubmission()
+{
+ /**
+ * All elements values have been set just before the submission.
+ * The input elements values should be in the submit url but the ouput
+ * element value should not appear.
+ */
+
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?a=afield&b=bfield`,
+ "The output element value should not be submitted");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ reflectString({
+ element: document.createElement("output"),
+ attribute: "name",
+ });
+
+ var o = document.getElementsByTagName('output');
+ is(o.length, 1, "There should be one output element");
+
+ o = o[0];
+ ok(o instanceof HTMLOutputElement,
+ "The output should be instance of HTMLOutputElement");
+
+ o = document.getElementById('o');
+ ok(o instanceof HTMLOutputElement,
+ "The output should be instance of HTMLOutputElement");
+
+ is(o.type, "output", "Output type IDL attribute should be 'output'");
+
+ checkNameAttribute(o);
+
+ checkValueAndDefaultValueIDLAttribute(o);
+
+ checkValueModeFlag(o);
+
+ checkDescendantChanged(o);
+
+ checkFormIDLAttribute(o);
+
+ checkHtmlForIDLAttribute(o);
+
+ submitForm();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_pattern_attribute.html b/dom/html/test/forms/test_pattern_attribute.html
new file mode 100644
index 0000000000..71d79c1def
--- /dev/null
+++ b/dom/html/test/forms/test_pattern_attribute.html
@@ -0,0 +1,324 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345512
+-->
+<head>
+ <title>Test for Bug 345512</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input { background-color: rgb(0,0,0) !important; }
+ input:valid { background-color: rgb(0,255,0) !important; }
+ input:invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345512">Mozilla Bug 345512</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <input id='i' pattern="tulip" oninvalid="invalidEventHandler(event);">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345512 **/
+
+var gInvalid = false;
+
+function invalidEventHandler(e)
+{
+ is(e.type, "invalid", "Invalid event type should be invalid");
+ gInvalid = true;
+}
+
+function completeValidityCheck(element, alwaysValid, isBarred)
+{
+ // Check when pattern matches.
+ if (element.type == 'email') {
+ element.pattern = ".*@bar.com";
+ element.value = "foo@bar.com";
+ } else if (element.type == 'url') {
+ element.pattern = "http://.*\\.com$";
+ element.value = "http://mozilla.com";
+ } else if (element.type == 'file') {
+ element.pattern = "foo";
+ SpecialPowers.wrap(element).mozSetFileArray([new File(["foo"], "foo")]);
+ } else {
+ element.pattern = "foo";
+ element.value = "foo";
+ }
+
+ checkValidPattern(element, true, isBarred);
+
+ // Check when pattern does not match.
+
+ if (element.type == 'email') {
+ element.pattern = ".*@bar.com";
+ element.value = "foo@foo.com";
+ } else if (element.type == 'url') {
+ element.pattern = "http://.*\\.com$";
+ element.value = "http://mozilla.org";
+ } else if (element.type == 'file') {
+ element.pattern = "foo";
+ SpecialPowers.wrap(element).mozSetFileArray([new File(["bar"], "bar")]);
+ } else {
+ element.pattern = "foo";
+ element.value = "bar";
+ }
+
+ if (!alwaysValid) {
+ checkInvalidPattern(element, true);
+ } else {
+ checkValidPattern(element, true, isBarred);
+ }
+}
+
+function checkValidPattern(element, completeCheck, isBarred)
+{
+ if (completeCheck) {
+ gInvalid = false;
+
+ ok(!element.validity.patternMismatch,
+ "Element should not suffer from pattern mismatch");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "Element should be valid");
+ ok(!gInvalid, "Invalid event shouldn't have been thrown");
+ is(element.validationMessage, '',
+ "Validation message should be the empty string");
+ if (element.type != 'radio' && element.type != 'checkbox') {
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ isBarred ? "rgb(0, 0, 0)" : "rgb(0, 255, 0)",
+ "The pseudo-class is not correctly applied");
+ }
+ } else {
+ ok(!element.validity.patternMismatch,
+ "Element should not suffer from pattern mismatch");
+ }
+}
+
+function checkInvalidPattern(element, completeCheck)
+{
+ if (completeCheck) {
+ gInvalid = false;
+
+ ok(element.validity.patternMismatch,
+ "Element should suffer from pattern mismatch");
+ ok(!element.validity.valid, "Element should not be valid");
+ ok(!element.checkValidity(), "Element should not be valid");
+ ok(gInvalid, "Invalid event should have been thrown");
+ is(element.validationMessage,
+ "Please match the requested format.",
+ "Validation message is not valid");
+ } else {
+ ok(element.validity.patternMismatch,
+ "Element should suffer from pattern mismatch");
+ }
+
+ if (element.type != 'radio' && element.type != 'checkbox') {
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ }
+}
+
+function checkSyntaxError(element)
+{
+ ok(!element.validity.patternMismatch,
+ "On SyntaxError, element should not suffer");
+}
+
+function checkPatternValidity(element)
+{
+ element.pattern = "foo";
+
+ element.value = '';
+ checkValidPattern(element);
+
+ element.value = "foo";
+ checkValidPattern(element);
+
+ element.value = "bar";
+ checkInvalidPattern(element);
+
+ element.value = "foobar";
+ checkInvalidPattern(element);
+
+ element.value = "foofoo";
+ checkInvalidPattern(element);
+
+ element.pattern = "foo\"bar";
+ element.value = "foo\"bar";
+ checkValidPattern(element);
+
+ element.value = 'foo"bar';
+ checkValidPattern(element);
+
+ element.pattern = "foo'bar";
+ element.value = "foo\'bar";
+ checkValidPattern(element);
+
+ element.pattern = "foo\\(bar";
+ element.value = "foo(bar";
+ checkValidPattern(element);
+
+ element.value = "foo";
+ checkInvalidPattern(element);
+
+ element.pattern = "foo\\)bar";
+ element.value = "foo)bar";
+ checkValidPattern(element);
+
+ element.value = "foo";
+ checkInvalidPattern(element);
+
+ // Check for 'i' flag disabled. Should be case sensitive.
+ element.value = "Foo";
+ checkInvalidPattern(element);
+
+ // We can't check for the 'g' flag because we only test, we don't execute.
+ // We can't check for the 'm' flag because .value shouldn't contain line breaks.
+
+ // We need '\\\\' because '\\' will produce '\\' and we want to escape the '\'
+ // for the regexp.
+ element.pattern = "foo\\\\bar";
+ element.value = "foo\\bar";
+ checkValidPattern(element);
+
+ // We may want to escape the ' in the pattern, but this is a SyntaxError
+ // when unicode flag is set.
+ element.pattern = "foo\\'bar";
+ element.value = "foo'bar";
+ checkSyntaxError(element);
+ element.value = "baz";
+ checkSyntaxError(element);
+
+ // We should check the pattern attribute do not pollute |RegExp.lastParen|.
+ is(RegExp.lastParen, "", "RegExp.lastParen should be the empty string");
+
+ element.pattern = "(foo)";
+ element.value = "foo";
+ checkValidPattern(element);
+ is(RegExp.lastParen, "", "RegExp.lastParen should be the empty string");
+
+ // That may sound weird but the empty string is a valid pattern value.
+ element.pattern = "";
+ element.value = "";
+ checkValidPattern(element);
+
+ element.value = "foo";
+ checkInvalidPattern(element);
+
+ // Checking some complex patterns. As we are using js regexp mechanism, these
+ // tests doesn't aim to test the regexp mechanism.
+ element.pattern = "\\d{2}\\s\\d{2}\\s\\d{4}"
+ element.value = "01 01 2010"
+ checkValidPattern(element);
+
+ element.value = "01/01/2010"
+ checkInvalidPattern(element);
+
+ element.pattern = "[0-9a-zA-Z]([\\-.\\w]*[0-9a-zA-Z_+])*@([0-9a-zA-Z][\\-\\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9}";
+ element.value = "foo@bar.com";
+ checkValidPattern(element);
+
+ element.value = "...@bar.com";
+ checkInvalidPattern(element);
+
+ element.pattern = "^(?:\\w{3,})$";
+ element.value = "foo";
+ checkValidPattern(element);
+
+ element.value = "f";
+ checkInvalidPattern(element);
+
+ // If @title is specified, it should be added in the validation message.
+ if (element.type == 'email') {
+ element.pattern = "foo@bar.com"
+ element.value = "bar@foo.com";
+ } else if (element.type == 'url') {
+ element.pattern = "http://mozilla.com";
+ element.value = "http://mozilla.org";
+ } else {
+ element.pattern = "foo";
+ element.value = "bar";
+ }
+ element.title = "this is an explanation of the regexp";
+ is(element.validationMessage,
+ "Please match the requested format: " + element.title + ".",
+ "Validation message is not valid");
+ element.title = "";
+ is(element.validationMessage,
+ "Please match the requested format.",
+ "Validation message is not valid");
+
+ element.pattern = "foo";
+ if (element.type == 'email') {
+ element.value = "bar@foo.com";
+ } else if (element.type == 'url') {
+ element.value = "http://mozilla.org";
+ } else {
+ element.value = "bar";
+ }
+ checkInvalidPattern(element);
+
+ element.removeAttribute('pattern');
+ checkValidPattern(element, true);
+
+ // Unicode pattern
+ for (var pattern of ["\\u{1F438}{2}", "\u{1F438}{2}",
+ "\\uD83D\\uDC38{2}", "\uD83D\uDC38{2}",
+ "\u{D83D}\u{DC38}{2}"]) {
+ element.pattern = pattern;
+
+ element.value = "\u{1F438}\u{1F438}";
+ checkValidPattern(element);
+
+ element.value = "\uD83D\uDC38\uD83D\uDC38";
+ checkValidPattern(element);
+
+ element.value = "\uD83D\uDC38\uDC38";
+ checkInvalidPattern(element);
+ }
+
+ element.pattern = "\\u{D83D}\\u{DC38}{2}";
+
+ element.value = "\u{1F438}\u{1F438}";
+ checkInvalidPattern(element);
+
+ element.value = "\uD83D\uDC38\uD83D\uDC38";
+ checkInvalidPattern(element);
+
+ element.value = "\uD83D\uDC38\uDC38";
+ checkInvalidPattern(element);
+}
+
+var input = document.getElementById('i');
+
+// |validTypes| are the types which accept @pattern
+// and |invalidTypes| are the ones which do not accept it.
+var validTypes = Array('text', 'password', 'search', 'tel', 'email', 'url');
+var barredTypes = Array('hidden', 'reset', 'button');
+var invalidTypes = Array('checkbox', 'radio', 'file', 'number', 'range', 'date',
+ 'time', 'color', 'submit', 'image', 'month', 'week',
+ 'datetime-local');
+
+for (type of validTypes) {
+ input.type = type;
+ completeValidityCheck(input, false);
+ checkPatternValidity(input);
+}
+
+for (type of barredTypes) {
+ input.type = type;
+ completeValidityCheck(input, true, true);
+}
+
+for (type of invalidTypes) {
+ input.type = type;
+ completeValidityCheck(input, true);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_preserving_metadata_between_reloads.html b/dom/html/test/forms/test_preserving_metadata_between_reloads.html
new file mode 100644
index 0000000000..07ca05f7ce
--- /dev/null
+++ b/dom/html/test/forms/test_preserving_metadata_between_reloads.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test preserving metadata between page reloads</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ </head>
+<body>
+<p id="display"></p>
+<div id="content">
+ <iframe id="test-frame" width="800px" height="600px" srcdoc='
+ <html>
+ <body>
+ <h3>Bug 1635224: Preserve mLastValueChangeWasInteractive between reloads</h3>
+ <div>
+ <form>
+ <textarea id="maxlen-textarea" maxlength="2" rows="2" cols="10"></textarea><br/>
+ <input id="maxlen-inputtext" type="text" maxlength="2"><br/>
+ <textarea id="minlen-textarea" minlength="8" rows="2" cols="10"></textarea><br/>
+ <input id="minlen-inputtext" type="text" minlength="8"><br/>
+ </form>
+ </div>
+ </body>
+ </html>
+'></iframe>
+</div>
+
+<pre id="test">
+<script>
+ SimpleTest.waitForExplicitFinish()
+ const Ci = SpecialPowers.Ci;
+ const str = "aaaaa";
+
+ function afterLoad() {
+ SimpleTest.waitForFocus(async function () {
+ await SpecialPowers.pushPrefEnv({"set": [["editor.truncate_user_pastes", false]]});
+ var iframeDoc = $("test-frame").contentDocument;
+ var src = iframeDoc.getElementById("src");
+
+ function test(fieldId, callback) {
+ var field = iframeDoc.getElementById(fieldId);
+ field.focus();
+ SimpleTest.waitForClipboard(str,
+ function () {
+ SpecialPowers.Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(str);
+ },
+ function () {
+ synthesizeKey("v", { accelKey: true });
+ is(field.value, "aaaaa", "the value of " + fieldId + " was entered correctly");
+ is(field.checkValidity(), false, "the validity of " + fieldId + " should be false");
+ $("test-frame").contentWindow.location.reload();
+ is(field.value, "aaaaa", "the value of " + fieldId + " persisted correctly");
+ is(field.checkValidity(), false, "the validity of " + fieldId + " should be false after reload");
+ callback();
+ },
+ function () {
+ ok(false, "Failed to copy the string");
+ SimpleTest.finish();
+ }
+ );
+ }
+
+ function runNextTest() {
+ if (fieldIds.length) {
+ var currentFieldId = fieldIds.shift();
+ test(currentFieldId, runNextTest);
+ } else {
+ SimpleTest.finish();
+ }
+ }
+
+ var fieldIds = ["maxlen-textarea", "maxlen-inputtext", "minlen-textarea", "minlen-inputtext"];
+ runNextTest();
+ });
+ }
+ addLoadEvent(afterLoad);
+</script>
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/html/test/forms/test_progress_element.html b/dom/html/test/forms/test_progress_element.html
new file mode 100644
index 0000000000..065adf94ea
--- /dev/null
+++ b/dom/html/test/forms/test_progress_element.html
@@ -0,0 +1,307 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=514437
+https://bugzilla.mozilla.org/show_bug.cgi?id=633913
+-->
+<head>
+ <title>Test for progress element content and layout</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=514437">Mozilla Bug 514437</a>
+and
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=633913">Mozilla Bug 633913</a>
+<p id="display"></p>
+<iframe name="submit_frame" style="visibility: hidden;"></iframe>
+<div id="content" style="visibility: hidden;">
+ <form id='f' method='get' target='submit_frame' action='foo'>
+ <progress id='p'></progress>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.expectAssertions(0, 1);
+
+/** Test for progress element content and layout **/
+
+function checkFormIDLAttribute(aElement)
+{
+ is("form" in aElement, false, "<progress> shouldn't have a form attribute");
+}
+
+function checkAttribute(aElement, aAttribute, aNewValue, aExpectedValueForIDL)
+{
+ var expectedValueForIDL = aNewValue;
+ var expectedValueForContent = String(aNewValue);
+
+ if (aExpectedValueForIDL !== undefined) {
+ expectedValueForIDL = aExpectedValueForIDL;
+ }
+
+ if (aNewValue != null) {
+ aElement.setAttribute(aAttribute, aNewValue);
+ is(aElement.getAttribute(aAttribute), expectedValueForContent,
+ aAttribute + " content attribute should be " + expectedValueForContent);
+ is(aElement[aAttribute], expectedValueForIDL,
+ aAttribute + " IDL attribute should be " + expectedValueForIDL);
+
+ if (parseFloat(aNewValue) == aNewValue) {
+ aElement[aAttribute] = aNewValue;
+ is(aElement.getAttribute(aAttribute), expectedValueForContent,
+ aAttribute + " content attribute should be " + expectedValueForContent);
+ is(aElement[aAttribute], parseFloat(expectedValueForIDL),
+ aAttribute + " IDL attribute should be " + parseFloat(expectedValueForIDL));
+ }
+ } else {
+ aElement.removeAttribute(aAttribute);
+ is(aElement.getAttribute(aAttribute), null,
+ aAttribute + " content attribute should be null");
+ is(aElement[aAttribute], expectedValueForIDL,
+ aAttribute + " IDL attribute should be " + expectedValueForIDL);
+ }
+}
+
+function checkValueAttribute()
+{
+ var tests = [
+ // value has to be a valid float, its default value is 0.0 otherwise.
+ [ null, 0.0 ],
+ [ 'fo', 0.0 ],
+ // If value < 0.0, 0.0 is used instead.
+ [ -1.0, 0.0 ],
+ // If value >= max, max is used instead (max default value is 1.0).
+ [ 2.0, 1.0 ],
+ [ 1.0, 0.5, 0.5 ],
+ [ 10.0, 5.0, 5.0 ],
+ [ 13.37, 13.37, 42.0 ],
+ // Regular reflection.
+ [ 0.0 ],
+ [ 0.5 ],
+ [ 1.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('progress');
+
+ for (var test of tests) {
+ if (test[2]) {
+ element.setAttribute('max', test[2]);
+ }
+
+ checkAttribute(element, 'value', test[0], test[1]);
+
+ element.removeAttribute('max');
+ }
+}
+
+function checkMaxAttribute()
+{
+ var tests = [
+ // max default value is 1.0.
+ [ null, 1.0 ],
+ // If value <= 0.0, 1.0 is used instead.
+ [ 0.0, 1.0 ],
+ [ -1.0, 1.0 ],
+ // Regular reflection.
+ [ 0.5 ],
+ [ 1.0 ],
+ [ 2.0 ],
+ // Check double-precision value.
+ [ 0.234567898765432 ],
+ ];
+
+ var element = document.createElement('progress');
+
+ for (var test of tests) {
+ checkAttribute(element, 'max', test[0], test[1]);
+ }
+}
+
+function checkPositionAttribute()
+{
+ function checkPositionValue(aElement, aValue, aMax, aExpected) {
+ if (aValue != null) {
+ aElement.setAttribute('value', aValue);
+ } else {
+ aElement.removeAttribute('value');
+ }
+
+ if (aMax != null) {
+ aElement.setAttribute('max', aMax);
+ } else {
+ aElement.removeAttribute('max');
+ }
+
+ is(aElement.position, aExpected, "position IDL attribute should be " + aExpected);
+ }
+
+ var tests = [
+ // value has to be defined (indeterminate state).
+ [ null, null, -1.0 ],
+ [ null, 1.0, -1.0 ],
+ // value has to be defined to a valid float (indeterminate state).
+ [ 'foo', 1.0, -1.0 ],
+ // If value < 0.0, 0.0 is used instead.
+ [ -1.0, 1.0, 0.0 ],
+ // If value >= max, max is used instead.
+ [ 2.0, 1.0, 1.0 ],
+ // If max isn't present, max is set to 1.0.
+ [ 1.0, null, 1.0 ],
+ // If max isn't a valid float, max is set to 1.0.
+ [ 1.0, 'foo', 1.0 ],
+ // If max isn't > 0, max is set to 1.0.
+ [ 1.0, -1.0, 1.0 ],
+ // A few simple and valid values.
+ [ 0.0, 1.0, 0.0 ],
+ [ 0.1, 1.0, 0.1/1.0 ],
+ [ 0.1, 2.0, 0.1/2.0 ],
+ [ 10, 50, 10/50 ],
+ // Values implying .position is a double.
+ [ 1.0, 3.0, 1.0/3.0 ],
+ [ 0.1, 0.7, 0.1/0.7 ],
+ ];
+
+ var element = document.createElement('progress');
+
+ for (var test of tests) {
+ checkPositionValue(element, test[0], test[1], test[2], test[3]);
+ }
+}
+
+function checkIndeterminatePseudoClass()
+{
+ function checkIndeterminate(aElement, aValue, aMax, aIndeterminate) {
+ if (aValue != null) {
+ aElement.setAttribute('value', aValue);
+ } else {
+ aElement.removeAttribute('value');
+ }
+
+ if (aMax != null) {
+ aElement.setAttribute('max', aMax);
+ } else {
+ aElement.removeAttribute('max');
+ }
+
+ is(aElement.matches("progress:indeterminate"), aIndeterminate,
+ "<progress> indeterminate state should be " + aIndeterminate);
+ }
+
+ var tests = [
+ // Indeterminate state: (value is undefined, or not a float)
+ // value has to be defined (indeterminate state).
+ [ null, null, true ],
+ [ null, 1.0, true ],
+ [ 'foo', 1.0, true ],
+ // Determined state:
+ [ -1.0, 1.0, false ],
+ [ 2.0, 1.0, false ],
+ [ 1.0, null, false ],
+ [ 1.0, 'foo', false ],
+ [ 1.0, -1.0, false ],
+ [ 0.0, 1.0, false ],
+ ];
+
+ var element = document.createElement('progress');
+
+ for (var test of tests) {
+ checkIndeterminate(element, test[0], test[1], test[2]);
+ }
+}
+
+function checkFormListedElement(aElement)
+{
+ is(document.forms[0].elements.length, 0, "the form should have no element");
+}
+
+function checkLabelable(aElement)
+{
+ var content = document.getElementById('content');
+ var label = document.createElement('label');
+
+ content.appendChild(label);
+ label.appendChild(aElement);
+ is(label.control, aElement, "progress should be labelable");
+
+ // Cleaning-up.
+ content.removeChild(label);
+ content.appendChild(aElement);
+}
+
+function checkNotResetableAndFormSubmission(aElement)
+{
+ // Creating an input element to check the submission worked.
+ var form = document.forms[0];
+ var input = document.createElement('input');
+
+ input.name = 'a';
+ input.value = 'tulip';
+ form.appendChild(input);
+
+ // Setting values.
+ aElement.value = 42.0;
+ aElement.max = 100.0;
+
+ document.getElementsByName('submit_frame')[0].addEventListener("load", function() {
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/forms/foo?a=tulip`,
+ "The progress element value should not be submitted");
+
+ checkNotResetable();
+ }, {once: true});
+
+ form.submit();
+}
+
+function checkNotResetable()
+{
+ // Try to reset the form.
+ var form = document.forms[0];
+ var element = document.getElementById('p');
+
+ element.value = 3.0;
+ element.max = 42.0;
+
+ form.reset();
+
+ SimpleTest.executeSoon(function() {
+ is(element.value, 3.0, "progress.value should not have changed");
+ is(element.max, 42.0, "progress.max should not have changed");
+
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+var p = document.getElementById('p');
+
+ok(p instanceof HTMLProgressElement,
+ "The progress element should be instance of HTMLProgressElement");
+is(p.constructor, HTMLProgressElement,
+ "The progress element constructor should be HTMLProgressElement");
+
+checkFormIDLAttribute(p);
+
+checkValueAttribute();
+
+checkMaxAttribute();
+
+checkPositionAttribute();
+
+checkIndeterminatePseudoClass();
+
+checkFormListedElement(p);
+
+checkLabelable(p);
+
+checkNotResetableAndFormSubmission(p);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_radio_in_label.html b/dom/html/test/forms/test_radio_in_label.html
new file mode 100644
index 0000000000..7e8a232cc3
--- /dev/null
+++ b/dom/html/test/forms/test_radio_in_label.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=229925
+-->
+<head>
+ <title>Test for Bug 229925</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=229925">Mozilla Bug 229925</a>
+<p id="display"></p>
+<form>
+ <label>
+ <span id="s1">LABEL</span>
+ <input type="radio" name="rdo" value="1" id="r1" onmousedown="document.body.appendChild(document.createTextNode('down'));">
+ <input type="radio" name="rdo" value="2" id="r2" checked="checked">
+ </label>
+</form>
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 229925 **/
+SimpleTest.waitForExplicitFinish();
+var r1 = document.getElementById("r1");
+var r2 = document.getElementById("r2");
+var s1 = document.getElementById("s1");
+startTest();
+function startTest() {
+ r1.click();
+ ok(r1.checked,
+ "The first radio input element should be checked by clicking the element");
+ r2.click();
+ ok(r2.checked,
+ "The second radio input element should be checked by clicking the element");
+ s1.click();
+ ok(r1.checked,
+ "The first radio input element should be checked by clicking other element");
+
+ r1.focus();
+ synthesizeKey("KEY_ArrowLeft");
+ ok(r2.checked,
+ "The second radio input element should be checked by key");
+ synthesizeKey("KEY_ArrowLeft");
+ ok(r1.checked,
+ "The first radio input element should be checked by key");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_radio_radionodelist.html b/dom/html/test/forms/test_radio_radionodelist.html
new file mode 100644
index 0000000000..8761c22b58
--- /dev/null
+++ b/dom/html/test/forms/test_radio_radionodelist.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=779723
+-->
+<head>
+ <title>Test for Bug 779723</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=779723">Mozilla Bug 779723</a>
+<p id="display"></p>
+<form>
+ <input type="checkbox" name="rdo" value="0" id="r0" checked="checked">
+ <input type="radio" name="rdo" id="r1">
+ <input type="radio" name="rdo" id="r2" value="2">
+</form>
+<script class="testbody" type="text/javascript">
+/** Test for Bug 779723 **/
+
+var rdoList = document.forms[0].elements.namedItem('rdo');
+is(rdoList.value, "", "The value attribute should be empty");
+
+document.getElementById('r2').checked = true;
+is(rdoList.value, "2", "The value attribute should be 2");
+
+document.getElementById('r1').checked = true;
+is(rdoList.value, "on", "The value attribute should be on");
+
+document.getElementById('r1').value = 1;
+is(rdoList.value, "1", "The value attribute should be 1");
+
+is(rdoList.value, document.getElementById('r1').value,
+ "The value attribute should be equal to the first checked radio input element's value");
+ok(!document.getElementById('r2').checked,
+ "The second radio input element should not be checked");
+
+rdoList.value = '2';
+is(rdoList.value, document.getElementById('r2').value,
+ "The value attribute should be equal to the second radio input element's value");
+ok(document.getElementById('r2').checked,
+ "The second radio input element should be checked");
+
+rdoList.value = '3';
+is(rdoList.value, document.getElementById('r2').value,
+ "The value attribute should be the second radio input element's value");
+ok(document.getElementById('r2').checked,
+ "The second radio input element should be checked");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_reportValidation_preventDefault.html b/dom/html/test/forms/test_reportValidation_preventDefault.html
new file mode 100644
index 0000000000..3f3b99d140
--- /dev/null
+++ b/dom/html/test/forms/test_reportValidation_preventDefault.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1088761
+-->
+<head>
+ <title>Test for Bug 1088761</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input, textarea, fieldset, button, select, output, object { background-color: rgb(0,0,0) !important; }
+ :valid { background-color: rgb(0,255,0) !important; }
+ :invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1088761">Mozilla Bug 1088761</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <fieldset id='f' oninvalid="invalidEventHandler(event, true);"></fieldset>
+ <input id='i' required oninvalid="invalidEventHandler(event, true);">
+ <button id='b' oninvalid="invalidEventHandler(event, true);"></button>
+ <select id='s' required oninvalid="invalidEventHandler(event, true);"></select>
+ <textarea id='t' required oninvalid="invalidEventHandler(event, true);"></textarea>
+ <output id='o' oninvalid="invalidEventHandler(event, true);"></output>
+ <object id='obj' oninvalid="invalidEventHandler(event, true);"></object>
+</div>
+<div id="content2" style="display: none">
+ <fieldset id='f2' oninvalid="invalidEventHandler(event, false);"></fieldset>
+ <input id='i2' required oninvalid="invalidEventHandler(event, false);">
+ <button id='b2' oninvalid="invalidEventHandler(event, false);"></button>
+ <select id='s2' required oninvalid="invalidEventHandler(event, false);"></select>
+ <textarea id='t2' required oninvalid="invalidEventHandler(event, false);"></textarea>
+ <output id='o2' oninvalid="invalidEventHandler(event, false);"></output>
+ <object id='obj2' oninvalid="invalidEventHandler(event, false);"></object>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1088761 **/
+
+var gInvalid = false;
+
+function invalidEventHandler(aEvent, isPreventDefault)
+{
+ if (isPreventDefault) {
+ aEvent.preventDefault();
+ }
+
+ is(aEvent.type, "invalid", "Invalid event type should be invalid");
+ ok(!aEvent.bubbles, "Invalid event should not bubble");
+ ok(aEvent.cancelable, "Invalid event should be cancelable");
+ gInvalid = true;
+}
+
+function checkReportValidityForInvalid(element)
+{
+ gInvalid = false;
+ ok(!element.reportValidity(), "reportValidity() should return false when the element is not valid");
+ ok(gInvalid, "Invalid event should have been handled");
+}
+
+function checkReportValidityForValid(element)
+{
+ gInvalid = false;
+ ok(element.reportValidity(), "reportValidity() should return true when the element is valid");
+ ok(!gInvalid, "Invalid event shouldn't have been handled");
+}
+
+checkReportValidityForInvalid(document.getElementById('i'));
+checkReportValidityForInvalid(document.getElementById('s'));
+checkReportValidityForInvalid(document.getElementById('t'));
+
+checkReportValidityForInvalid(document.getElementById('i2'));
+checkReportValidityForInvalid(document.getElementById('s2'));
+checkReportValidityForInvalid(document.getElementById('t2'));
+
+checkReportValidityForValid(document.getElementById('o'));
+checkReportValidityForValid(document.getElementById('obj'));
+checkReportValidityForValid(document.getElementById('f'));
+
+checkReportValidityForValid(document.getElementById('o2'));
+checkReportValidityForValid(document.getElementById('obj2'));
+checkReportValidityForValid(document.getElementById('f2'));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_required_attribute.html b/dom/html/test/forms/test_required_attribute.html
new file mode 100644
index 0000000000..a95a5cc339
--- /dev/null
+++ b/dom/html/test/forms/test_required_attribute.html
@@ -0,0 +1,416 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345822
+-->
+<head>
+ <title>Test for Bug 345822</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345822">Mozilla Bug 345822</a>
+<p id="display"></p>
+<div id="content">
+ <form>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345822 **/
+
+function checkNotSufferingFromBeingMissing(element, doNotApply)
+{
+ ok(!element.validity.valueMissing,
+ "Element should not suffer from value missing");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "Element should be valid");
+ is(element.validationMessage, "",
+ "Validation message should be the empty string");
+
+ if (doNotApply) {
+ ok(!element.matches(':valid'), ":valid should not apply");
+ ok(!element.matches(':invalid'), ":invalid should not apply");
+ } else {
+ ok(element.matches(':valid'), ":valid should apply");
+ ok(!element.matches(':invalid'), ":invalid should not apply");
+ }
+}
+
+function checkSufferingFromBeingMissing(element)
+{
+ ok(element.validity.valueMissing, "Element should suffer from value missing");
+ ok(!element.validity.valid, "Element should not be valid");
+ ok(!element.checkValidity(), "Element should not be valid");
+
+ if (element.type == 'checkbox')
+ {
+ is(element.validationMessage,
+ "Please check this box if you want to proceed.",
+ "Validation message is wrong");
+ }
+ else if (element.type == 'radio')
+ {
+ is(element.validationMessage,
+ "Please select one of these options.",
+ "Validation message is wrong");
+ }
+ else if (element.type == 'file')
+ {
+ is(element.validationMessage,
+ "Please select a file.",
+ "Validation message is wrong");
+ }
+ else if (element.type == 'number')
+ {
+ is(element.validationMessage,
+ "Please enter a number.",
+ "Validation message is wrong");
+ }
+ else // text fields
+ {
+ is(element.validationMessage,
+ "Please fill out this field.",
+ "Validation message is wrong");
+ }
+
+ ok(!element.matches(':valid'), ":valid should apply");
+ ok(element.matches(':invalid'), ":invalid should not apply");
+}
+
+function checkTextareaRequiredValidity()
+{
+ var element = document.createElement('textarea');
+ document.forms[0].appendChild(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ element.readOnly = true;
+ checkNotSufferingFromBeingMissing(element, true);
+
+ element.readOnly = false;
+ checkSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = 'foo';
+ checkNotSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = '';
+ checkSufferingFromBeingMissing(element);
+
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.focus();
+ element.required = true;
+ SpecialPowers.wrap(element).value = 'foobar';
+ element.blur();
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.form.reportValidity();
+ checkSufferingFromBeingMissing(element);
+
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ // TODO: for the moment, a textarea outside of a document is mutable.
+ SpecialPowers.wrap(element).value = ''; // To make -moz-ui-valid apply.
+ element.required = false;
+ document.forms[0].removeChild(element);
+ checkNotSufferingFromBeingMissing(element);
+}
+
+function checkInputRequiredNotApply(type, isBarred)
+{
+ var element = document.createElement('input');
+ element.type = type;
+ document.forms[0].appendChild(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element, isBarred);
+
+ element.required = true;
+ checkNotSufferingFromBeingMissing(element, isBarred);
+
+ element.required = false;
+
+ document.forms[0].removeChild(element);
+ checkNotSufferingFromBeingMissing(element, isBarred);
+}
+
+function checkInputRequiredValidity(type)
+{
+ var element = document.createElement('input');
+ element.type = type;
+ document.forms[0].appendChild(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ element.readOnly = true;
+ checkNotSufferingFromBeingMissing(element, true);
+
+ element.readOnly = false;
+ checkSufferingFromBeingMissing(element);
+
+ if (element.type == 'email') {
+ SpecialPowers.wrap(element).value = 'foo@bar.com';
+ } else if (element.type == 'url') {
+ SpecialPowers.wrap(element).value = 'http://mozilla.org/';
+ } else if (element.type == 'number') {
+ SpecialPowers.wrap(element).value = '42';
+ } else if (element.type == 'date') {
+ SpecialPowers.wrap(element).value = '2010-10-10';
+ } else if (element.type == 'time') {
+ SpecialPowers.wrap(element).value = '21:21';
+ // TODO: Bug 1864327. This test is wrong, and needs fixing properly.
+ // eslint-disable-next-line no-cond-assign
+ } else if (element.type = 'month') {
+ SpecialPowers.wrap(element).value = '2010-10';
+ } else {
+ SpecialPowers.wrap(element).value = 'foo';
+ }
+ checkNotSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = '';
+ checkSufferingFromBeingMissing(element);
+
+ element.focus();
+ element.required = true;
+ SpecialPowers.wrap(element).value = 'foobar';
+ element.blur();
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = '';
+ element.form.reportValidity();
+ checkSufferingFromBeingMissing(element);
+
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ SpecialPowers.wrap(element).value = ''; // To make :-moz-ui-valid apply.
+ checkSufferingFromBeingMissing(element);
+ document.forms[0].removeChild(element);
+ // Removing the child changes nothing about whether it's valid
+ checkSufferingFromBeingMissing(element);
+}
+
+function checkInputRequiredValidityForCheckbox()
+{
+ var element = document.createElement('input');
+ element.type = 'checkbox';
+ document.forms[0].appendChild(element);
+
+ element.checked = false;
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ element.checked = true;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.checked = false;
+ checkSufferingFromBeingMissing(element);
+
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.focus();
+ element.required = true;
+ element.checked = true;
+ element.blur();
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ element.checked = false;
+ element.form.reportValidity();
+ checkSufferingFromBeingMissing(element);
+
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ element.checked = false;
+ document.forms[0].removeChild(element);
+ checkSufferingFromBeingMissing(element);
+}
+
+function checkInputRequiredValidityForRadio()
+{
+ var element = document.createElement('input');
+ element.type = 'radio';
+ element.name = 'test'
+ document.forms[0].appendChild(element);
+
+ element.checked = false;
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ element.checked = true;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.checked = false;
+ checkSufferingFromBeingMissing(element);
+
+ // A required radio button should not suffer from value missing if another
+ // radio button from the same group is checked.
+ var element2 = document.createElement('input');
+ element2.type = 'radio';
+ element2.name = 'test';
+
+ element2.checked = true;
+ element2.required = false;
+ document.forms[0].appendChild(element2);
+
+ // Adding a checked radio should make required radio in the group not
+ // suffering from being missing.
+ checkNotSufferingFromBeingMissing(element);
+
+ element.checked = false;
+ element2.checked = false;
+ checkSufferingFromBeingMissing(element);
+
+ // The other radio button should not be disabled.
+ // A disabled checked radio button in the radio group
+ // is enough to not suffer from value missing.
+ element2.checked = true;
+ element2.disabled = true;
+ checkNotSufferingFromBeingMissing(element);
+
+ // If a radio button is not required but another radio button is required in
+ // the same group, the not required radio button should suffer from value
+ // missing.
+ element2.disabled = false;
+ element2.checked = false;
+ element.required = false;
+ element2.required = true;
+ checkSufferingFromBeingMissing(element);
+ checkSufferingFromBeingMissing(element2);
+
+ element.checked = true;
+ checkNotSufferingFromBeingMissing(element2);
+
+ // The checked radio is not in the group anymore, element2 should be invalid.
+ element.form.removeChild(element);
+ checkNotSufferingFromBeingMissing(element);
+ checkSufferingFromBeingMissing(element2);
+
+ element2.focus();
+ element2.required = true;
+ element2.checked = true;
+ element2.blur();
+ element2.form.reset();
+ checkSufferingFromBeingMissing(element2);
+
+ element2.required = true;
+ element2.checked = false;
+ element2.form.reportValidity();
+ checkSufferingFromBeingMissing(element2);
+
+ element2.form.reset();
+ checkSufferingFromBeingMissing(element2);
+
+ element2.required = true;
+ element2.checked = false;
+ document.forms[0].removeChild(element2);
+ checkSufferingFromBeingMissing(element2);
+}
+
+function checkInputRequiredValidityForFile()
+{
+ var element = document.createElement('input');
+ element.type = 'file'
+ document.forms[0].appendChild(element);
+
+ var file = new File([""], "345822_file");
+
+ SpecialPowers.wrap(element).value = "";
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).mozSetFileArray([file]);
+ checkNotSufferingFromBeingMissing(element);
+
+ SpecialPowers.wrap(element).value = "";
+ checkSufferingFromBeingMissing(element);
+
+ element.required = false;
+ checkNotSufferingFromBeingMissing(element);
+
+ element.focus();
+ SpecialPowers.wrap(element).mozSetFileArray([file]);
+ element.required = true;
+ element.blur();
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ SpecialPowers.wrap(element).value = '';
+ element.form.reportValidity();
+ checkSufferingFromBeingMissing(element);
+
+ element.form.reset();
+ checkSufferingFromBeingMissing(element);
+
+ element.required = true;
+ SpecialPowers.wrap(element).value = '';
+ document.forms[0].removeChild(element);
+ checkSufferingFromBeingMissing(element);
+}
+
+checkTextareaRequiredValidity();
+
+// The require attribute behavior depend of the input type.
+// First of all, checks for types that make the element barred from
+// constraint validation.
+var typeBarredFromConstraintValidation = ["hidden", "button", "reset"];
+for (type of typeBarredFromConstraintValidation) {
+ checkInputRequiredNotApply(type, true);
+}
+
+// Then, checks for the types which do not use the required attribute.
+var typeRequireNotApply = ['range', 'color', 'submit', 'image'];
+for (type of typeRequireNotApply) {
+ checkInputRequiredNotApply(type, false);
+}
+
+// Now, checking for all types which accept the required attribute.
+var typeRequireApply = ["text", "password", "search", "tel", "email", "url",
+ "number", "date", "time", "month", "week",
+ "datetime-local"];
+
+for (type of typeRequireApply) {
+ checkInputRequiredValidity(type);
+}
+
+checkInputRequiredValidityForCheckbox();
+checkInputRequiredValidityForRadio();
+checkInputRequiredValidityForFile();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_restore_form_elements.html b/dom/html/test/forms/test_restore_form_elements.html
new file mode 100644
index 0000000000..be22a29b7b
--- /dev/null
+++ b/dom/html/test/forms/test_restore_form_elements.html
@@ -0,0 +1,174 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=737851
+-->
+<head>
+ <meta charset="utf-8">
+
+ <title>Test for Bug 737851</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=737851">Mozilla Bug 737851</a>
+
+<p id="display"></p>
+
+
+<div id="content">
+
+ <iframe id="frame" width="800px" height="600px" srcdoc='
+ <html>
+ <body style="display:none;">
+
+ <h3>Checking persistence of inputs through js inserts and moves</h3>
+ <div id="test">
+ <input id="a"/>
+ <input id="b"/>
+ <form id="form1">
+ <input id="c"/>
+ <input id="d"/>
+ </form>
+ <form id="form2">
+ <input id="radio1" type="radio" name="radio"/>
+ <input type="radio" name="radio"/>
+ <input type="radio" name="radio"/>
+ <input type="radio" name="radio"/>
+ </form>
+ <input id="e"/>
+ </div>
+
+ <h3>Bug 728798: checking persistence of inputs when forward-using @form</h3>
+ <div>
+ <input id="728798-a" form="728798-form" name="a"/>
+ <form id="728798-form">
+ <input id="728798-b" form="728798-form" name="b"/>
+ <input id="728798-c" name="c"/>
+ </form>
+ <input id="728798-d" form="728798-form" name="d"/>
+ </div>
+
+ </body>
+ </html>
+ '></iframe>
+
+</div>
+
+
+<pre id="test">
+<script type="text/javascript">
+
+var frameElem = document.getElementById("frame");
+var frame = frameElem.contentWindow;
+
+
+/* -- Main test run -- */
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ shuffle();
+ fill();
+ frameElem.addEventListener("load", function() {
+ shuffle();
+ checkAllFields();
+ SimpleTest.finish();
+ });
+ frame.location.reload();
+})
+
+
+/* -- Input fields js changes and moves -- */
+
+function shuffle() {
+ var framedoc = frame.document;
+
+ // Insert a button (toplevel)
+ var btn = framedoc.createElement("button");
+ var testdiv = framedoc.getElementById("test");
+ testdiv.insertBefore(btn, framedoc.getElementById("b"));
+
+ // Insert a dynamically generated input (in a form)
+ var newInput = framedoc.createElement("input");
+ newInput.setAttribute("id","c0");
+ var form1 = framedoc.getElementById("form1");
+ form1.insertBefore(newInput, form1.firstChild);
+
+ // Move an input around
+ var inputD = framedoc.getElementById("d");
+ var form2 = framedoc.getElementById("form2");
+ form2.insertBefore(inputD, form2.firstChild)
+
+ // Clone an existing input
+ var inputE2 = framedoc.getElementById("e").cloneNode(true);
+ inputE2.setAttribute("id","e2");
+ testdiv.appendChild(inputE2);
+}
+
+
+/* -- Input fields fill & check -- */
+
+/* Values entered in the input fields (by id) */
+
+var fieldValues = {
+ 'a':'simple input',
+ 'b':'moved by inserting a button before (no form)',
+ 'c0':'dynamically generated input',
+ 'c':'moved by inserting an input before (in a form)',
+ 'd':'moved from a form to another',
+ 'e':'the original',
+ 'e2':'the clone',
+ '728798-a':'before the form',
+ '728798-b':'from within the form',
+ '728798-c':'no form attribute in the form',
+ '728798-d':'after the form'
+}
+
+/* Fields for which the input is changed, and corresponding value
+ (clone and creation, same behaviour as webkit) */
+
+var changedFields = {
+ // dynamically generated input field not preserved
+ 'c0':'',
+ // cloned input field is restored with the value of the original
+ 'e2':fieldValues.e
+}
+
+/* Simulate user input by entering the values */
+
+function fill() {
+ for (id in fieldValues) {
+ frame.document.getElementById(id).value = fieldValues[id];
+ }
+ // an input is inserted before the radios (that may move the selected one by 1)
+ frame.document.getElementById('radio1').checked = true;
+}
+
+/* Check that all the fields are as they have been entered */
+
+function checkAllFields() {
+
+ for (id in fieldValues) {
+ var fieldValue = frame.document.getElementById(id).value;
+ if (changedFields[id] === undefined) {
+ is(fieldValue, fieldValues[id],
+ "Field "+id+" should be restored after reload");
+ } else {
+ is(fieldValue, changedFields[id],
+ "Field "+id+" normally gets a different value after reload");
+ }
+ }
+
+ ok(frame.document.getElementById('radio1').checked,
+ "Radio button radio1 should be restored after reload")
+
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_save_restore_custom_elements.html b/dom/html/test/forms/test_save_restore_custom_elements.html
new file mode 100644
index 0000000000..489ad0ca2f
--- /dev/null
+++ b/dom/html/test/forms/test_save_restore_custom_elements.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1556358
+-->
+
+<head>
+ <title>Test for Bug 1556358</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1556358">Mozilla Bug 1556358</a>
+ <p id="display"></p>
+ <div id="content">
+ <iframe src="save_restore_custom_elements_sample.html"></iframe>
+ </div>
+ <script type="application/javascript">
+ /** Test for Bug 1556358 **/
+
+ function formDataWith(...entries) {
+ const formData = new FormData();
+ for (let [key, value] of entries) {
+ formData.append(key, value);
+ }
+ return formData;
+ }
+
+ const states = [
+ "test state",
+ new File(["state"], "state.txt"),
+ formDataWith(["1", "state"], ["2", new Blob(["state_blob"])]),
+ null,
+ undefined,
+ ];
+ const values = [
+ "test value",
+ new File(["value"], "value.txt"),
+ formDataWith(["1", "value"], ["2", new Blob(["value_blob"])]),
+ "null state",
+ "both value and state",
+ ];
+
+ add_task(async () => {
+ const frame = document.querySelector("iframe");
+ const elementTags = ["c-e", "upgraded-ce"];
+
+ // Set the custom element values.
+ for (const tags of elementTags) {
+ [...frame.contentDocument.querySelectorAll(tags)]
+ .forEach((e, i) => {
+ e.set(states[i], values[i]);
+ });
+ }
+
+ await new Promise(resolve => {
+ frame.addEventListener("load", resolve);
+ frame.contentWindow.location.reload();
+ });
+
+ for (const tag of elementTags) {
+ // Retrieve the restored values.
+ const ceStates =
+ [...frame.contentDocument.querySelectorAll(tag)].map((e) => e.state);
+ is(ceStates.length, 5, "Should have 5 custom element states");
+
+ const [restored, original] = [ceStates, states];
+ is(restored[0], original[0], "Value should be restored");
+
+ const file = restored[1];
+ isnot(file, original[1], "Restored file object differs from original object.");
+ is(file.name, original[1].name, "File name should be restored");
+ is(await file.text(), await original[1].text(), "File text should be restored");
+
+ const formData = restored[2];
+ isnot(formData, original[2], "Restored formdata object differs from original object.");
+ is(formData.get("1"), original[2].get("1"), "Form data string should be restored");
+ is(await formData.get("2").text(), await original[2].get("2").text(), "Form data blob should be restored");
+
+ isnot(restored[3], original[3], "Null values don't get restored");
+ is(restored[3], undefined, "Null values don't get restored");
+
+ is(restored[4], "both value and state", "Undefined state should be set to value");
+ }
+ });
+ </script>
+</body>
+
+</html>
diff --git a/dom/html/test/forms/test_save_restore_radio_groups.html b/dom/html/test/forms/test_save_restore_radio_groups.html
new file mode 100644
index 0000000000..c5ef924a0e
--- /dev/null
+++ b/dom/html/test/forms/test_save_restore_radio_groups.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=350022
+-->
+<head>
+ <title>Test for Bug 350022</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=350022">Mozilla Bug 350022</a>
+<p id="display"></p>
+<div id="content"><!-- style="display: none">-->
+ <iframe src="save_restore_radio_groups.sjs"></iframe>
+ <iframe src="save_restore_radio_groups.sjs"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 350022 **/
+
+function checkRadioGroup(aFrame, aResults)
+{
+ var radios = frames[aFrame].document.getElementsByTagName('input');
+
+ is(radios.length, aResults.length,
+ "Radio group should have " + aResults.length + "elements");
+
+ for (var i=0; i<aResults.length; ++i) {
+ is(radios[i].checked, aResults[i],
+ "Radio checked state should be " + aResults[i]);
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ /**
+ * We have two iframes each containing one radio button group.
+ * We are going to change the selected radio button in one group.
+ * Then, both iframes will be reloaded and the new groups will have another
+ * radio checked by default.
+ * For the first group (which had a selection change), nothing should change.
+ * For the second, the selected radio button should change.
+ */
+ checkRadioGroup(0, [true, false, false]);
+ checkRadioGroup(1, [true, false, false]);
+
+ frames[0].document.getElementsByTagName('input')[2].checked = true;
+ checkRadioGroup(0, [false, false, true]);
+
+ framesElts = document.getElementsByTagName('iframe');
+ framesElts[0].addEventListener("load", function() {
+ checkRadioGroup(0, [false, false, true]);
+
+ framesElts[1].addEventListener("load", function() {
+ checkRadioGroup(1, [false, true, false]);
+ SimpleTest.finish();
+ }, {once: true});
+
+ frames[1].location.reload();
+ }, {once: true});
+
+ frames[0].location.reload();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_select_change_event.html b/dom/html/test/forms/test_select_change_event.html
new file mode 100644
index 0000000000..ec3ed58c5e
--- /dev/null
+++ b/dom/html/test/forms/test_select_change_event.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265968
+-->
+<head>
+ <title>Test for Bug 1265968</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265968">Mozilla Bug 1265968</a>
+<p id="display"></p>
+<div id="content">
+ <select id="select" onchange="++selectChange;">
+ <option>one</option>
+ <option>two</option>
+ <option>three</option>
+ <option>four</option>
+ <option>five</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+ var select = document.getElementById("select");
+ var selectChange = 0;
+ var expectedChange = 0;
+
+ select.focus();
+ for (var i = 1; i < select.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+ is(select.options[i].selected, true, "Option should be selected");
+ is(selectChange, ++expectedChange, "Down key should fire change event.");
+ }
+
+ // We are at the end of the list, going down should not fire change event.
+ synthesizeKey("KEY_ArrowDown");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ for (var i = select.length - 2; i >= 0; i--) {
+ synthesizeKey("KEY_ArrowUp");
+ is(select.options[i].selected, true, "Option should be selected");
+ is(selectChange, ++expectedChange, "Up key should fire change event.");
+ }
+
+ // We are at the top of the list, going up should not fire change event.
+ synthesizeKey("KEY_ArrowUp");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_select_input_change_event.html b/dom/html/test/forms/test_select_input_change_event.html
new file mode 100644
index 0000000000..fcf384e423
--- /dev/null
+++ b/dom/html/test/forms/test_select_input_change_event.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265968
+-->
+<head>
+ <title>Test for Bug 1024350</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1024350">Mozilla Bug 1024350</a>
+<p id="display"></p>
+<div id="content">
+ <select oninput='++selectInput;' onchange="++selectChange;">
+ <option>one</option>
+ </select>
+ <select oninput='++selectInput;' onchange="++selectChange;">
+ <option>one</option>
+ <option>two</option>
+ </select>
+ <select multiple size='1' oninput='++selectInput;' onchange="++selectChange;">
+ <option>one</option>
+ </select>
+ <select multiple oninput='++selectInput;' onchange="++selectChange;">
+ <option>one</option>
+ <option>two</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+ var selectSingleOneItem = document.getElementsByTagName('select')[0];
+ var selectSingle = document.getElementsByTagName('select')[1];
+ var selectMultipleOneItem = document.getElementsByTagName('select')[2];
+ var selectMultiple = document.getElementsByTagName('select')[3];
+
+ var selectChange = 0;
+ var selectInput = 0;
+ var expectedChange = 0;
+ var expectedInput = 0;
+
+ selectSingleOneItem.focus();
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list.");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ synthesizeKey("KEY_ArrowUp");
+ is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list.");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+ selectSingle.focus();
+ for (var i = 1; i < selectSingle.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+
+ is(selectSingle.options[i].selected, true, "Option should be selected");
+ is(selectInput, ++expectedInput, "Down key should fire input event.");
+ is(selectChange, ++expectedChange, "Down key should fire change event.");
+ }
+
+ // We are at the end of the list, going down should not fire change event.
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list.");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ for (var i = selectSingle.length - 2; i >= 0; i--) {
+ synthesizeKey("KEY_ArrowUp");
+
+ is(selectSingle.options[i].selected, true, "Option should be selected");
+ is(selectInput, ++expectedInput, "Up key should fire input event.");
+ is(selectChange, ++expectedChange, "Up key should fire change event.");
+ }
+
+ // We are at the top of the list, going up should not fire change event.
+ synthesizeKey("KEY_ArrowUp");
+ is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list.");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+ selectMultipleOneItem.focus();
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, ++expectedInput, "Down key should fire input event when reaching end of the list.");
+ is(selectChange, ++expectedChange, "Down key should fire change event when reaching end of the list.");
+
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list.");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ synthesizeKey("KEY_ArrowUp");
+ is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list.");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+ selectMultiple.focus();
+ for (var i = 0; i < selectMultiple.length; i++) {
+ synthesizeKey("KEY_ArrowDown");
+
+ is(selectMultiple.options[i].selected, true, "Option should be selected");
+ is(selectInput, ++expectedInput, "Down key should fire input event.");
+ is(selectChange, ++expectedChange, "Down key should fire change event.");
+ }
+
+ // We are at the end of the list, going down should not fire change event.
+ synthesizeKey("KEY_ArrowDown");
+ is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list.");
+ is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list.");
+
+ for (var i = selectMultiple.length - 2; i >= 0; i--) {
+ synthesizeKey("KEY_ArrowUp");
+
+ is(selectMultiple.options[i].selected, true, "Option should be selected");
+ is(selectInput, ++expectedInput, "Up key should fire input event.");
+ is(selectChange, ++expectedChange, "Up key should fire change event.");
+ }
+
+ // We are at the top of the list, going up should not fire change event.
+ synthesizeKey("KEY_ArrowUp");
+ is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list.");
+ is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list.");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_select_selectedOptions.html b/dom/html/test/forms/test_select_selectedOptions.html
new file mode 100644
index 0000000000..745e0ba4f3
--- /dev/null
+++ b/dom/html/test/forms/test_select_selectedOptions.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596681
+-->
+<head>
+ <title>Test for HTMLSelectElement.selectedOptions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596681">Mozilla Bug 596681</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLSelectElement's selectedOptions attribute.
+ *
+ * selectedOptions is a live list of the options that have selectedness of true
+ * (not the selected content attribute).
+ *
+ * See http://www.whatwg.org/html/#dom-select-selectedoptions
+ **/
+
+function checkSelectedOptions(size, elements)
+{
+ is(selectedOptions.length, size,
+ "select should have " + size + " selected options");
+ for (let i = 0; i < size; ++i) {
+ ok(selectedOptions[i], "selected option is valid");
+ if (selectedOptions[i]) {
+ is(selectedOptions[i].value, elements[i].value, "selected options are correct");
+ }
+ }
+}
+
+let select = document.createElement("select");
+document.body.appendChild(select);
+let selectedOptions = select.selectedOptions;
+
+ok("selectedOptions" in select,
+ "select element should have a selectedOptions IDL attribute");
+
+ok(select.selectedOptions instanceof HTMLCollection,
+ "selectedOptions should be an HTMLCollection instance");
+
+let option1 = document.createElement("option");
+let option2 = document.createElement("option");
+let option3 = document.createElement("option");
+option1.id = "option1";
+option1.value = "option1";
+option2.value = "option2";
+option3.value = "option3";
+
+checkSelectedOptions(0, null);
+
+select.add(option1, null);
+is(selectedOptions.namedItem("option1").value, "option1", "named getter works");
+checkSelectedOptions(1, [option1]);
+
+select.add(option2, null);
+checkSelectedOptions(1, [option1]);
+
+select.options[1].selected = true;
+checkSelectedOptions(1, [option2]);
+
+select.multiple = true;
+checkSelectedOptions(1, [option2]);
+
+select.options[0].selected = true;
+checkSelectedOptions(2, [option1, option2]);
+
+option1.selected = false;
+// Usinig selected directly on the option should work.
+checkSelectedOptions(1, [option2]);
+
+select.remove(1);
+select.add(option2, 0);
+select.options[0].selected = true;
+select.options[1].selected = true;
+// Should be in tree order.
+checkSelectedOptions(2, [option2, option1]);
+
+select.add(option3, null);
+checkSelectedOptions(2, [option2, option1]);
+
+select.options[2].selected = true;
+checkSelectedOptions(3, [option2, option1, option3]);
+
+select.length = 0;
+option1.selected = false;
+option2.selected = false;
+option3.selected = false;
+var optgroup1 = document.createElement("optgroup");
+optgroup1.appendChild(option1);
+optgroup1.appendChild(option2);
+select.add(optgroup1)
+var optgroup2 = document.createElement("optgroup");
+optgroup2.appendChild(option3);
+select.add(optgroup2);
+
+checkSelectedOptions(0, null);
+
+option2.selected = true;
+checkSelectedOptions(1, [option2]);
+
+option3.selected = true;
+checkSelectedOptions(2, [option2, option3]);
+
+optgroup1.removeChild(option2);
+checkSelectedOptions(1, [option3]);
+
+document.body.removeChild(select);
+option1.selected = true;
+checkSelectedOptions(2, [option1, option3]);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_select_validation.html b/dom/html/test/forms/test_select_validation.html
new file mode 100644
index 0000000000..6d02aa0746
--- /dev/null
+++ b/dom/html/test/forms/test_select_validation.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=942321
+-->
+<head>
+ <title>Test for Bug 942321</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=942321">Mozilla Bug 942321</a>
+<p id="display"></p>
+<form id="form" href="">
+ <select required id="testselect">
+ <option id="placeholder" value="" selected>placeholder</option>
+ <option value="test" id="actualvalue">test</option>
+ <select>
+ <input type="submit" />
+</form>
+<script class="testbody" type="text/javascript">
+/** Test for Bug 942321 **/
+var option = document.getElementById("actualvalue");
+option.selected = true;
+is(form.checkValidity(), true, "Select is required and should be valid");
+
+var placeholder = document.getElementById("placeholder");
+placeholder.selected = true;
+is(form.checkValidity(), false, "Select is required and should be invalid");
+
+placeholder.value = "not-invalid-anymore";
+is(form.checkValidity(), true, "Select is required and should be valid when option's value is changed by javascript");
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/forms/test_set_range_text.html b/dom/html/test/forms/test_set_range_text.html
new file mode 100644
index 0000000000..f85014ae77
--- /dev/null
+++ b/dom/html/test/forms/test_set_range_text.html
@@ -0,0 +1,242 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=850364
+-->
+<head>
+<title>Tests for Bug 850364 && Bug 918940</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=850364">Mozilla Bug 850364</a>
+<p id="display"></p>
+<div id="content">
+
+<!-- "SetRangeText() supported types"-->
+<input type="text" id="input_text"></input>
+<input type="search" id="input_search"></input>
+<input type="url" id="input_url"></input>
+<input type="tel" id="input_tel"></input>
+<input type="password" id="input_password"></input>
+<textarea id="input_textarea"></textarea>
+
+<!-- "SetRangeText() non-supported types" -->
+<input type="button" id="input_button"></input>
+<input type="submit" id="input_submit"></input>
+<input type="image" id="input_image"></input>
+<input type="reset" id="input_reset"></input>
+<input type="radio" id="input_radio"></input>
+<input type="checkbox" id="input_checkbox"></input>
+<input type="range" id="input_range"></input>
+<input type="file" id="input_file"></input>
+<input type="email" id="input_email"></input>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ /** Tests for Bug 850364 && Bug 918940**/
+
+ var SupportedTypes = ["text", "search", "url", "tel", "password", "textarea"];
+ var NonSupportedTypes = ["button", "submit", "image", "reset", "radio",
+ "checkbox", "range", "file", "email"];
+
+ SimpleTest.waitForExplicitFinish();
+
+ function TestInputs() {
+
+ var opThrows, elem, i, msg;
+
+ //Non-supported types should throw
+ for (i = 0; i < NonSupportedTypes.length; ++i) {
+ opThrows = false;
+ msg = "input_" + NonSupportedTypes[i];
+ elem = document.getElementById(msg);
+ elem.focus();
+ try {
+ elem.setRangeText("abc");
+ } catch (ex) {
+ opThrows = true;
+ }
+ ok(opThrows, msg + " should throw InvalidStateError");
+ }
+
+ var numOfSelectCalls = 0, expectedNumOfSelectCalls = 0;
+ //Supported types should not throw
+ for (i = 0; i < SupportedTypes.length; ++i) {
+ opThrows = false;
+ msg = "input_" + SupportedTypes[i];
+ elem = document.getElementById(msg);
+ elem.focus();
+ try {
+ elem.setRangeText("abc");
+ } catch (ex) {
+ opThrows = true;
+ }
+ is(opThrows, false, msg + " should not throw InvalidStateError");
+
+ elem.addEventListener("select", function (aEvent) {
+ ok(true, "select event should be fired for " + aEvent.target.id);
+ if (++numOfSelectCalls == expectedNumOfSelectCalls) {
+ SimpleTest.finish();
+ } else if (numOfSelectCalls > expectedNumOfSelectCalls) {
+ ok(false, "Too many select events were fired");
+ }
+ });
+
+ elem.addEventListener("input", function (aEvent) {
+ ok(false, "input event should NOT be fired for " + + aEvent.target.id);
+ });
+
+ var test = " setRange(replacement), shrink";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(1, 6);
+ elem.setRangeText("xyz");
+ is(elem.value, "0xyz6789ABCDEF", msg + test);
+ is(elem.selectionStart, 1, msg + test);
+ is(elem.selectionEnd, 4, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "0mnk6789ABCDEF", msg + test);
+ expectedNumOfSelectCalls += 2;
+
+ test = " setRange(replacement), expand";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(1, 2);
+ elem.setRangeText("xyz");
+ is(elem.value, "0xyz23456789ABCDEF", msg + test);
+ is(elem.selectionStart, 1, msg + test);
+ is(elem.selectionEnd, 4, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "0mnk23456789ABCDEF", msg + test);
+ expectedNumOfSelectCalls += 2;
+
+ test = " setRange(replacement) pure insertion at start";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(0, 0);
+ elem.setRangeText("xyz");
+ is(elem.value, "xyz0123456789ABCDEF", msg + test);
+ is(elem.selectionStart, 0, msg + test);
+ is(elem.selectionEnd, 0, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "mnkxyz0123456789ABCDEF", msg + test);
+ expectedNumOfSelectCalls += 1;
+
+ test = " setRange(replacement) pure insertion in the middle";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(4, 4);
+ elem.setRangeText("xyz");
+ is(elem.value, "0123xyz456789ABCDEF", msg + test);
+ is(elem.selectionStart, 4, msg + test);
+ is(elem.selectionEnd, 4, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "0123mnkxyz456789ABCDEF", msg + test);
+ expectedNumOfSelectCalls += 1;
+
+ test = " setRange(replacement) pure insertion at the end";
+ elem.value = "0123456789ABCDEF";
+ elem.setSelectionRange(16, 16);
+ elem.setRangeText("xyz");
+ is(elem.value, "0123456789ABCDEFxyz", msg + test);
+ is(elem.selectionStart, 16, msg + test);
+ is(elem.selectionEnd, 16, msg + test);
+ elem.setRangeText("mnk");
+ is(elem.value, "0123456789ABCDEFmnkxyz", msg + test);
+
+ //test SetRange(replacement, start, end, mode) with start > end
+ try {
+ elem.setRangeText("abc", 20, 4);
+ } catch (ex) {
+ opThrows = (ex.name == "IndexSizeError" && ex.code == DOMException.INDEX_SIZE_ERR);
+ }
+ is(opThrows, true, msg + " should throw IndexSizeError");
+
+ //test SelectionMode 'select'
+ elem.value = "0123456789ABCDEF";
+ elem.setRangeText("xyz", 4, 9, "select");
+ is(elem.value, "0123xyz9ABCDEF", msg + ".value == \"0123xyz9ABCDEF\"");
+ is(elem.selectionStart, 4, msg + ".selectionStart == 4, with \"select\"");
+ is(elem.selectionEnd, 7, msg + ".selectionEnd == 7, with \"select\"");
+ expectedNumOfSelectCalls += 1;
+
+ elem.setRangeText("pqm", 6, 25, "select");
+ is(elem.value, "0123xypqm", msg + ".value == \"0123xypqm\"");
+ is(elem.selectionStart, 6, msg + ".selectionStart == 6, with \"select\"");
+ is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"select\"");
+ expectedNumOfSelectCalls += 1;
+
+ //test SelectionMode 'start'
+ elem.value = "0123456789ABCDEF";
+ elem.setRangeText("xyz", 4, 9, "start");
+ is(elem.value, "0123xyz9ABCDEF", msg + ".value == \"0123xyz9ABCDEF\"");
+ is(elem.selectionStart, 4, msg + ".selectionStart == 4, with \"start\"");
+ is(elem.selectionEnd, 4, msg + ".selectionEnd == 4, with \"start\"");
+ expectedNumOfSelectCalls += 1;
+
+ elem.setRangeText("pqm", 6, 25, "start");
+ is(elem.value, "0123xypqm", msg + ".value == \"0123xypqm\"");
+ is(elem.selectionStart, 6, msg + ".selectionStart == 6, with \"start\"");
+ is(elem.selectionEnd, 6, msg + ".selectionEnd == 6, with \"start\"");
+ expectedNumOfSelectCalls += 1;
+
+ //test SelectionMode 'end'
+ elem.value = "0123456789ABCDEF";
+ elem.setRangeText("xyz", 4, 9, "end");
+ is(elem.value, "0123xyz9ABCDEF", msg + ".value == \"0123xyz9ABCDEF\"");
+ is(elem.selectionStart, 7, msg + ".selectionStart == 7, with \"end\"");
+ is(elem.selectionEnd, 7, msg + ".selectionEnd == 7, with \"end\"");
+ expectedNumOfSelectCalls += 1;
+
+ elem.setRangeText("pqm", 6, 25, "end");
+ is(elem.value, "0123xypqm", msg + ".value == \"0123xypqm\"");
+ is(elem.selectionStart, 9, msg + ".selectionStart == 9, with \"end\"");
+ is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"end\"");
+ expectedNumOfSelectCalls += 1;
+
+ //test SelectionMode 'preserve' (default)
+
+ //subcase: selection{Start|End} > end
+ elem.value = "0123456789";
+ elem.setSelectionRange(6, 9);
+ elem.setRangeText("Z", 1, 2, "preserve");
+ is(elem.value, "0Z23456789", msg + ".value == \"0Z23456789\"");
+ is(elem.selectionStart, 6, msg + ".selectionStart == 6, with \"preserve\"");
+ is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"preserve\"");
+ expectedNumOfSelectCalls += 1;
+
+ //subcase: selection{Start|End} < end
+ elem.value = "0123456789";
+ elem.setSelectionRange(4, 5);
+ elem.setRangeText("QRST", 2, 9, "preserve");
+ is(elem.value, "01QRST9", msg + ".value == \"01QRST9\"");
+ is(elem.selectionStart, 2, msg + ".selectionStart == 2, with \"preserve\"");
+ is(elem.selectionEnd, 6, msg + ".selectionEnd == 6, with \"preserve\"");
+ expectedNumOfSelectCalls += 2;
+
+ //subcase: selectionStart > end, selectionEnd < end
+ elem.value = "0123456789";
+ elem.setSelectionRange(8, 4);
+ elem.setRangeText("QRST", 1, 5);
+ is(elem.value, "0QRST56789", msg + ".value == \"0QRST56789\"");
+ is(elem.selectionStart, 1, msg + ".selectionStart == 1, with \"default\"");
+ is(elem.selectionEnd, 5, msg + ".selectionEnd == 5, with \"default\"");
+ expectedNumOfSelectCalls += 2;
+
+ //subcase: selectionStart < end, selectionEnd > end
+ elem.value = "0123456789";
+ elem.setSelectionRange(4, 9);
+ elem.setRangeText("QRST", 2, 6);
+ is(elem.value, "01QRST6789", msg + ".value == \"01QRST6789\"");
+ is(elem.selectionStart, 2, msg + ".selectionStart == 2, with \"default\"");
+ is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"default\"");
+ expectedNumOfSelectCalls += 2;
+ }
+ }
+
+ addLoadEvent(TestInputs);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_step_attribute.html b/dom/html/test/forms/test_step_attribute.html
new file mode 100644
index 0000000000..f0af250c06
--- /dev/null
+++ b/dom/html/test/forms/test_step_attribute.html
@@ -0,0 +1,1060 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=635553
+-->
+<head>
+ <title>Test for Bug 635553</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635499">Mozilla Bug 635499</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 635553 **/
+
+var data = [
+ { type: 'hidden', apply: false },
+ { type: 'text', apply: false },
+ { type: 'search', apply: false },
+ { type: 'tel', apply: false },
+ { type: 'url', apply: false },
+ { type: 'email', apply: false },
+ { type: 'password', apply: false },
+ { type: 'date', apply: true },
+ { type: 'month', apply: true },
+ { type: 'week', apply: true },
+ { type: 'time', apply: true },
+ { type: 'datetime-local', apply: true },
+ { type: 'number', apply: true },
+ { type: 'range', apply: true },
+ { type: 'color', apply: false },
+ { type: 'checkbox', apply: false },
+ { type: 'radio', apply: false },
+ { type: 'file', apply: false },
+ { type: 'submit', apply: false },
+ { type: 'image', apply: false },
+ { type: 'reset', apply: false },
+ { type: 'button', apply: false },
+];
+
+function getFreshElement(type) {
+ var elmt = document.createElement('input');
+ elmt.type = type;
+ return elmt;
+}
+
+function checkValidity(aElement, aValidity, aApply, aData)
+{
+ aValidity = aApply ? aValidity : true;
+
+ is(aElement.validity.valid, aValidity,
+ "element validity should be " + aValidity);
+ is(aElement.validity.stepMismatch, !aValidity,
+ "element step mismatch status should be " + !aValidity);
+
+ if (aValidity) {
+ is(aElement.validationMessage, "", "There should be no validation message.");
+ } else {
+ if (aElement.validity.rangeUnderflow) {
+ var underflowMsg =
+ (aElement.type == "date" || aElement.type == "time") ?
+ ("Please select a value that is no earlier than " + aElement.min + ".") :
+ ("Please select a value that is no less than " + aElement.min + ".");
+ is(aElement.validationMessage, underflowMsg,
+ "Checking range underflow validation message.");
+ } else if (aData.low == aData.high) {
+ is(aElement.validationMessage, "Please select a valid value. " +
+ "The nearest valid value is " + aData.low + ".",
+ "There should be a validation message.");
+ } else {
+ is(aElement.validationMessage, "Please select a valid value. " +
+ "The two nearest valid values are " + aData.low + " and " + aData.high + ".",
+ "There should be a validation message.");
+ }
+ }
+
+ is(aElement.matches(":valid"), aElement.willValidate && aValidity,
+ (aElement.willValidate && aValidity) ? ":valid should apply" : "valid shouldn't apply");
+ is(aElement.matches(":invalid"), aElement.willValidate && !aValidity,
+ (aElement.wil && aValidity) ? ":invalid shouldn't apply" : "valid should apply");
+}
+
+for (var test of data) {
+ var input = getFreshElement(test.type);
+ var apply = test.apply;
+
+ if (test.todo) {
+ todo_is(input.type, test.type, test.type + " isn't implemented yet");
+ continue;
+ }
+
+ // The element should be valid, there should be no step mismatch.
+ checkValidity(input, true, apply);
+
+ // Checks to do for all types that support step:
+ // - check for @step=0,
+ // - check for @step behind removed,
+ // - check for @step being 'any' with different case variations.
+ switch (input.type) {
+ case 'text':
+ case 'hidden':
+ case 'search':
+ case 'password':
+ case 'tel':
+ case 'radio':
+ case 'checkbox':
+ case 'reset':
+ case 'button':
+ case 'submit':
+ case 'image':
+ case 'color':
+ input.value = '0';
+ checkValidity(input, true, apply);
+ break;
+ case 'url':
+ input.value = 'http://mozilla.org';
+ checkValidity(input, true, apply);
+ break;
+ case 'email':
+ input.value = 'foo@bar.com';
+ checkValidity(input, true, apply);
+ break;
+ case 'file':
+ var file = new File([''], '635499_file');
+
+ SpecialPowers.wrap(input).mozSetFileArray([file]);
+ checkValidity(input, true, apply);
+
+ break;
+ case 'date':
+ // For date, the step is calulated on the timestamp since 1970-01-01
+ // which mean that for all dates prior to the epoch, this timestamp is < 0
+ // and the behavior might differ, therefore we have to test for these cases.
+
+ // When step is invalid, every date is valid
+ input.step = 0;
+ input.value = '2012-07-05';
+ checkValidity(input, true, apply);
+
+ input.step = 'foo';
+ input.value = '1970-01-01';
+ checkValidity(input, true, apply);
+
+ input.step = '-1';
+ input.value = '1969-12-12';
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step');
+ input.value = '1500-01-01';
+ checkValidity(input, true, apply);
+
+ input.step = 'any';
+ input.value = '1966-12-12';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '2013-02-03';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid date, there is a step base.
+ input.min = '2008-02-28';
+ input.step = '2';
+ input.value = '2008-03-01';
+ checkValidity(input, true, apply);
+
+ input.value = '2008-02-29';
+ checkValidity(input, false, apply, { low: "2008-02-28", high: "2008-03-01" });
+
+ input.min = '2008-02-27';
+ input.value = '2008-02-28';
+ checkValidity(input, false, apply, { low: "2008-02-27", high: "2008-02-29" });
+
+ input.min = '2009-02-27';
+ input.value = '2009-02-28';
+ checkValidity(input, false, apply, { low: "2009-02-27", high: "2009-03-01" });
+
+ input.min = '2009-02-01';
+ input.step = '1.1';
+ input.value = '2009-02-02';
+ checkValidity(input, true, apply);
+
+ // Without any step attribute the date is valid
+ input.removeAttribute('step');
+ checkValidity(input, true, apply);
+
+ input.min = '1950-01-01';
+ input.step = '366';
+ input.value = '1951-01-01';
+ checkValidity(input, false, apply, { low: "1950-01-01", high: "1951-01-02" });
+
+ input.min = '1951-01-01';
+ input.step = '365';
+ input.value = '1952-01-01';
+ checkValidity(input, true, apply);
+
+ input.step = '0.9';
+ input.value = '1951-01-02';
+ is(input.step, '0.9', "check that step value is unchanged");
+ checkValidity(input, true, apply);
+
+ input.step = '0.4';
+ input.value = '1951-01-02';
+ is(input.step, '0.4', "check that step value is unchanged");
+ checkValidity(input, true, apply);
+
+ input.step = '1.5';
+ input.value = '1951-01-02';
+ is(input.step, '1.5', "check that step value is unchanged");
+ checkValidity(input, false, apply, { low: "1951-01-01", high: "1951-01-03" });
+
+ input.value = '1951-01-08';
+ checkValidity(input, false, apply, { low: "1951-01-07", high: "1951-01-09" });
+
+ input.step = '3000';
+ input.min= '1968-01-01';
+ input.value = '1968-05-12';
+ checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" });
+
+ input.value = '1971-01-01';
+ checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" });
+
+ input.value = '1991-01-01';
+ checkValidity(input, false, apply, { low: "1984-06-05", high: "1992-08-22" });
+
+ input.value = '1984-06-05';
+ checkValidity(input, true, apply);
+
+ input.value = '1992-08-22';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1991-01-01';
+ input.value = '1991-01-01';
+ checkValidity(input, true, apply);
+
+ input.value = '1991-01-02';
+ checkValidity(input, false, apply, { low: "1991-01-01", high: "1991-01-03" });
+
+ input.value = '1991-01-03';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1969-12-20';
+ input.value = '1969-12-20';
+ checkValidity(input, true, apply);
+
+ input.value = '1969-12-21';
+ checkValidity(input, false, apply, { low: "1969-12-20", high: "1969-12-22" });
+
+ input.value = '1969-12-22';
+ checkValidity(input, true, apply);
+
+ break;
+ case 'number':
+ // When step=0, the allowed step is 1.
+ input.step = '0';
+ input.value = '1.2';
+ checkValidity(input, false, apply, { low: 1, high: 2 });
+
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ input.value = '0';
+ checkValidity(input, true, apply);
+
+ // When step is NaN, the allowed step value is 1.
+ input.step = 'foo';
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ input.value = '1.5';
+ checkValidity(input, false, apply, { low: 1, high: 2 });
+
+ // When step is negative, the allowed step value is 1.
+ input.step = '-0.1';
+ checkValidity(input, false, apply, { low: 1, high: 2 });
+
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ // When step is missing, the allowed step value is 1.
+ input.removeAttribute('step');
+ input.value = '1.5';
+ checkValidity(input, false, apply, { low: 1, high: 2 });
+
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ // When step is 'any', all values are fine wrt to step.
+ input.step = 'any';
+ checkValidity(input, true, apply);
+
+ input.step = 'aNy';
+ input.value = '1337';
+ checkValidity(input, true, apply);
+
+ input.step = 'AnY';
+ input.value = '0.1';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '-13.37';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid float, there is a step base.
+ input.min = '1';
+ input.step = '2';
+ input.value = '3';
+ checkValidity(input, true, apply);
+
+ input.value = '2';
+ checkValidity(input, false, apply, { low: 1, high: 3 });
+
+ input.removeAttribute('step'); // step = 1
+ input.min = '0.5';
+ input.value = '5.5';
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ checkValidity(input, false, apply, { low: 0.5, high: 1.5 });
+
+ input.min = '-0.1';
+ input.step = '1';
+ input.value = '0.9';
+ checkValidity(input, true, apply);
+
+ input.value = '0.1';
+ checkValidity(input, false, apply, { low: -0.1, high: 0.9 });
+
+ // When min is set to NaN, there is no step base (step base=0 actually).
+ input.min = 'foo';
+ input.step = '1';
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ input.value = '0.5';
+ checkValidity(input, false, apply, { low: 0, high: 1 });
+
+ input.min = '';
+ input.value = '1';
+ checkValidity(input, true, apply);
+
+ input.value = '0.5';
+ checkValidity(input, false, apply, { low: 0, high: 1 });
+
+ input.removeAttribute('min');
+
+ // If value isn't a number, the element isn't invalid.
+ input.value = '';
+ checkValidity(input, true, apply);
+
+ // Regular situations.
+ input.step = '2';
+ input.value = '1.5';
+ checkValidity(input, false, apply, { low: 0, high: 2 });
+
+ input.value = '42.0';
+ checkValidity(input, true, apply);
+
+ input.step = '0.1';
+ input.value = '-0.1';
+ checkValidity(input, true, apply);
+
+ input.step = '2';
+ input.removeAttribute('min');
+ input.max = '10';
+ input.value = '-9';
+ checkValidity(input, false, apply, {low: -10, high: -8});
+
+ // If there is a value defined but no min, the step base is the value.
+ input = getFreshElement(test.type);
+ input.setAttribute('value', '1');
+ input.step = 2;
+ checkValidity(input, true, apply);
+
+ input.value = 3;
+ checkValidity(input, true, apply);
+
+ input.value = 2;
+ checkValidity(input, false, apply, {low: 1, high: 3});
+
+ // Should also work with defaultValue.
+ input = getFreshElement(test.type);
+ input.defaultValue = 1;
+ input.step = 2;
+ checkValidity(input, true, apply);
+
+ input.value = 3;
+ checkValidity(input, true, apply);
+
+ input.value = 2;
+ checkValidity(input, false, apply, {low: 1, high: 3});
+
+ // Rounding issues.
+ input = getFreshElement(test.type);
+ input.min = 0.1;
+ input.step = 0.2;
+ input.value = 0.3;
+ checkValidity(input, true, apply);
+
+ // Check that when the higher value is higher than max, we don't show it.
+ input = getFreshElement(test.type);
+ input.step = '2';
+ input.min = '1';
+ input.max = '10.9';
+ input.value = '10';
+
+ is(input.validationMessage, "Please select a valid value. " +
+ "The nearest valid value is 9.",
+ "The validation message should not include the higher value.");
+ break;
+ case 'range':
+ // Range is special in that it clamps to valid values, so it is much
+ // rarer for it to be invalid.
+
+ // When step=0, the allowed value step is 1.
+ input.step = '0';
+ input.value = '1.2';
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '0';
+ is(input.value, '0', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ // When step is NaN, the allowed step value is 1.
+ input.step = 'foo';
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '1.5';
+ is(input.value, '2', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ // When step is negative, the allowed step value is 1.
+ input.step = '-0.1';
+ is(input.value, '2', "check that the value still coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ // When step is missing, the allowed step value is 1.
+ input.removeAttribute('step');
+ input.value = '1.5';
+ is(input.value, '2', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ // When step is 'any', all values are fine wrt to step.
+ input.step = 'any';
+ checkValidity(input, true, apply);
+
+ input.step = 'aNy';
+ input.value = '97';
+ is(input.value, '97', "check that the value for step=aNy is unchanged");
+ checkValidity(input, true, apply);
+
+ input.step = 'AnY';
+ input.value = '0.1';
+ is(input.value, '0.1', "check that a positive fractional value with step=AnY is unchanged");
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.min = -100;
+ input.value = '-13.37';
+ is(input.value, '-13.37', "check that a negative fractional value with step=ANY is unchanged");
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid float, there is a step base.
+ input.min = '1'; // the step base
+ input.step = '2';
+ input.value = '3';
+ is(input.value, '3', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '2';
+ is(input.value, '3', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = '1.99';
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step'); // step = 1
+ input.min = '0.5'; // step base
+ input.value = '5.5';
+ is(input.value, '5.5', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '1';
+ is(input.value, '1.5', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.min = '-0.1'; // step base
+ input.step = '1';
+ input.value = '0.9';
+ is(input.value, '0.9', "the value should be a valid step");
+ checkValidity(input, true, apply);
+
+ input.value = '0.1';
+ is(input.value, '-0.1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ // When min is set to NaN, the step base is the value.
+ input.min = 'foo';
+ input.step = '1';
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '0.5';
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.min = '';
+ input.value = '1';
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = '0.5';
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('min');
+
+ // Test when the value isn't a number
+ input.value = '';
+ is(input.value, '50', "value be should default to the value midway between the minimum (0) and the maximum (100)");
+ checkValidity(input, true, apply);
+
+ // Regular situations.
+ input.step = '2';
+ input.value = '1.5';
+ is(input.value, '2', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = '42.0';
+ is(input.value, '42.0', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.step = '0.1';
+ input.value = '-0.1';
+ is(input.value, '0', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.step = '2';
+ input.removeAttribute('min');
+ input.max = '10';
+ input.value = '-9';
+ is(input.value, '0', "check the value is clamped to the minimum's default of zero");
+ checkValidity(input, true, apply);
+
+ // If @value is defined but not @min, the step base is @value.
+ input = getFreshElement(test.type);
+ input.setAttribute('value', '1');
+ input.step = 2;
+ is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ input.value = 3;
+ is(input.value, '3', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = 2;
+ is(input.value, '3', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ // Should also work with defaultValue.
+ input = getFreshElement(test.type);
+ input.defaultValue = 1;
+ input.step = 2;
+ is(input.value, '1', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = 3;
+ is(input.value, '3', "check that the value coincides with a step");
+ checkValidity(input, true, apply);
+
+ input.value = 2;
+ is(input.value, '3', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close");
+ checkValidity(input, true, apply);
+
+ // Check contrived error case where there are no valid steps in range:
+ // No @min, so the step base is the default minimum, zero, the valid
+ // range is 0-1, -1 gets clamped to zero.
+ input = getFreshElement(test.type);
+ input.step = '3';
+ input.max = '1';
+ input.defaultValue = '-1';
+ is(input.value, '0', "the value should have been clamped to the default minimum, zero");
+ checkValidity(input, false, apply, {low: -1, high: -1});
+
+ // Check that when the closest of the two steps that the value is between
+ // is greater than the maximum we sanitize to the lower step.
+ input = getFreshElement(test.type);
+ input.step = '2';
+ input.min = '1';
+ input.max = '10.9';
+ input.value = '10.8'; // closest step in 11, but 11 > maximum
+ is(input.value, '9', "check that the value coincides with a step");
+
+ // The way that step base is defined, the converse (the value not being
+ // on a step, and the nearest step being a value that would be underflow)
+ // is not possible, so nothing to test there.
+
+ is(input.validationMessage, "",
+ "The validation message should be empty.");
+ break;
+ case 'time':
+ // Tests invalid step values. That defaults to step = 1 minute (60).
+ var values = [ '0', '-1', 'foo', 'any', 'ANY', 'aNy' ];
+ for (var value of values) {
+ input.step = value;
+ input.value = '19:06:00';
+ checkValidity(input, true, apply);
+ input.value = '19:06:51';
+ if (value.toLowerCase() != 'any') {
+ checkValidity(input, false, apply, {low: '19:06', high: '19:07'});
+ } else {
+ checkValidity(input, true, apply);
+ }
+ }
+
+ // No step means that we use the default step value.
+ input.removeAttribute('step');
+ input.value = '19:06:00';
+ checkValidity(input, true, apply);
+ input.value = '19:06:51';
+ checkValidity(input, false, apply, {low: '19:06', high: '19:07'});
+
+ var tests = [
+ // With step=1, we allow values by the second.
+ { step: '1', value: '19:11:01', min: '00:00', result: true },
+ { step: '1', value: '19:11:01.001', min: '00:00', result: false,
+ low: '19:11:01', high: '19:11:02' },
+ { step: '1', value: '19:11:01.1', min: '00:00', result: false,
+ low: '19:11:01', high: '19:11:02' },
+ // When step >= 86400000, only the minimum value is valid.
+ // This is actually @value if there is no @min.
+ { step: '86400000', value: '00:00', result: true },
+ { step: '86400000', value: '00:01', result: true },
+ { step: '86400000', value: '00:00', min: '00:01', result: false },
+ { step: '86400000', value: '00:01', min: '00:00', result: false,
+ low: '00:00', high: '00:00' },
+ // When step < 1, it should just work.
+ { step: '0.1', value: '15:05:05.1', min: '00:00', result: true },
+ { step: '0.1', value: '15:05:05.101', min: '00:00', result: false,
+ low: '15:05:05.100', high: '15:05:05.200' },
+ { step: '0.2', value: '15:05:05.2', min: '00:00', result: true },
+ { step: '0.2', value: '15:05:05.1', min: '00:00', result: false,
+ low: '15:05:05', high: '15:05:05.200' },
+ { step: '0.01', value: '15:05:05.01', min: '00:00', result: true },
+ { step: '0.01', value: '15:05:05.011', min: '00:00', result: false,
+ low: '15:05:05.010', high: '15:05:05.020' },
+ { step: '0.02', value: '15:05:05.02', min: '00:00', result: true },
+ { step: '0.02', value: '15:05:05.01', min: '00:00', result: false,
+ low: '15:05:05', high: '15:05:05.020' },
+ { step: '0.002', value: '15:05:05.002', min: '00:00', result: true },
+ { step: '0.002', value: '15:05:05.001', min: '00:00', result: false,
+ low: '15:05:05', high: '15:05:05.002' },
+ // When step<=0.001, any value is allowed.
+ { step: '0.001', value: '15:05:05.001', min: '00:00', result: true },
+ { step: '0.001', value: '15:05:05', min: '00:00', result: true },
+ { step: '0.000001', value: '15:05:05', min: '00:00', result: true },
+ // This value has conversion to double issues.
+ { step: '0.0000001', value: '15:05:05', min: '00:00', result: true },
+ // Some random values.
+ { step: '100', value: '15:06:40', min: '00:00', result: true },
+ { step: '100', value: '15:05:05.010', min: '00:00', result: false,
+ low: '15:05', high: '15:06:40' },
+ { step: '3600', value: '15:00', min: '00:00', result: true },
+ { step: '3600', value: '15:14', min: '00:00', result: false,
+ low: '15:00', high: '16:00' },
+ { step: '7200', value: '14:00', min: '00:00', result: true },
+ { step: '7200', value: '15:14', min: '00:00', result: false,
+ low: '14:00', high: '16:00' },
+ { step: '7260', value: '14:07', min: '00:00', result: true },
+ { step: '7260', value: '15:14', min: '00:00', result: false,
+ low: '14:07', high: '16:08' },
+ ];
+
+ var type = test.type;
+ for (var test of tests) {
+ var input = getFreshElement(type);
+ input.step = test.step;
+ input.setAttribute('value', test.value);
+ if (test.min !== undefined) {
+ input.min = test.min;
+ }
+
+ if (test.todo) {
+ todo(input.validity.valid, test.result,
+ "This test should fail for the moment because of precission issues");
+ continue;
+ }
+
+ if (test.result) {
+ checkValidity(input, true, apply);
+ } else {
+ checkValidity(input, false, apply,
+ { low: test.low, high: test.high });
+ }
+ }
+
+ break;
+ case 'month':
+ // When step is invalid, every date is valid
+ input.step = 0;
+ input.value = '2016-07';
+ checkValidity(input, true, apply);
+
+ input.step = 'foo';
+ input.value = '1970-01';
+ checkValidity(input, true, apply);
+
+ input.step = '-1';
+ input.value = '1970-01';
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step');
+ input.value = '1500-01';
+ checkValidity(input, true, apply);
+
+ input.step = 'any';
+ input.value = '1966-12';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '2013-02';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid month, there is a step base.
+ input.min = '2000-01';
+ input.step = '2';
+ input.value = '2000-03';
+ checkValidity(input, true, apply);
+
+ input.value = '2000-02';
+ checkValidity(input, false, apply, { low: "2000-01", high: "2000-03" });
+
+ input.min = '2012-12';
+ input.value = '2013-01';
+ checkValidity(input, false, apply, { low: "2012-12", high: "2013-02" });
+
+ input.min = '2010-10';
+ input.value = '2010-11';
+ checkValidity(input, false, apply, { low: "2010-10", high: "2010-12" });
+
+ input.min = '2010-01';
+ input.step = '1.1';
+ input.value = '2010-02';
+ checkValidity(input, true, apply);
+
+ input.min = '2010-05';
+ input.step = '1.9';
+ input.value = '2010-06';
+ checkValidity(input, false, apply, { low: "2010-05", high: "2010-07" });
+
+ // Without any step attribute the date is valid
+ input.removeAttribute('step');
+ checkValidity(input, true, apply);
+
+ input.min = '1950-01';
+ input.step = '13';
+ input.value = '1951-01';
+ checkValidity(input, false, apply, { low: "1950-01", high: "1951-02" });
+
+ input.min = '1951-01';
+ input.step = '12';
+ input.value = '1952-01';
+ checkValidity(input, true, apply);
+
+ input.step = '0.9';
+ input.value = '1951-02';
+ checkValidity(input, true, apply);
+
+ input.step = '1.5';
+ input.value = '1951-04';
+ checkValidity(input, false, apply, { low: "1951-03", high: "1951-05" });
+
+ input.value = '1951-08';
+ checkValidity(input, false, apply, { low: "1951-07", high: "1951-09" });
+
+ input.step = '300';
+ input.min= '1968-01';
+ input.value = '1968-05';
+ checkValidity(input, false, apply, { low: "1968-01", high: "1993-01" });
+
+ input.value = '1971-01';
+ checkValidity(input, false, apply, { low: "1968-01", high: "1993-01" });
+
+ input.value = '1994-01';
+ checkValidity(input, false, apply, { low: "1993-01", high: "2018-01" });
+
+ input.value = '2018-01';
+ checkValidity(input, true, apply);
+
+ input.value = '2043-01';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1991-01';
+ input.value = '1991-01';
+ checkValidity(input, true, apply);
+
+ input.value = '1991-02';
+ checkValidity(input, false, apply, { low: "1991-01", high: "1991-03" });
+
+ input.value = '1991-03';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1969-12';
+ input.value = '1969-12';
+ checkValidity(input, true, apply);
+
+ input.value = '1970-01';
+ checkValidity(input, false, apply, { low: "1969-12", high: "1970-02" });
+
+ input.value = '1970-02';
+ checkValidity(input, true, apply);
+
+ break;
+ case 'week':
+ // When step is invalid, every week is valid
+ input.step = 0;
+ input.value = '2016-W30';
+ checkValidity(input, true, apply);
+
+ input.step = 'foo';
+ input.value = '1970-W01';
+ checkValidity(input, true, apply);
+
+ input.step = '-1';
+ input.value = '1970-W01';
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step');
+ input.value = '1500-W01';
+ checkValidity(input, true, apply);
+
+ input.step = 'any';
+ input.value = '1966-W52';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '2013-W10';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid week, there is a step base.
+ input.min = '2000-W01';
+ input.step = '2';
+ input.value = '2000-W03';
+ checkValidity(input, true, apply);
+
+ input.value = '2000-W02';
+ checkValidity(input, false, apply, { low: "2000-W01", high: "2000-W03" });
+
+ input.min = '2012-W52';
+ input.value = '2013-W01';
+ checkValidity(input, false, apply, { low: "2012-W52", high: "2013-W02" });
+
+ input.min = '2010-W01';
+ input.step = '1.1';
+ input.value = '2010-W02';
+ checkValidity(input, true, apply);
+
+ input.min = '2010-W05';
+ input.step = '1.9';
+ input.value = '2010-W06';
+ checkValidity(input, false, apply, { low: "2010-W05", high: "2010-W07" });
+
+ // Without any step attribute the week is valid
+ input.removeAttribute('step');
+ checkValidity(input, true, apply);
+
+ input.min = '1950-W01';
+ input.step = '53';
+ input.value = '1951-W01';
+ checkValidity(input, false, apply, { low: "1950-W01", high: "1951-W02" });
+
+ input.min = '1951-W01';
+ input.step = '52';
+ input.value = '1952-W01';
+ checkValidity(input, true, apply);
+
+ input.step = '0.9';
+ input.value = '1951-W02';
+ checkValidity(input, true, apply);
+
+ input.step = '1.5';
+ input.value = '1951-W04';
+ checkValidity(input, false, apply, { low: "1951-W03", high: "1951-W05" });
+
+ input.value = '1951-W20';
+ checkValidity(input, false, apply, { low: "1951-W19", high: "1951-W21" });
+
+ input.step = '300';
+ input.min= '1968-W01';
+ input.value = '1968-W05';
+ checkValidity(input, false, apply, { low: "1968-W01", high: "1973-W40" });
+
+ input.value = '1971-W01';
+ checkValidity(input, false, apply, { low: "1968-W01", high: "1973-W40" });
+
+ input.value = '1975-W01';
+ checkValidity(input, false, apply, { low: "1973-W40", high: "1979-W27" });
+
+ input.value = '1985-W14';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1991-W01';
+ input.value = '1991-W01';
+ checkValidity(input, true, apply);
+
+ input.value = '1991-W02';
+ checkValidity(input, false, apply, { low: "1991-W01", high: "1991-W03" });
+
+ input.value = '1991-W03';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1969-W52';
+ input.value = '1969-W52';
+ checkValidity(input, true, apply);
+
+ input.value = '1970-W01';
+ checkValidity(input, false, apply, { low: "1969-W52", high: "1970-W02" });
+
+ input.value = '1970-W02';
+ checkValidity(input, true, apply);
+
+ break;
+ case 'datetime-local':
+ // When step is invalid, every datetime is valid
+ input.step = 0;
+ input.value = '2017-02-06T12:00';
+ checkValidity(input, true, apply);
+
+ input.step = 'foo';
+ input.value = '1970-01-01T00:00';
+ checkValidity(input, true, apply);
+
+ input.step = '-1';
+ input.value = '1969-12-12 00:10';
+ checkValidity(input, true, apply);
+
+ input.removeAttribute('step');
+ input.value = '1500-01-01T12:00';
+ checkValidity(input, true, apply);
+
+ input.step = 'any';
+ input.value = '1966-12-12T12:00';
+ checkValidity(input, true, apply);
+
+ input.step = 'ANY';
+ input.value = '2017-01-01 12:00';
+ checkValidity(input, true, apply);
+
+ // When min is set to a valid datetime, there is a step base.
+ input.min = '2017-01-01T00:00:00';
+ input.step = '2';
+ input.value = '2017-01-01T00:00:02';
+ checkValidity(input, true, apply);
+
+ input.value = '2017-01-01T00:00:03';
+ checkValidity(input, false, apply,
+ { low: "2017-01-01T00:00:02", high: "2017-01-01T00:00:04" });
+
+ input.min = '2017-01-01T00:00:05';
+ input.value = '2017-01-01T00:00:08';
+ checkValidity(input, false, apply,
+ { low: "2017-01-01T00:00:07", high: "2017-01-01T00:00:09" });
+
+ input.min = '2000-01-01T00:00';
+ input.step = '120';
+ input.value = '2000-01-01T00:02';
+ checkValidity(input, true, apply);
+
+ // Without any step attribute the datetime is valid
+ input.removeAttribute('step');
+ checkValidity(input, true, apply);
+
+ input.min = '1950-01-01T00:00';
+ input.step = '129600'; // 1.5 day
+ input.value = '1950-01-02T00:00';
+ checkValidity(input, false, apply,
+ { low: "1950-01-01T00:00", high: "1950-01-02T12:00" });
+
+ input.step = '259200'; // 3 days
+ input.value = '1950-01-04T12:00';
+ checkValidity(input, false, apply,
+ { low: "1950-01-04T00:00", high: "1950-01-07T00:00" });
+
+ input.value = '1950-01-10T00:00';
+ checkValidity(input, true, apply);
+
+ input.step = '0.5'; // half a second
+ input.value = '1950-01-01T00:00:00.123';
+ checkValidity(input, false, apply,
+ { low: "1950-01-01T00:00", high: "1950-01-01T00:00:00.500" });
+
+ input.value = '2000-01-01T12:30:30.600';
+ checkValidity(input, false, apply,
+ { low: "2000-01-01T12:30:30.500", high: "2000-01-01T12:30:31" });
+
+ input.value = '1950-01-05T00:00:00.500';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1991-01-01T12:00';
+ input.value = '1991-01-01T12:00';
+ checkValidity(input, true, apply);
+
+ input.value = '1991-01-01T12:00:03';
+ checkValidity(input, false, apply,
+ { low: "1991-01-01T12:00:02.100", high: "1991-01-01T12:00:04.200" });
+
+ input.value = '1991-01-01T12:00:06.3';
+ checkValidity(input, true, apply);
+
+ input.step = '2.1';
+ input.min = '1969-12-20T10:00:05';
+ input.value = '1969-12-20T10:00:05';
+ checkValidity(input, true, apply);
+
+ input.value = '1969-12-20T10:00:08';
+ checkValidity(input, false, apply,
+ { low: "1969-12-20T10:00:07.100", high: "1969-12-20T10:00:09.200" });
+
+ input.value = '1969-12-20T10:00:09.200';
+ checkValidity(input, true, apply);
+
+ break;
+ default:
+ ok(false, "Implement the tests for <input type='" + test.type + " >");
+ break;
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_stepup_stepdown.html b/dom/html/test/forms/test_stepup_stepdown.html
new file mode 100644
index 0000000000..8ad7fbfeee
--- /dev/null
+++ b/dom/html/test/forms/test_stepup_stepdown.html
@@ -0,0 +1,1137 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=636627
+-->
+<head>
+ <title>Test for Bug 636627</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=636627">Mozilla Bug 636627</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 636627 **/
+
+/**
+ * This test is testing stepDown() and stepUp().
+ */
+
+function checkPresence()
+{
+ var input = document.createElement('input');
+ is('stepDown' in input, true, 'stepDown() should be an input function');
+ is('stepUp' in input, true, 'stepUp() should be an input function');
+}
+
+function checkAvailability()
+{
+ var testData =
+ [
+ ["text", false],
+ ["password", false],
+ ["search", false],
+ ["telephone", false],
+ ["email", false],
+ ["url", false],
+ ["hidden", false],
+ ["checkbox", false],
+ ["radio", false],
+ ["file", false],
+ ["submit", false],
+ ["image", false],
+ ["reset", false],
+ ["button", false],
+ ["number", true],
+ ["range", true],
+ ["date", true],
+ ["time", true],
+ ["month", true],
+ ["week", true],
+ ["datetime-local", true],
+ ["color", false],
+ ];
+
+ var element = document.createElement("input");
+ element.setAttribute('value', '0');
+
+ for (data of testData) {
+ var exceptionCaught = false;
+ element.type = data[0];
+ try {
+ element.stepDown();
+ } catch (e) {
+ exceptionCaught = true;
+ }
+ is(exceptionCaught, !data[1], "stepDown() availability is not correct");
+
+ exceptionCaught = false;
+ try {
+ element.stepUp();
+ } catch (e) {
+ exceptionCaught = true;
+ }
+ is(exceptionCaught, !data[1], "stepUp() availability is not correct");
+ }
+}
+
+function checkStepDown()
+{
+ // This testData is very similar to the one in checkStepUp with some changes
+ // relative to stepDown.
+ var testData = [
+ /* Initial value | step | min | max | stepDown arg | final value | exception */
+ { type: 'number', data: [
+ // Regular case.
+ [ '1', null, null, null, null, '0', false ],
+ // Argument testing.
+ [ '1', null, null, null, 1, '0', false ],
+ [ '9', null, null, null, 9, '0', false ],
+ [ '1', null, null, null, -1, '2', false ],
+ [ '1', null, null, null, 0, '1', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '1', null, null, null, 1.1, '0', false ],
+ // With step values.
+ [ '1', '0.5', null, null, null, '0.5', false ],
+ [ '1', '0.25', null, null, 4, '0', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '1', '0', null, null, null, '0', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '1', '-1', null, null, null, '0', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '1', 'foo', null, null, null, '0', false ],
+ // Min values testing.
+ [ '1', '1', 'foo', null, null, '0', false ],
+ [ '1', null, '-10', null, null, '0', false ],
+ [ '1', null, '0', null, null, '0', false ],
+ [ '1', null, '10', null, null, '1', false ],
+ [ '1', null, '2', null, null, '1', false ],
+ [ '1', null, '1', null, null, '1', false ],
+ // Max values testing.
+ [ '1', '1', null, 'foo', null, '0', false ],
+ [ '1', null, null, '10', null, '0', false ],
+ [ '1', null, null, '0', null, '0', false ],
+ [ '1', null, null, '-10', null, '-10', false ],
+ [ '1', null, null, '1', null, '0', false ],
+ [ '5', null, null, '3', '3', '2', false ],
+ [ '5', '2', '-6', '3', '2', '2', false ],
+ [ '-3', '5', '-10', '-3', null, '-5', false ],
+ // Step mismatch.
+ [ '1', '2', '-2', null, null, '0', false ],
+ [ '3', '2', '-2', null, null, '2', false ],
+ [ '3', '2', '-2', null, '2', '0', false ],
+ [ '3', '2', '-2', null, '-2', '6', false ],
+ [ '1', '2', '-6', null, null, '0', false ],
+ [ '1', '2', '-2', null, null, '0', false ],
+ [ '1', '3', '-6', null, null, '0', false ],
+ [ '2', '3', '-6', null, null, '0', false ],
+ [ '2', '3', '1', null, null, '1', false ],
+ [ '5', '3', '1', null, null, '4', false ],
+ [ '3', '2', '-6', null, null, '2', false ],
+ [ '5', '2', '-6', null, null, '4', false ],
+ [ '6', '2', '1', null, null, '5', false ],
+ [ '8', '3', '1', null, null, '7', false ],
+ [ '9', '2', '-10', null, null, '8', false ],
+ [ '7', '3', '-10', null, null, '5', false ],
+ [ '-2', '3', '-10', null, null, '-4', false ],
+ // Clamping.
+ [ '0', '2', '-1', null, null, '-1', false ],
+ [ '10', '2', '0', '4', '10', '0', false ],
+ [ '10', '2', '0', '4', '5', '0', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '-1', false ],
+ [ '', '2', null, null, null, '-2', false ],
+ [ '', '2', '3', null, null, '3', false ],
+ [ '', null, '3', null, null, '3', false ],
+ [ '', '2', '3', '8', null, '3', false ],
+ [ '', null, '-10', '10', null, '-1', false ],
+ [ '', '3', '-10', '10', null, '-1', false ],
+ // With step = 'any'.
+ [ '0', 'any', null, null, 1, null, true ],
+ [ '0', 'ANY', null, null, 1, null, true ],
+ [ '0', 'AnY', null, null, 1, null, true ],
+ [ '0', 'aNy', null, null, 1, null, true ],
+ // With @value = step base.
+ [ '1', '2', null, null, null, '-1', false ],
+ ]},
+ { type: 'range', data: [
+ // Regular case.
+ [ '1', null, null, null, null, '0', false ],
+ // Argument testing.
+ [ '1', null, null, null, 1, '0', false ],
+ [ '9', null, null, null, 9, '0', false ],
+ [ '1', null, null, null, -1, '2', false ],
+ [ '1', null, null, null, 0, '1', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '1', null, null, null, 1.1, '0', false ],
+ // With step values.
+ [ '1', '0.5', null, null, null, '0.5', false ],
+ [ '1', '0.25', null, null, 4, '0', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '1', '0', null, null, null, '0', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '1', '-1', null, null, null, '0', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '1', 'foo', null, null, null, '0', false ],
+ // Min values testing.
+ [ '1', '1', 'foo', null, null, '0', false ],
+ [ '1', null, '-10', null, null, '0', false ],
+ [ '1', null, '0', null, null, '0', false ],
+ [ '1', null, '10', null, null, '10', false ],
+ [ '1', null, '2', null, null, '2', false ],
+ [ '1', null, '1', null, null, '1', false ],
+ // Max values testing.
+ [ '1', '1', null, 'foo', null, '0', false ],
+ [ '1', null, null, '10', null, '0', false ],
+ [ '1', null, null, '0', null, '0', false ],
+ [ '1', null, null, '-10', null, '0', false ],
+ [ '1', null, null, '1', null, '0', false ],
+ [ '5', null, null, '3', '3', '0', false ],
+ [ '5', '2', '-6', '3', '2', '-2', false ],
+ [ '-3', '5', '-10', '-3', null, '-10', false ],
+ // Step mismatch.
+ [ '1', '2', '-2', null, null, '0', false ],
+ [ '3', '2', '-2', null, null, '2', false ],
+ [ '3', '2', '-2', null, '2', '0', false ],
+ [ '3', '2', '-2', null, '-2', '8', false ],
+ [ '1', '2', '-6', null, null, '0', false ],
+ [ '1', '2', '-2', null, null, '0', false ],
+ [ '1', '3', '-6', null, null, '-3', false ],
+ [ '2', '3', '-6', null, null, '0', false ],
+ [ '2', '3', '1', null, null, '1', false ],
+ [ '5', '3', '1', null, null, '1', false ],
+ [ '3', '2', '-6', null, null, '2', false ],
+ [ '5', '2', '-6', null, null, '4', false ],
+ [ '6', '2', '1', null, null, '5', false ],
+ [ '8', '3', '1', null, null, '4', false ],
+ [ '9', '2', '-10', null, null, '8', false ],
+ [ '7', '3', '-10', null, null, '5', false ],
+ [ '-2', '3', '-10', null, null, '-4', false ],
+ // Clamping.
+ [ '0', '2', '-1', null, null, '-1', false ],
+ [ '10', '2', '0', '4', '10', '0', false ],
+ [ '10', '2', '0', '4', '5', '0', false ],
+ // value = "" (default will be 50).
+ [ '', null, null, null, null, '49', false ],
+ // With step = 'any'.
+ [ '0', 'any', null, null, 1, null, true ],
+ [ '0', 'ANY', null, null, 1, null, true ],
+ [ '0', 'AnY', null, null, 1, null, true ],
+ [ '0', 'aNy', null, null, 1, null, true ],
+ // With @value = step base.
+ [ '1', '2', null, null, null, '1', false ],
+ ]},
+ { type: 'date', data: [
+ // Regular case.
+ [ '2012-07-09', null, null, null, null, '2012-07-08', false ],
+ // Argument testing.
+ [ '2012-07-09', null, null, null, 1, '2012-07-08', false ],
+ [ '2012-07-09', null, null, null, 5, '2012-07-04', false ],
+ [ '2012-07-09', null, null, null, -1, '2012-07-10', false ],
+ [ '2012-07-09', null, null, null, 0, '2012-07-09', false ],
+ // Month/Year wrapping.
+ [ '2012-08-01', null, null, null, 1, '2012-07-31', false ],
+ [ '1969-01-02', null, null, null, 4, '1968-12-29', false ],
+ [ '1969-01-01', null, null, null, -365, '1970-01-01', false ],
+ [ '2012-02-29', null, null, null, -1, '2012-03-01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2012-01-02', null, null, null, 1.1, '2012-01-01', false ],
+ [ '2012-01-02', null, null, null, 1.9, '2012-01-01', false ],
+ // With step values.
+ [ '2012-01-03', '0.5', null, null, null, '2012-01-02', false ],
+ [ '2012-01-02', '0.5', null, null, null, '2012-01-01', false ],
+ [ '2012-01-01', '2', null, null, null, '2011-12-30', false ],
+ [ '2012-01-02', '0.25',null, null, 4, '2011-12-29', false ],
+ [ '2012-01-15', '1.1', '2012-01-01', null, 1, '2012-01-14', false ],
+ [ '2012-01-12', '1.1', '2012-01-01', null, 2, '2012-01-10', false ],
+ [ '2012-01-23', '1.1', '2012-01-01', null, 10, '2012-01-13', false ],
+ [ '2012-01-23', '1.1', '2012-01-01', null, 11, '2012-01-12', false ],
+ [ '1968-01-12', '1.1', '1968-01-01', null, 8, '1968-01-04', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2012-01-02', '0', null, null, null, '2012-01-01', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2012-01-02', '-1', null, null, null, '2012-01-01', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2012-01-02', 'foo', null, null, null, '2012-01-01', false ],
+ // Min values testing.
+ [ '2012-01-03', '1', 'foo', null, 2, '2012-01-01', false ],
+ [ '2012-01-02', '1', '2012-01-01', null, null, '2012-01-01', false ],
+ [ '2012-01-01', '1', '2012-01-01', null, null, '2012-01-01', false ],
+ [ '2012-01-01', '1', '2012-01-10', null, 1, '2012-01-01', false ],
+ [ '2012-01-05', '3', '2012-01-01', null, null, '2012-01-04', false ],
+ [ '1969-01-01', '5', '1969-01-01', '1969-01-02', null, '1969-01-01', false ],
+ // Max values testing.
+ [ '2012-01-02', '1', null, 'foo', null, '2012-01-01', false ],
+ [ '2012-01-02', null, null, '2012-01-05', null, '2012-01-01', false ],
+ [ '2012-01-03', null, null, '2012-01-03', null, '2012-01-02', false ],
+ [ '2012-01-07', null, null, '2012-01-04', 4, '2012-01-03', false ],
+ [ '2012-01-07', '2', null, '2012-01-04', 3, '2012-01-01', false ],
+ // Step mismatch.
+ [ '2012-01-04', '2', '2012-01-01', null, null, '2012-01-03', false ],
+ [ '2012-01-06', '2', '2012-01-01', null, 2, '2012-01-03', false ],
+ [ '2012-01-05', '2', '2012-01-04', '2012-01-08', null, '2012-01-04', false ],
+ [ '1970-01-04', '2', null, null, null, '1970-01-02', false ],
+ [ '1970-01-09', '3', null, null, null, '1970-01-06', false ],
+ // Clamping.
+ [ '2012-05-01', null, null, '2012-01-05', null, '2012-01-05', false ],
+ [ '1970-01-05', '2', '1970-01-02', '1970-01-05', null, '1970-01-04', false ],
+ [ '1970-01-01', '5', '1970-01-02', '1970-01-09', 10, '1970-01-01', false ],
+ [ '1970-01-07', '5', '1969-12-27', '1970-01-06', 2, '1970-01-01', false ],
+ [ '1970-03-08', '3', '1970-02-01', '1970-02-07', 15, '1970-02-01', false ],
+ [ '1970-01-10', '3', '1970-01-01', '1970-01-06', 2, '1970-01-04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1969-12-31', false ],
+ // With step = 'any'.
+ [ '2012-01-01', 'any', null, null, 1, null, true ],
+ [ '2012-01-01', 'ANY', null, null, 1, null, true ],
+ [ '2012-01-01', 'AnY', null, null, 1, null, true ],
+ [ '2012-01-01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'time', data: [
+ // Regular case.
+ [ '16:39', null, null, null, null, '16:38', false ],
+ // Argument testing.
+ [ '16:40', null, null, null, 1, '16:39', false ],
+ [ '16:40', null, null, null, 5, '16:35', false ],
+ [ '16:40', null, null, null, -1, '16:41', false ],
+ [ '16:40', null, null, null, 0, '16:40', false ],
+ // hour/minutes/seconds wrapping.
+ [ '05:00', null, null, null, null, '04:59', false ],
+ [ '05:00:00', 1, null, null, null, '04:59:59', false ],
+ [ '05:00:00', 0.1, null, null, null, '04:59:59.900', false ],
+ [ '05:00:00', 0.01, null, null, null, '04:59:59.990', false ],
+ [ '05:00:00', 0.001, null, null, null, '04:59:59.999', false ],
+ // stepDown() on '00:00' gives '23:59'.
+ [ '00:00', null, null, null, 1, '23:59', false ],
+ [ '00:00', null, null, null, 3, '23:57', false ],
+ // Some random step values..
+ [ '16:56', '0.5', null, null, null, '16:55:59.500', false ],
+ [ '16:56', '2', null, null, null, '16:55:58', false ],
+ [ '16:56', '0.25',null, null, 4, '16:55:59', false ],
+ [ '16:57', '1.1', '16:00', null, 1, '16:56:59.900', false ],
+ [ '16:57', '1.1', '16:00', null, 2, '16:56:58.800', false ],
+ [ '16:57', '1.1', '16:00', null, 10, '16:56:50', false ],
+ [ '16:57', '1.1', '16:00', null, 11, '16:56:48.900', false ],
+ [ '16:57', '1.1', '16:00', null, 8, '16:56:52.200', false ],
+ // Invalid @step, means that we use the default value.
+ [ '17:01', '0', null, null, null, '17:00', false ],
+ [ '17:01', '-1', null, null, null, '17:00', false ],
+ [ '17:01', 'foo', null, null, null, '17:00', false ],
+ // Min values testing.
+ [ '17:02', '60', 'foo', null, 2, '17:00', false ],
+ [ '17:10', '60', '17:09', null, null, '17:09', false ],
+ [ '17:10', '60', '17:10', null, null, '17:10', false ],
+ [ '17:10', '60', '17:30', null, 1, '17:10', false ],
+ [ '17:10', '180', '17:05', null, null, '17:08', false ],
+ [ '17:10', '300', '17:10', '17:11', null, '17:10', false ],
+ // Max values testing.
+ [ '17:15', '60', null, 'foo', null, '17:14', false ],
+ [ '17:15', null, null, '17:20', null, '17:14', false ],
+ [ '17:15', null, null, '17:15', null, '17:14', false ],
+ [ '17:15', null, null, '17:13', 4, '17:11', false ],
+ [ '17:15', '120', null, '17:13', 3, '17:09', false ],
+ // Step mismatch.
+ [ '17:19', '120', '17:10', null, null, '17:18', false ],
+ [ '17:19', '120', '17:10', null, 2, '17:16', false ],
+ [ '17:19', '120', '17:18', '17:25', null, '17:18', false ],
+ [ '17:19', '120', null, null, null, '17:17', false ],
+ [ '17:19', '180', null, null, null, '17:16', false ],
+ // Clamping.
+ [ '17:22', null, null, '17:11', null, '17:11', false ],
+ [ '17:22', '120', '17:20', '17:22', null, '17:20', false ],
+ [ '17:22', '300', '17:12', '17:20', 10, '17:12', false ],
+ [ '17:22', '300', '17:18', '17:20', 2, '17:18', false ],
+ [ '17:22', '180', '17:00', '17:20', 15, '17:00', false ],
+ [ '17:22', '180', '17:10', '17:20', 2, '17:16', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '23:59', false ],
+ // With step = 'any'.
+ [ '17:26', 'any', null, null, 1, null, true ],
+ [ '17:26', 'ANY', null, null, 1, null, true ],
+ [ '17:26', 'AnY', null, null, 1, null, true ],
+ [ '17:26', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'month', data: [
+ // Regular case.
+ [ '2016-08', null, null, null, null, '2016-07', false ],
+ // Argument testing.
+ [ '2016-08', null, null, null, 1, '2016-07', false ],
+ [ '2016-08', null, null, null, 5, '2016-03', false ],
+ [ '2016-08', null, null, null, -1, '2016-09', false ],
+ [ '2016-08', null, null, null, 0, '2016-08', false ],
+ // Month/Year wrapping.
+ [ '2016-01', null, null, null, 1, '2015-12', false ],
+ [ '1969-02', null, null, null, 4, '1968-10', false ],
+ [ '1969-01', null, null, null, -12, '1970-01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2016-08', null, null, null, 1.1, '2016-07', false ],
+ [ '2016-01', null, null, null, 1.9, '2015-12', false ],
+ // With step values.
+ [ '2016-03', '0.5', null, null, null, '2016-02', false ],
+ [ '2016-03', '2', null, null, null, '2016-01', false ],
+ [ '2016-03', '0.25',null, null, 4, '2015-11', false ],
+ [ '2016-12', '1.1', '2016-01', null, 1, '2016-11', false ],
+ [ '2016-12', '1.1', '2016-01', null, 2, '2016-10', false ],
+ [ '2016-12', '1.1', '2016-01', null, 10, '2016-02', false ],
+ [ '2016-12', '1.1', '2016-01', null, 12, '2016-01', false ],
+ [ '1968-12', '1.1', '1968-01', null, 8, '1968-04', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2016-02', '0', null, null, null, '2016-01', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2016-02', '-1', null, null, null, '2016-01', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2016-02', 'foo', null, null, null, '2016-01', false ],
+ // Min values testing.
+ [ '2016-03', '1', 'foo', null, 2, '2016-01', false ],
+ [ '2016-02', '1', '2016-01', null, null, '2016-01', false ],
+ [ '2016-01', '1', '2016-01', null, null, '2016-01', false ],
+ [ '2016-01', '1', '2016-01', null, 1, '2016-01', false ],
+ [ '2016-05', '3', '2016-01', null, null, '2016-04', false ],
+ [ '1969-01', '5', '1969-01', '1969-02', null, '1969-01', false ],
+ // Max values testing.
+ [ '2016-02', '1', null, 'foo', null, '2016-01', false ],
+ [ '2016-02', null, null, '2016-05', null, '2016-01', false ],
+ [ '2016-03', null, null, '2016-03', null, '2016-02', false ],
+ [ '2016-07', null, null, '2016-04', 4, '2016-03', false ],
+ [ '2016-07', '2', null, '2016-04', 3, '2016-01', false ],
+ // Step mismatch.
+ [ '2016-04', '2', '2016-01', null, null, '2016-03', false ],
+ [ '2016-06', '2', '2016-01', null, 2, '2016-03', false ],
+ [ '2016-05', '2', '2016-04', '2016-08', null, '2016-04', false ],
+ [ '1970-04', '2', null, null, null, '1970-02', false ],
+ [ '1970-09', '3', null, null, null, '1970-06', false ],
+ // Clamping.
+ [ '2016-05', null, null, '2016-01', null, '2016-01', false ],
+ [ '1970-05', '2', '1970-02', '1970-05', null, '1970-04', false ],
+ [ '1970-01', '5', '1970-02', '1970-09', 10, '1970-01', false ],
+ [ '1970-07', '5', '1969-12', '1970-10', 2, '1969-12', false ],
+ [ '1970-08', '3', '1970-01', '1970-07', 15, '1970-01', false ],
+ [ '1970-10', '3', '1970-01', '1970-06', 2, '1970-04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1969-12', false ],
+ // With step = 'any'.
+ [ '2016-01', 'any', null, null, 1, null, true ],
+ [ '2016-01', 'ANY', null, null, 1, null, true ],
+ [ '2016-01', 'AnY', null, null, 1, null, true ],
+ [ '2016-01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'week', data: [
+ // Regular case.
+ [ '2016-W40', null, null, null, null, '2016-W39', false ],
+ // Argument testing.
+ [ '2016-W40', null, null, null, 1, '2016-W39', false ],
+ [ '2016-W40', null, null, null, 5, '2016-W35', false ],
+ [ '2016-W40', null, null, null, -1, '2016-W41', false ],
+ [ '2016-W40', null, null, null, 0, '2016-W40', false ],
+ // Week/Year wrapping.
+ [ '2016-W01', null, null, null, 1, '2015-W53', false ],
+ [ '1969-W02', null, null, null, 4, '1968-W50', false ],
+ [ '1969-W01', null, null, null, -52, '1970-W01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2016-W40', null, null, null, 1.1, '2016-W39', false ],
+ [ '2016-W01', null, null, null, 1.9, '2015-W53', false ],
+ // With step values.
+ [ '2016-W03', '0.5', null, null, null, '2016-W02', false ],
+ [ '2016-W03', '2', null, null, null, '2016-W01', false ],
+ [ '2016-W03', '0.25', null, null, 4, '2015-W52', false ],
+ [ '2016-W52', '1.1', '2016-W01', null, 1, '2016-W51', false ],
+ [ '2016-W52', '1.1', '2016-W01', null, 2, '2016-W50', false ],
+ [ '2016-W52', '1.1', '2016-W01', null, 10, '2016-W42', false ],
+ [ '2016-W52', '1.1', '2016-W01', null, 52, '2016-W01', false ],
+ [ '1968-W52', '1.1', '1968-W01', null, 8, '1968-W44', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2016-W02', '0', null, null, null, '2016-W01', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2016-W02', '-1', null, null, null, '2016-W01', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2016-W02', 'foo', null, null, null, '2016-W01', false ],
+ // Min values testing.
+ [ '2016-W03', '1', 'foo', null, 2, '2016-W01', false ],
+ [ '2016-W02', '1', '2016-01', null, null, '2016-W01', false ],
+ [ '2016-W01', '1', '2016-W01', null, null, '2016-W01', false ],
+ [ '2016-W01', '1', '2016-W01', null, 1, '2016-W01', false ],
+ [ '2016-W05', '3', '2016-W01', null, null, '2016-W04', false ],
+ [ '1969-W01', '5', '1969-W01', '1969-W02', null, '1969-W01', false ],
+ // Max values testing.
+ [ '2016-W02', '1', null, 'foo', null, '2016-W01', false ],
+ [ '2016-W02', null, null, '2016-W05', null, '2016-W01', false ],
+ [ '2016-W03', null, null, '2016-W03', null, '2016-W02', false ],
+ [ '2016-W07', null, null, '2016-W04', 4, '2016-W03', false ],
+ [ '2016-W07', '2', null, '2016-W04', 3, '2016-W01', false ],
+ // Step mismatch.
+ [ '2016-W04', '2', '2016-W01', null, null, '2016-W03', false ],
+ [ '2016-W06', '2', '2016-W01', null, 2, '2016-W03', false ],
+ [ '2016-W05', '2', '2016-W04', '2016-W08', null, '2016-W04', false ],
+ [ '1970-W04', '2', null, null, null, '1970-W02', false ],
+ [ '1970-W09', '3', null, null, null, '1970-W06', false ],
+ // Clamping.
+ [ '2016-W05', null, null, '2016-W01', null, '2016-W01', false ],
+ [ '1970-W05', '2', '1970-W02', '1970-W05', null, '1970-W04', false ],
+ [ '1970-W01', '5', '1970-W02', '1970-W09', 10, '1970-W01', false ],
+ [ '1970-W07', '5', '1969-W52', '1970-W10', 2, '1969-W52', false ],
+ [ '1970-W08', '3', '1970-W01', '1970-W07', 15, '1970-W01', false ],
+ [ '1970-W10', '3', '1970-W01', '1970-W06', 2, '1970-W04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-W01', false ],
+ // With step = 'any'.
+ [ '2016-W01', 'any', null, null, 1, null, true ],
+ [ '2016-W01', 'ANY', null, null, 1, null, true ],
+ [ '2016-W01', 'AnY', null, null, 1, null, true ],
+ [ '2016-W01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'datetime-local', data: [
+ // Regular case.
+ [ '2017-02-07T09:30', null, null, null, null, '2017-02-07T09:29', false ],
+ // Argument testing.
+ [ '2017-02-07T09:30', null, null, null, 1, '2017-02-07T09:29', false ],
+ [ '2017-02-07T09:30', null, null, null, 5, '2017-02-07T09:25', false ],
+ [ '2017-02-07T09:30', null, null, null, -1, '2017-02-07T09:31', false ],
+ [ '2017-02-07T09:30', null, null, null, 0, '2017-02-07T09:30', false ],
+ // hour/minutes/seconds wrapping.
+ [ '2000-01-01T05:00', null, null, null, null, '2000-01-01T04:59', false ],
+ [ '2000-01-01T05:00:00', 1, null, null, null, '2000-01-01T04:59:59', false ],
+ [ '2000-01-01T05:00:00', 0.1, null, null, null, '2000-01-01T04:59:59.900', false ],
+ [ '2000-01-01T05:00:00', 0.01, null, null, null, '2000-01-01T04:59:59.990', false ],
+ [ '2000-01-01T05:00:00', 0.001, null, null, null, '2000-01-01T04:59:59.999', false ],
+ // month/year wrapping.
+ [ '2012-08-01T12:00', null, null, null, 1440, '2012-07-31T12:00', false ],
+ [ '1969-01-02T12:00', null, null, null, 5760, '1968-12-29T12:00', false ],
+ [ '1969-12-31T00:00', null, null, null, -1440, '1970-01-01T00:00', false ],
+ [ '2012-02-29T00:00', null, null, null, -1440, '2012-03-01T00:00', false ],
+ // stepDown() on '00:00' gives '23:59'.
+ [ '2017-02-07T00:00', null, null, null, 1, '2017-02-06T23:59', false ],
+ [ '2017-02-07T00:00', null, null, null, 3, '2017-02-06T23:57', false ],
+ // Some random step values..
+ [ '2017-02-07T16:07', '0.5', null, null, null, '2017-02-07T16:06:59.500', false ],
+ [ '2017-02-07T16:07', '2', null, null, null, '2017-02-07T16:06:58', false ],
+ [ '2017-02-07T16:07', '0.25', null, null, 4, '2017-02-07T16:06:59', false ],
+ [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 1, '2017-02-07T16:06:59.100', false ],
+ [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 2, '2017-02-07T16:06:58', false ],
+ [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 10, '2017-02-07T16:06:49.200', false ],
+ [ '2017-02-07T16:07', '129600', '2017-02-01T00:00', null, 2, '2017-02-05T12:00', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2017-02-07T10:15', '0', null, null, null, '2017-02-07T10:14', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2017-02-07T10:15', '-1', null, null, null, '2017-02-07T10:14', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2017-02-07T10:15', 'foo', null, null, null, '2017-02-07T10:14', false ],
+ // Min values testing.
+ [ '2012-02-02T17:02', '60', 'foo', null, 2, '2012-02-02T17:00', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:09', null, null, '2012-02-02T17:09', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:10', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:10', false ],
+ [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:08', false ],
+ [ '2012-02-03T20:05', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ],
+ [ '2012-02-03T18:00', '129600', '2012-02-01T00:00', null, null, '2012-02-02T12:00', false ],
+ // Max values testing.
+ [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:14', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:14', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:14', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:11', false ],
+ [ '2012-02-02T17:15', '120', null, '2012-02-02T17:13', 3, '2012-02-02T17:09', false ],
+ [ '2012-02-03T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-02T20:05', false ],
+ [ '2012-02-03T18:00', '129600', null, '2012-02-03T20:00', null, '2012-02-02T06:00', false ],
+ // Step mismatch.
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:18', false ],
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:16', false ],
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:18', false ],
+ [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:17', false ],
+ [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:16', false ],
+ [ '2017-02-07T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-06T17:19', false ],
+ // Clamping.
+ [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:11', false ],
+ [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:20', false ],
+ [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:12', false ],
+ [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:18', false ],
+ [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 15, '2017-02-07T15:00', false ],
+ [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 2, '2017-02-07T17:00', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1969-12-31T23:59', false ],
+ // With step = 'any'.
+ [ '2017-02-07T15:20', 'any', null, null, 1, null, true ],
+ [ '2017-02-07T15:20', 'ANY', null, null, 1, null, true ],
+ [ '2017-02-07T15:20', 'AnY', null, null, 1, null, true ],
+ [ '2017-02-07T15:20', 'aNy', null, null, 1, null, true ],
+ ]},
+ ];
+
+ for (var test of testData) {
+ for (var data of test.data) {
+ var element = document.createElement("input");
+ element.type = test.type;
+
+ if (data[1] != null) {
+ element.step = data[1];
+ }
+
+ if (data[2] != null) {
+ element.min = data[2];
+ }
+
+ if (data[3] != null) {
+ element.max = data[3];
+ }
+
+ // Set 'value' last for type=range, because the final sanitized value
+ // after setting 'step', 'min' and 'max' can be affected by the order in
+ // which those attributes are set. Setting 'value' last makes it simpler
+ // to reason about what the final value should be.
+ if (data[0] != null) {
+ element.setAttribute('value', data[0]);
+ }
+
+ var exceptionCaught = false;
+ try {
+ if (data[4] != null) {
+ element.stepDown(data[4]);
+ } else {
+ element.stepDown();
+ }
+
+ is(element.value, data[5], "The value for type=" + test.type + " should be " + data[5]);
+ } catch (e) {
+ exceptionCaught = true;
+ is(element.value, data[0], e.name + "The value should not have changed");
+ is(e.name, 'InvalidStateError',
+ "It should be a InvalidStateError exception.");
+ } finally {
+ is(exceptionCaught, data[6], "exception status should be " + data[6]);
+ }
+ }
+ }
+}
+
+function checkStepUp()
+{
+ // This testData is very similar to the one in checkStepDown with some changes
+ // relative to stepUp.
+ var testData = [
+ /* Initial value | step | min | max | stepUp arg | final value | exception */
+ { type: 'number', data: [
+ // Regular case.
+ [ '1', null, null, null, null, '2', false ],
+ // Argument testing.
+ [ '1', null, null, null, 1, '2', false ],
+ [ '9', null, null, null, 9, '18', false ],
+ [ '1', null, null, null, -1, '0', false ],
+ [ '1', null, null, null, 0, '1', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '1', null, null, null, 1.1, '2', false ],
+ // With step values.
+ [ '1', '0.5', null, null, null, '1.5', false ],
+ [ '1', '0.25', null, null, 4, '2', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '1', '0', null, null, null, '2', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '1', '-1', null, null, null, '2', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '1', 'foo', null, null, null, '2', false ],
+ // Min values testing.
+ [ '1', '1', 'foo', null, null, '2', false ],
+ [ '1', null, '-10', null, null, '2', false ],
+ [ '1', null, '0', null, null, '2', false ],
+ [ '1', null, '10', null, null, '10', false ],
+ [ '1', null, '2', null, null, '2', false ],
+ [ '1', null, '1', null, null, '2', false ],
+ [ '0', null, '4', null, '5', '5', false ],
+ [ '0', '2', '5', null, '3', '5', false ],
+ // Max values testing.
+ [ '1', '1', null, 'foo', null, '2', false ],
+ [ '1', null, null, '10', null, '2', false ],
+ [ '1', null, null, '0', null, '1', false ],
+ [ '1', null, null, '-10', null, '1', false ],
+ [ '1', null, null, '1', null, '1', false ],
+ [ '-3', '5', '-10', '-3', null, '-3', false ],
+ // Step mismatch.
+ [ '1', '2', '0', null, null, '2', false ],
+ [ '1', '2', '0', null, '2', '4', false ],
+ [ '8', '2', null, '9', null, '8', false ],
+ [ '-3', '2', '-6', null, null, '-2', false ],
+ [ '9', '3', '-10', null, null, '11', false ],
+ [ '7', '3', '-10', null, null, '8', false ],
+ [ '7', '3', '5', null, null, '8', false ],
+ [ '9', '4', '3', null, null, '11', false ],
+ [ '-2', '3', '-6', null, null, '0', false ],
+ [ '7', '3', '6', null, null, '9', false ],
+ // Clamping.
+ [ '1', '2', '0', '3', null, '2', false ],
+ [ '0', '5', '1', '8', '10', '6', false ],
+ [ '-9', '3', '-8', '-1', '5', '-2', false ],
+ [ '-9', '3', '8', '15', '15', '14', false ],
+ [ '-1', '3', '-1', '4', '3', '2', false ],
+ [ '-3', '2', '-6', '-2', null, '-2', false ],
+ [ '-3', '2', '-6', '-1', null, '-2', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1', false ],
+ [ '', null, null, null, null, '1', false ],
+ [ '', '2', null, null, null, '2', false ],
+ [ '', '2', '3', null, null, '3', false ],
+ [ '', null, '3', null, null, '3', false ],
+ [ '', '2', '3', '8', null, '3', false ],
+ [ '', null, '-10', '10', null, '1', false ],
+ [ '', '3', '-10', '10', null, '2', false ],
+ // With step = 'any'.
+ [ '0', 'any', null, null, 1, null, true ],
+ [ '0', 'ANY', null, null, 1, null, true ],
+ [ '0', 'AnY', null, null, 1, null, true ],
+ [ '0', 'aNy', null, null, 1, null, true ],
+ // With @value = step base.
+ [ '1', '2', null, null, null, '3', false ],
+ ]},
+ { type: 'range', data: [
+ // Regular case.
+ [ '1', null, null, null, null, '2', false ],
+ // Argument testing.
+ [ '1', null, null, null, 1, '2', false ],
+ [ '9', null, null, null, 9, '18', false ],
+ [ '1', null, null, null, -1, '0', false ],
+ [ '1', null, null, null, 0, '1', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '1', null, null, null, 1.1, '2', false ],
+ // With step values.
+ [ '1', '0.5', null, null, null, '1.5', false ],
+ [ '1', '0.25', null, null, 4, '2', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '1', '0', null, null, null, '2', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '1', '-1', null, null, null, '2', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '1', 'foo', null, null, null, '2', false ],
+ // Min values testing.
+ [ '1', '1', 'foo', null, null, '2', false ],
+ [ '1', null, '-10', null, null, '2', false ],
+ [ '1', null, '0', null, null, '2', false ],
+ [ '1', null, '10', null, null, '11', false ],
+ [ '1', null, '2', null, null, '3', false ],
+ [ '1', null, '1', null, null, '2', false ],
+ [ '0', null, '4', null, '5', '9', false ],
+ [ '0', '2', '5', null, '3', '11', false ],
+ // Max values testing.
+ [ '1', '1', null, 'foo', null, '2', false ],
+ [ '1', null, null, '10', null, '2', false ],
+ [ '1', null, null, '0', null, '0', false ],
+ [ '1', null, null, '-10', null, '0', false ],
+ [ '1', null, null, '1', null, '1', false ],
+ [ '-3', '5', '-10', '-3', null, '-5', false ],
+ // Step mismatch.
+ [ '1', '2', '0', null, null, '4', false ],
+ [ '1', '2', '0', null, '2', '6', false ],
+ [ '8', '2', null, '9', null, '8', false ],
+ [ '-3', '2', '-6', null, null, '0', false ],
+ [ '9', '3', '-10', null, null, '11', false ],
+ [ '7', '3', '-10', null, null, '11', false ],
+ [ '7', '3', '5', null, null, '11', false ],
+ [ '9', '4', '3', null, null, '15', false ],
+ [ '-2', '3', '-6', null, null, '0', false ],
+ [ '7', '3', '6', null, null, '9', false ],
+ // Clamping.
+ [ '1', '2', '0', '3', null, '2', false ],
+ [ '0', '5', '1', '8', '10', '6', false ],
+ [ '-9', '3', '-8', '-1', '5', '-2', false ],
+ [ '-9', '3', '8', '15', '15', '14', false ],
+ [ '-1', '3', '-1', '4', '3', '2', false ],
+ [ '-3', '2', '-6', '-2', null, '-2', false ],
+ [ '-3', '2', '-6', '-1', null, '-2', false ],
+ // value = "" (default will be 50).
+ [ '', null, null, null, null, '51', false ],
+ // With step = 'any'.
+ [ '0', 'any', null, null, 1, null, true ],
+ [ '0', 'ANY', null, null, 1, null, true ],
+ [ '0', 'AnY', null, null, 1, null, true ],
+ [ '0', 'aNy', null, null, 1, null, true ],
+ // With @value = step base.
+ [ '1', '2', null, null, null, '3', false ],
+ ]},
+ { type: 'date', data: [
+ // Regular case.
+ [ '2012-07-09', null, null, null, null, '2012-07-10', false ],
+ // Argument testing.
+ [ '2012-07-09', null, null, null, 1, '2012-07-10', false ],
+ [ '2012-07-09', null, null, null, 9, '2012-07-18', false ],
+ [ '2012-07-09', null, null, null, -1, '2012-07-08', false ],
+ [ '2012-07-09', null, null, null, 0, '2012-07-09', false ],
+ // Month/Year wrapping.
+ [ '2012-07-31', null, null, null, 1, '2012-08-01', false ],
+ [ '1968-12-29', null, null, null, 4, '1969-01-02', false ],
+ [ '1970-01-01', null, null, null, -365, '1969-01-01', false ],
+ [ '2012-03-01', null, null, null, -1, '2012-02-29', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2012-01-01', null, null, null, 1.1, '2012-01-02', false ],
+ [ '2012-01-01', null, null, null, 1.9, '2012-01-02', false ],
+ // With step values.
+ [ '2012-01-01', '0.5', null, null, null, '2012-01-02', false ],
+ [ '2012-01-01', '2', null, null, null, '2012-01-03', false ],
+ [ '2012-01-01', '0.25', null, null, 4, '2012-01-05', false ],
+ [ '2012-01-01', '1.1', '2012-01-01', null, 1, '2012-01-02', false ],
+ [ '2012-01-01', '1.1', '2012-01-01', null, 2, '2012-01-03', false ],
+ [ '2012-01-01', '1.1', '2012-01-01', null, 10, '2012-01-11', false ],
+ [ '2012-01-01', '1.1', '2012-01-01', null, 11, '2012-01-12', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2012-01-01', '0', null, null, null, '2012-01-02', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2012-01-01', '-1', null, null, null, '2012-01-02', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2012-01-01', 'foo', null, null, null, '2012-01-02', false ],
+ // Min values testing.
+ [ '2012-01-01', '1', 'foo', null, null, '2012-01-02', false ],
+ [ '2012-01-01', null, '2011-12-01', null, null, '2012-01-02', false ],
+ [ '2012-01-01', null, '2012-01-02', null, null, '2012-01-02', false ],
+ [ '2012-01-01', null, '2012-01-01', null, null, '2012-01-02', false ],
+ [ '2012-01-01', null, '2012-01-04', null, 4, '2012-01-05', false ],
+ [ '2012-01-01', '2', '2012-01-04', null, 3, '2012-01-06', false ],
+ // Max values testing.
+ [ '2012-01-01', '1', null, 'foo', 2, '2012-01-03', false ],
+ [ '2012-01-01', '1', null, '2012-01-10', 1, '2012-01-02', false ],
+ [ '2012-01-02', null, null, '2012-01-01', null, '2012-01-02', false ],
+ [ '2012-01-02', null, null, '2012-01-02', null, '2012-01-02', false ],
+ [ '1969-01-02', '5', '1969-01-01', '1969-01-02', null, '1969-01-02', false ],
+ // Step mismatch.
+ [ '2012-01-02', '2', '2012-01-01', null, null, '2012-01-03', false ],
+ [ '2012-01-02', '2', '2012-01-01', null, 2, '2012-01-05', false ],
+ [ '2012-01-05', '2', '2012-01-01', '2012-01-06', null, '2012-01-05', false ],
+ [ '1970-01-02', '2', null, null, null, '1970-01-04', false ],
+ [ '1970-01-05', '3', null, null, null, '1970-01-08', false ],
+ [ '1970-01-03', '3', null, null, null, '1970-01-06', false ],
+ [ '1970-01-03', '3', '1970-01-02', null, null, '1970-01-05', false ],
+ // Clamping.
+ [ '2012-01-01', null, '2012-01-31', null, null, '2012-01-31', false ],
+ [ '1970-01-02', '2', '1970-01-01', '1970-01-04', null, '1970-01-03', false ],
+ [ '1970-01-01', '5', '1970-01-02', '1970-01-09', 10, '1970-01-07', false ],
+ [ '1969-12-28', '5', '1969-12-29', '1970-01-06', 3, '1970-01-03', false ],
+ [ '1970-01-01', '3', '1970-02-01', '1970-02-07', 15, '1970-02-07', false ],
+ [ '1970-01-01', '3', '1970-01-01', '1970-01-06', 2, '1970-01-04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-01-02', false ],
+ // With step = 'any'.
+ [ '2012-01-01', 'any', null, null, 1, null, true ],
+ [ '2012-01-01', 'ANY', null, null, 1, null, true ],
+ [ '2012-01-01', 'AnY', null, null, 1, null, true ],
+ [ '2012-01-01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'time', data: [
+ // Regular case.
+ [ '16:39', null, null, null, null, '16:40', false ],
+ // Argument testing.
+ [ '16:40', null, null, null, 1, '16:41', false ],
+ [ '16:40', null, null, null, 5, '16:45', false ],
+ [ '16:40', null, null, null, -1, '16:39', false ],
+ [ '16:40', null, null, null, 0, '16:40', false ],
+ // hour/minutes/seconds wrapping.
+ [ '04:59', null, null, null, null, '05:00', false ],
+ [ '04:59:59', 1, null, null, null, '05:00', false ],
+ [ '04:59:59.900', 0.1, null, null, null, '05:00', false ],
+ [ '04:59:59.990', 0.01, null, null, null, '05:00', false ],
+ [ '04:59:59.999', 0.001, null, null, null, '05:00', false ],
+ // stepUp() on '23:59' gives '00:00'.
+ [ '23:59', null, null, null, 1, '00:00', false ],
+ [ '23:59', null, null, null, 3, '00:02', false ],
+ // Some random step values..
+ [ '16:56', '0.5', null, null, null, '16:56:00.500', false ],
+ [ '16:56', '2', null, null, null, '16:56:02', false ],
+ [ '16:56', '0.25',null, null, 4, '16:56:01', false ],
+ [ '16:57', '1.1', '16:00', null, 1, '16:57:01', false ],
+ [ '16:57', '1.1', '16:00', null, 2, '16:57:02.100', false ],
+ [ '16:57', '1.1', '16:00', null, 10, '16:57:10.900', false ],
+ [ '16:57', '1.1', '16:00', null, 11, '16:57:12', false ],
+ [ '16:57', '1.1', '16:00', null, 8, '16:57:08.700', false ],
+ // Invalid @step, means that we use the default value.
+ [ '17:01', '0', null, null, null, '17:02', false ],
+ [ '17:01', '-1', null, null, null, '17:02', false ],
+ [ '17:01', 'foo', null, null, null, '17:02', false ],
+ // Min values testing.
+ [ '17:02', '60', 'foo', null, 2, '17:04', false ],
+ [ '17:10', '60', '17:09', null, null, '17:11', false ],
+ [ '17:10', '60', '17:10', null, null, '17:11', false ],
+ [ '17:10', '60', '17:30', null, 1, '17:30', false ],
+ [ '17:10', '180', '17:05', null, null, '17:11', false ],
+ [ '17:10', '300', '17:10', '17:11', null,'17:10', false ],
+ // Max values testing.
+ [ '17:15', '60', null, 'foo', null, '17:16', false ],
+ [ '17:15', null, null, '17:20', null, '17:16', false ],
+ [ '17:15', null, null, '17:15', null, '17:15', false ],
+ [ '17:15', null, null, '17:13', 4, '17:15', false ],
+ [ '17:15', '120', null, '17:13', 3, '17:15', false ],
+ // Step mismatch.
+ [ '17:19', '120', '17:10', null, null, '17:20', false ],
+ [ '17:19', '120', '17:10', null, 2, '17:22', false ],
+ [ '17:19', '120', '17:18', '17:25', null, '17:20', false ],
+ [ '17:19', '120', null, null, null, '17:21', false ],
+ [ '17:19', '180', null, null, null, '17:22', false ],
+ // Clamping.
+ [ '17:22', null, null, '17:11', null, '17:22', false ],
+ [ '17:22', '120', '17:20', '17:22', null, '17:22', false ],
+ [ '17:22', '300', '17:12', '17:20', 10, '17:22', false ],
+ [ '17:22', '300', '17:18', '17:20', 2, '17:22', false ],
+ [ '17:22', '180', '17:00', '17:20', 15, '17:22', false ],
+ [ '17:22', '180', '17:10', '17:20', 2, '17:22', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '00:01', false ],
+ // With step = 'any'.
+ [ '17:26', 'any', null, null, 1, null, true ],
+ [ '17:26', 'ANY', null, null, 1, null, true ],
+ [ '17:26', 'AnY', null, null, 1, null, true ],
+ [ '17:26', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'month', data: [
+ // Regular case.
+ [ '2016-08', null, null, null, null, '2016-09', false ],
+ // Argument testing.
+ [ '2016-08', null, null, null, 1, '2016-09', false ],
+ [ '2016-08', null, null, null, 9, '2017-05', false ],
+ [ '2016-08', null, null, null, -1, '2016-07', false ],
+ [ '2016-08', null, null, null, 0, '2016-08', false ],
+ // Month/Year wrapping.
+ [ '2015-12', null, null, null, 1, '2016-01', false ],
+ [ '1968-12', null, null, null, 4, '1969-04', false ],
+ [ '1970-01', null, null, null, -12, '1969-01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2016-01', null, null, null, 1.1, '2016-02', false ],
+ [ '2016-01', null, null, null, 1.9, '2016-02', false ],
+ // With step values.
+ [ '2016-01', '0.5', null, null, null, '2016-02', false ],
+ [ '2016-01', '2', null, null, null, '2016-03', false ],
+ [ '2016-01', '0.25', null, null, 4, '2016-05', false ],
+ [ '2016-01', '1.1', '2016-01', null, 1, '2016-02', false ],
+ [ '2016-01', '1.1', '2016-01', null, 2, '2016-03', false ],
+ [ '2016-01', '1.1', '2016-01', null, 10, '2016-11', false ],
+ [ '2016-01', '1.1', '2016-01', null, 11, '2016-12', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2016-01', '0', null, null, null, '2016-02', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2016-01', '-1', null, null, null, '2016-02', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2016-01', 'foo', null, null, null, '2016-02', false ],
+ // Min values testing.
+ [ '2016-01', '1', 'foo', null, null, '2016-02', false ],
+ [ '2016-01', null, '2015-12', null, null, '2016-02', false ],
+ [ '2016-01', null, '2016-02', null, null, '2016-02', false ],
+ [ '2016-01', null, '2016-01', null, null, '2016-02', false ],
+ [ '2016-01', null, '2016-04', null, 4, '2016-05', false ],
+ [ '2016-01', '2', '2016-04', null, 3, '2016-06', false ],
+ // Max values testing.
+ [ '2016-01', '1', null, 'foo', 2, '2016-03', false ],
+ [ '2016-01', '1', null, '2016-02', 1, '2016-02', false ],
+ [ '2016-02', null, null, '2016-01', null, '2016-02', false ],
+ [ '2016-02', null, null, '2016-02', null, '2016-02', false ],
+ [ '1969-02', '5', '1969-01', '1969-02', null, '1969-02', false ],
+ // Step mismatch.
+ [ '2016-02', '2', '2016-01', null, null, '2016-03', false ],
+ [ '2016-02', '2', '2016-01', null, 2, '2016-05', false ],
+ [ '2016-05', '2', '2016-01', '2016-06', null, '2016-05', false ],
+ [ '1970-02', '2', null, null, null, '1970-04', false ],
+ [ '1970-05', '3', null, null, null, '1970-08', false ],
+ [ '1970-03', '3', null, null, null, '1970-06', false ],
+ [ '1970-03', '3', '1970-02', null, null, '1970-05', false ],
+ // Clamping.
+ [ '2016-01', null, '2016-12', null, null, '2016-12', false ],
+ [ '1970-02', '2', '1970-01', '1970-04', null, '1970-03', false ],
+ [ '1970-01', '5', '1970-02', '1970-09', 10, '1970-07', false ],
+ [ '1969-11', '5', '1969-12', '1970-06', 3, '1970-05', false ],
+ [ '1970-01', '3', '1970-02', '1971-07', 15, '1971-05', false ],
+ [ '1970-01', '3', '1970-01', '1970-06', 2, '1970-04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-02', false ],
+ // With step = 'any'.
+ [ '2016-01', 'any', null, null, 1, null, true ],
+ [ '2016-01', 'ANY', null, null, 1, null, true ],
+ [ '2016-01', 'AnY', null, null, 1, null, true ],
+ [ '2016-01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'week', data: [
+ // Regular case.
+ [ '2016-W40', null, null, null, null, '2016-W41', false ],
+ // Argument testing.
+ [ '2016-W40', null, null, null, 1, '2016-W41', false ],
+ [ '2016-W40', null, null, null, 20, '2017-W08', false ],
+ [ '2016-W40', null, null, null, -1, '2016-W39', false ],
+ [ '2016-W40', null, null, null, 0, '2016-W40', false ],
+ // Week/Year wrapping.
+ [ '2015-W53', null, null, null, 1, '2016-W01', false ],
+ [ '1968-W52', null, null, null, 4, '1969-W04', false ],
+ [ '1970-W01', null, null, null, -52, '1969-W01', false ],
+ // Float values are rounded to integer (1.1 -> 1).
+ [ '2016-W01', null, null, null, 1.1, '2016-W02', false ],
+ [ '2016-W01', null, null, null, 1.9, '2016-W02', false ],
+ // With step values.
+ [ '2016-W01', '0.5', null, null, null, '2016-W02', false ],
+ [ '2016-W01', '2', null, null, null, '2016-W03', false ],
+ [ '2016-W01', '0.25', null, null, 4, '2016-W05', false ],
+ [ '2016-W01', '1.1', '2016-01', null, 1, '2016-W02', false ],
+ [ '2016-W01', '1.1', '2016-01', null, 2, '2016-W03', false ],
+ [ '2016-W01', '1.1', '2016-01', null, 10, '2016-W11', false ],
+ [ '2016-W01', '1.1', '2016-01', null, 20, '2016-W21', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2016-W01', '0', null, null, null, '2016-W02', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2016-W01', '-1', null, null, null, '2016-W02', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2016-W01', 'foo', null, null, null, '2016-W02', false ],
+ // Min values testing.
+ [ '2016-W01', '1', 'foo', null, null, '2016-W02', false ],
+ [ '2016-W01', null, '2015-W53', null, null, '2016-W02', false ],
+ [ '2016-W01', null, '2016-W02', null, null, '2016-W02', false ],
+ [ '2016-W01', null, '2016-W01', null, null, '2016-W02', false ],
+ [ '2016-W01', null, '2016-W04', null, 4, '2016-W05', false ],
+ [ '2016-W01', '2', '2016-W04', null, 3, '2016-W06', false ],
+ // Max values testing.
+ [ '2016-W01', '1', null, 'foo', 2, '2016-W03', false ],
+ [ '2016-W01', '1', null, '2016-W02', 1, '2016-W02', false ],
+ [ '2016-W02', null, null, '2016-W01', null, '2016-W02', false ],
+ [ '2016-W02', null, null, '2016-W02', null, '2016-W02', false ],
+ [ '1969-W02', '5', '1969-W01', '1969-W02', null, '1969-W02', false ],
+ // Step mismatch.
+ [ '2016-W02', '2', '2016-W01', null, null, '2016-W03', false ],
+ [ '2016-W02', '2', '2016-W01', null, 2, '2016-W05', false ],
+ [ '2016-W05', '2', '2016-W01', '2016-W06', null, '2016-W05', false ],
+ [ '1970-W02', '2', null, null, null, '1970-W04', false ],
+ [ '1970-W05', '3', null, null, null, '1970-W08', false ],
+ [ '1970-W03', '3', null, null, null, '1970-W06', false ],
+ [ '1970-W03', '3', '1970-W02', null, null, '1970-W05', false ],
+ // Clamping.
+ [ '2016-W01', null, '2016-W52', null, null, '2016-W52', false ],
+ [ '1970-W02', '2', '1970-W01', '1970-W04', null, '1970-W03', false ],
+ [ '1970-W01', '5', '1970-W02', '1970-W09', 10, '1970-W07', false ],
+ [ '1969-W50', '5', '1969-W52', '1970-W06', 3, '1970-W05', false ],
+ [ '1970-W01', '3', '1970-W02', '1971-W07', 15, '1970-W44', false ],
+ [ '1970-W01', '3', '1970-W01', '1970-W06', 2, '1970-W04', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-W02', false ],
+ // With step = 'any'.
+ [ '2016-W01', 'any', null, null, 1, null, true ],
+ [ '2016-W01', 'ANY', null, null, 1, null, true ],
+ [ '2016-W01', 'AnY', null, null, 1, null, true ],
+ [ '2016-W01', 'aNy', null, null, 1, null, true ],
+ ]},
+ { type: 'datetime-local', data: [
+ // Regular case.
+ [ '2017-02-07T17:09', null, null, null, null, '2017-02-07T17:10', false ],
+ // Argument testing.
+ [ '2017-02-07T17:10', null, null, null, 1, '2017-02-07T17:11', false ],
+ [ '2017-02-07T17:10', null, null, null, 5, '2017-02-07T17:15', false ],
+ [ '2017-02-07T17:10', null, null, null, -1, '2017-02-07T17:09', false ],
+ [ '2017-02-07T17:10', null, null, null, 0, '2017-02-07T17:10', false ],
+ // hour/minutes/seconds wrapping.
+ [ '2000-01-01T04:59', null, null, null, null, '2000-01-01T05:00', false ],
+ [ '2000-01-01T04:59:59', 1, null, null, null, '2000-01-01T05:00', false ],
+ [ '2000-01-01T04:59:59.900', 0.1, null, null, null, '2000-01-01T05:00', false ],
+ [ '2000-01-01T04:59:59.990', 0.01, null, null, null, '2000-01-01T05:00', false ],
+ [ '2000-01-01T04:59:59.999', 0.001, null, null, null, '2000-01-01T05:00', false ],
+ // month/year wrapping.
+ [ '2012-07-31T12:00', null, null, null, 1440, '2012-08-01T12:00', false ],
+ [ '1968-12-29T12:00', null, null, null, 5760, '1969-01-02T12:00', false ],
+ [ '1970-01-01T00:00', null, null, null, -1440, '1969-12-31T00:00', false ],
+ [ '2012-03-01T00:00', null, null, null, -1440, '2012-02-29T00:00', false ],
+ // stepUp() on '23:59' gives '00:00'.
+ [ '2017-02-07T23:59', null, null, null, 1, '2017-02-08T00:00', false ],
+ [ '2017-02-07T23:59', null, null, null, 3, '2017-02-08T00:02', false ],
+ // Some random step values..
+ [ '2017-02-07T17:40', '0.5', null, null, null, '2017-02-07T17:40:00.500', false ],
+ [ '2017-02-07T17:40', '2', null, null, null, '2017-02-07T17:40:02', false ],
+ [ '2017-02-07T17:40', '0.25', null, null, 4, '2017-02-07T17:40:01', false ],
+ [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 1, '2017-02-07T17:40:00.200', false ],
+ [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 2, '2017-02-07T17:40:01.300', false ],
+ [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 10, '2017-02-07T17:40:10.100', false ],
+ [ '2017-02-07T17:40', '129600', '2017-02-01T00:00', null, 2, '2017-02-10T00:00', false ],
+ // step = 0 isn't allowed (-> step = 1).
+ [ '2017-02-07T17:39', '0', null, null, null, '2017-02-07T17:40', false ],
+ // step < 0 isn't allowed (-> step = 1).
+ [ '2017-02-07T17:39', '-1', null, null, null, '2017-02-07T17:40', false ],
+ // step = NaN isn't allowed (-> step = 1).
+ [ '2017-02-07T17:39', 'foo', null, null, null, '2017-02-07T17:40', false ],
+ // Min values testing.
+ [ '2012-02-02T17:00', '60', 'foo', null, 2, '2012-02-02T17:02', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:11', false ],
+ [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:30', false ],
+ [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:11', false ],
+ [ '2012-02-02T17:10', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ],
+ [ '2012-02-02T17:10', '129600', '2012-02-01T00:00', null, null, '2012-02-04T00:00', false ],
+ // Max values testing.
+ [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:16', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:16', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:15', false ],
+ [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:15', false ],
+ [ '2012-02-02T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-03T20:05', false ],
+ [ '2012-02-02T18:00', '129600', null, '2012-02-04T20:00', null, '2012-02-04T06:00', false ],
+ // Step mismatch.
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:20', false ],
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:22', false ],
+ [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:20', false ],
+ [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:21', false ],
+ [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:22', false ],
+ [ '2017-02-03T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-04T17:19', false ],
+ // Clamping.
+ [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:22', false ],
+ [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:22', false ],
+ [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:22', false ],
+ [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:22', false ],
+ [ '2017-02-06T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:20', 15, '2017-02-06T19:50', false ],
+ [ '2017-02-06T17:22', '600', '2017-02-02T17:10', '2017-02-07T17:20', 2, '2017-02-06T17:40', false ],
+ // value = "" (NaN).
+ [ '', null, null, null, null, '1970-01-01T00:01', false ],
+ // With step = 'any'.
+ [ '2017-02-07T17:30', 'any', null, null, 1, null, true ],
+ [ '2017-02-07T17:30', 'ANY', null, null, 1, null, true ],
+ [ '2017-02-07T17:30', 'AnY', null, null, 1, null, true ],
+ [ '2017-02-07T17:30', 'aNy', null, null, 1, null, true ],
+ ]},
+ ];
+
+ for (var test of testData) {
+ for (var data of test.data) {
+ var element = document.createElement("input");
+ element.type = test.type;
+
+ if (data[1] != null) {
+ element.step = data[1];
+ }
+
+ if (data[2] != null) {
+ element.min = data[2];
+ }
+
+ if (data[3] != null) {
+ element.max = data[3];
+ }
+
+ // Set 'value' last for type=range, because the final sanitized value
+ // after setting 'step', 'min' and 'max' can be affected by the order in
+ // which those attributes are set. Setting 'value' last makes it simpler
+ // to reason about what the final value should be.
+ if (data[0] != null) {
+ element.setAttribute('value', data[0]);
+ }
+
+ var exceptionCaught = false;
+ try {
+ if (data[4] != null) {
+ element.stepUp(data[4]);
+ } else {
+ element.stepUp();
+ }
+
+ is(element.value, data[5], "The value for type=" + test.type + " should be " + data[5]);
+ } catch (e) {
+ exceptionCaught = true;
+ is(element.value, data[0], e.name + "The value should not have changed");
+ is(e.name, 'InvalidStateError',
+ "It should be a InvalidStateError exception.");
+ } finally {
+ is(exceptionCaught, data[6], "exception status should be " + data[6]);
+ }
+ }
+ }
+}
+
+checkPresence();
+checkAvailability();
+
+checkStepDown();
+checkStepUp();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_submit_invalid_file.html b/dom/html/test/forms/test_submit_invalid_file.html
new file mode 100644
index 0000000000..68b5e44877
--- /dev/null
+++ b/dom/html/test/forms/test_submit_invalid_file.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=702949
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test invalid file submission</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=702949">Mozilla Bug 702949</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form action='http://mochi.test:8888/chrome/dom/html/test/forms/submit_invalid_file.sjs' method='post' target='result'
+ enctype='multipart/form-data'>
+ <input type='file' name='file'>
+ </form>
+ <iframe name='result'></iframe>
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+ /*
+ * Test invalid file submission by submitting a file that has been deleted
+ * from the file system before the form has been submitted.
+ * The form submission triggers a sjs file that shows its output in a frame.
+ * That means the test might time out if it fails.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(function() {
+ var { FileUtils } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+ );
+
+ var i = document.getElementsByTagName('input')[0];
+
+ var file = FileUtils.getDir("TmpD", []);
+ file.append("testfile");
+ file.createUnique(SpecialPowers.Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ SpecialPowers.wrap(i).value = file.path;
+ file.remove(/* recursive = */ false);
+
+ document.getElementsByName('result')[0].addEventListener('load', function() {
+ is(window.frames[0].document.body.textContent, "SUCCESS");
+ SimpleTest.finish();
+ });
+ document.forms[0].submit();
+ });
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_textarea_attributes_reflection.html b/dom/html/test/forms/test_textarea_attributes_reflection.html
new file mode 100644
index 0000000000..925f97e751
--- /dev/null
+++ b/dom/html/test/forms/test_textarea_attributes_reflection.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLTextAreaElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="../reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLTextAreaElement attributes reflection **/
+
+// .autofocus
+reflectBoolean({
+ element: document.createElement("textarea"),
+ attribute: "autofocus",
+});
+
+//.cols
+reflectUnsignedInt({
+ element: document.createElement("textarea"),
+ attribute: "cols",
+ nonZero: true,
+ defaultValue: 20,
+ fallback: true,
+});
+
+//.dirname
+reflectString({
+ element: document.createElement("textarea"),
+ attribute: "dirName"
+})
+
+// .disabled
+reflectBoolean({
+ element: document.createElement("textarea"),
+ attribute: "disabled",
+});
+
+// TODO: form (HTMLFormElement)
+
+// .maxLength
+reflectInt({
+ element: document.createElement("textarea"),
+ attribute: "maxLength",
+ nonNegative: true,
+});
+
+// .name
+reflectString({
+ element: document.createElement("textarea"),
+ attribute: "name",
+ otherValues: [ "isindex", "_charset_" ],
+});
+
+// .placeholder
+reflectString({
+ element: document.createElement("textarea"),
+ attribute: "placeholder",
+ otherValues: [ "foo\nbar", "foo\rbar", "foo\r\nbar" ],
+});
+
+// .readOnly
+reflectBoolean({
+ element: document.createElement("textarea"),
+ attribute: "readOnly",
+});
+
+// .required
+reflectBoolean({
+ element: document.createElement("textarea"),
+ attribute: "required",
+});
+
+// .rows
+reflectUnsignedInt({
+ element: document.createElement("textarea"),
+ attribute: "rows",
+ nonZero: true,
+ defaultValue: 2,
+ fallback: true,
+});
+
+// .wrap
+// TODO: make it an enumerated attributes limited to only known values, bug 670869.
+reflectString({
+ element: document.createElement("textarea"),
+ attribute: "wrap",
+ otherValues: [ "soft", "hard" ],
+});
+
+// .type doesn't reflect a content attribute.
+// .defaultValue doesn't reflect a content attribute.
+// .value doesn't reflect a content attribute.
+// .textLength doesn't reflect a content attribute.
+// .willValidate doesn't reflect a content attribute.
+// .validity doesn't reflect a content attribute.
+// .validationMessage doesn't reflect a content attribute.
+// .labels doesn't reflect a content attribute.
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_validation.html b/dom/html/test/forms/test_validation.html
new file mode 100644
index 0000000000..666d4a45c0
--- /dev/null
+++ b/dom/html/test/forms/test_validation.html
@@ -0,0 +1,343 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=345624
+-->
+<head>
+ <title>Test for Bug 345624</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input, textarea, fieldset, button, select, output, object { background-color: rgb(0,0,0) !important; }
+ :valid { background-color: rgb(0,255,0) !important; }
+ :invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345624">Mozilla Bug 345624</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <fieldset id='f'></fieldset>
+ <input id='i' oninvalid="invalidEventHandler(event);">
+ <button id='b' oninvalid="invalidEventHandler(event);"></button>
+ <select id='s' oninvalid="invalidEventHandler(event);"></select>
+ <textarea id='t' oninvalid="invalidEventHandler(event);"></textarea>
+ <output id='o' oninvalid="invalidEventHandler(event);"></output>
+ <object id='obj'></object>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 345624 **/
+
+var gInvalid = false;
+
+function invalidEventHandler(aEvent)
+{
+ function checkInvalidEvent(event)
+ {
+ is(event.type, "invalid", "Invalid event type should be invalid");
+ ok(!event.bubbles, "Invalid event should not bubble");
+ ok(event.cancelable, "Invalid event should be cancelable");
+ }
+
+ checkInvalidEvent(aEvent);
+
+ gInvalid = true;
+}
+
+function checkConstraintValidationAPIExist(element)
+{
+ ok('willValidate' in element, "willValidate is not available in the DOM");
+ ok('validationMessage' in element, "validationMessage is not available in the DOM");
+ ok('validity' in element, "validity is not available in the DOM");
+
+ if ('validity' in element) {
+ validity = element.validity;
+ ok('valueMissing' in validity, "validity.valueMissing is not available in the DOM");
+ ok('typeMismatch' in validity, "validity.typeMismatch is not available in the DOM");
+ ok('badInput' in validity, "validity.badInput is not available in the DOM");
+ ok('patternMismatch' in validity, "validity.patternMismatch is not available in the DOM");
+ ok('tooLong' in validity, "validity.tooLong is not available in the DOM");
+ ok('rangeUnderflow' in validity, "validity.rangeUnderflow is not available in the DOM");
+ ok('rangeOverflow' in validity, "validity.rangeOverflow is not available in the DOM");
+ ok('stepMismatch' in validity, "validity.stepMismatch is not available in the DOM");
+ ok('customError' in validity, "validity.customError is not available in the DOM");
+ ok('valid' in validity, "validity.valid is not available in the DOM");
+ }
+}
+
+function checkConstraintValidationAPIDefaultValues(element)
+{
+ // Not checking willValidate because the default value depends of the element
+
+ is(element.validationMessage, "", "validationMessage default value should be empty string");
+
+ ok(!element.validity.valueMissing, "The element should not suffer from a constraint validation");
+ ok(!element.validity.typeMismatch, "The element should not suffer from a constraint validation");
+ ok(!element.validity.badInput, "The element should not suffer from a constraint validation");
+ ok(!element.validity.patternMismatch, "The element should not suffer from a constraint validation");
+ ok(!element.validity.tooLong, "The element should not suffer from a constraint validation");
+ ok(!element.validity.rangeUnderflow, "The element should not suffer from a constraint validation");
+ ok(!element.validity.rangeOverflow, "The element should not suffer from a constraint validation");
+ ok(!element.validity.stepMismatch, "The element should not suffer from a constraint validation");
+ ok(!element.validity.customError, "The element should not suffer from a constraint validation");
+ ok(element.validity.valid, "The element should be valid by default");
+
+ ok(element.checkValidity(), "The element should be valid by default");
+}
+
+function checkDefaultPseudoClass()
+{
+ is(window.getComputedStyle(document.getElementById('f'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid should apply");
+
+ is(window.getComputedStyle(document.getElementById('o'))
+ .getPropertyValue('background-color'), "rgb(0, 0, 0)",
+ "Nor :valid and :invalid should apply");
+
+ is(window.getComputedStyle(document.getElementById('obj'))
+ .getPropertyValue('background-color'), "rgb(0, 0, 0)",
+ "Nor :valid and :invalid should apply");
+
+ is(window.getComputedStyle(document.getElementById('s'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid pseudo-class should apply");
+
+ is(window.getComputedStyle(document.getElementById('i'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid pseudo-class should apply");
+
+ is(window.getComputedStyle(document.getElementById('t'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid pseudo-class should apply");
+
+ is(window.getComputedStyle(document.getElementById('b'))
+ .getPropertyValue('background-color'), "rgb(0, 255, 0)",
+ ":valid pseudo-class should apply");
+}
+
+function checkSpecificWillValidate()
+{
+ // fieldset, output, object (TODO) and select elements
+ ok(!document.getElementById('f').willValidate, "Fielset element should be barred from constraint validation");
+ ok(!document.getElementById('obj').willValidate, "Object element should be barred from constraint validation");
+ ok(!document.getElementById('o').willValidate, "Output element should be barred from constraint validation");
+ ok(document.getElementById('s').willValidate, "Select element should not be barred from constraint validation");
+
+ // input element
+ i = document.getElementById('i');
+ i.type = "hidden";
+ ok(!i.willValidate, "Hidden state input should be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ i.type = "reset";
+ ok(!i.willValidate, "Reset button state input should be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ i.type = "button";
+ ok(!i.willValidate, "Button state input should be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ i.type = "image";
+ ok(i.willValidate, "Image state input should not be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid and :invalid should apply");
+ i.type = "submit";
+ ok(i.willValidate, "Submit state input should not be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid and :invalid should apply");
+ i.type = "number";
+ ok(i.willValidate, "Number state input should not be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+ i.type = "";
+ i.readOnly = 'true';
+ ok(!i.willValidate, "Readonly input should be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ i.removeAttribute('readOnly');
+ ok(i.willValidate, "Default input element should not be barred from constraint validation");
+ is(window.getComputedStyle(i).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+
+ // button element
+ b = document.getElementById('b');
+ b.type = "reset";
+ ok(!b.willValidate, "Reset state button should be barred from constraint validation");
+ is(window.getComputedStyle(b).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ b.type = "button";
+ ok(!b.willValidate, "Button state button should be barred from constraint validation");
+ is(window.getComputedStyle(b).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ b.type = "submit";
+ ok(b.willValidate, "Submit state button should not be barred from constraint validation");
+ is(window.getComputedStyle(b).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid and :invalid should apply");
+ b.type = "";
+ ok(b.willValidate, "Default button element should not be barred from constraint validation");
+ is(window.getComputedStyle(b).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+
+ // textarea element
+ t = document.getElementById('t');
+ t.readOnly = true;
+ ok(!t.willValidate, "Readonly textarea should be barred from constraint validation");
+ is(window.getComputedStyle(t).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+ t.removeAttribute('readOnly');
+ ok(t.willValidate, "Default textarea element should not be barred from constraint validation");
+ is(window.getComputedStyle(t).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+
+ // TODO: PROGRESS
+ // TODO: METER
+}
+
+function checkCommonWillValidate(element)
+{
+ // Not checking the default value because it has been checked previously.
+
+ element.disabled = true;
+ ok(!element.willValidate, "Disabled element should be barred from constraint validation");
+
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 0, 0)", "Nor :valid and :invalid should apply");
+
+ element.removeAttribute('disabled');
+
+ // TODO: If an element has a datalist element ancestor, it is barred from constraint validation.
+}
+
+function checkCustomError(element, isBarred)
+{
+ element.setCustomValidity("message");
+ if (!isBarred) {
+ is(element.validationMessage, "message",
+ "When the element has a custom validity message, validation message should return it");
+ } else {
+ is(element.validationMessage, "",
+ "An element barred from constraint validation can't have a validation message");
+ }
+ ok(element.validity.customError, "The element should suffer from a custom error");
+ ok(!element.validity.valid, "The element should not be valid with a custom error");
+
+ if (element.tagName == "FIELDSET") {
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ isBarred ? "rgb(0, 255, 0)" : "rgb(255, 0, 0)",
+ ":invalid pseudo-classs should apply to " + element.tagName);
+ }
+ else {
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ isBarred ? "rgb(0, 0, 0)" : "rgb(255, 0, 0)",
+ ":invalid pseudo-classs should apply to " + element.tagName);
+ }
+
+ element.setCustomValidity("");
+ is(element.validationMessage, "", "The element should not have a validation message when reseted");
+ ok(!element.validity.customError, "The element should not suffer anymore from a custom error");
+ ok(element.validity.valid, "The element should now be valid");
+
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ isBarred && element.tagName != "FIELDSET" ? "rgb(0, 0, 0)" : "rgb(0, 255, 0)",
+ ":valid pseudo-classs should apply");
+}
+
+function checkCheckValidity(element)
+{
+ element.setCustomValidity("message");
+ ok(!element.checkValidity(), "checkValidity() should return false when the element is not valid");
+
+ ok(gInvalid, "Invalid event should have been handled");
+
+ gInvalid = false;
+ element.setCustomValidity("");
+
+ ok(element.checkValidity(), "Element should be valid");
+ ok(!gInvalid, "Invalid event should not have been handled");
+}
+
+function checkValidityStateObjectAliveWithoutElement(element)
+{
+ // We are creating a temporary element and getting it's ValidityState object.
+ // Then, we make sure it is removed by the garbage collector and we check the
+ // ValidityState default values (it should not crash).
+
+ var v = document.createElement(element).validity;
+ SpecialPowers.gc();
+
+ ok(!v.valueMissing,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.typeMismatch,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.badInput,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.patternMismatch,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.tooLong,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.rangeUnderflow,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.rangeOverflow,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.stepMismatch,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(!v.customError,
+ "When the element is not alive, it shouldn't suffer from constraint validation");
+ ok(v.valid, "When the element is not alive, it should be valid");
+}
+
+checkConstraintValidationAPIExist(document.getElementById('f'));
+checkConstraintValidationAPIExist(document.getElementById('i'));
+checkConstraintValidationAPIExist(document.getElementById('b'));
+checkConstraintValidationAPIExist(document.getElementById('s'));
+checkConstraintValidationAPIExist(document.getElementById('t'));
+checkConstraintValidationAPIExist(document.getElementById('o'));
+checkConstraintValidationAPIExist(document.getElementById('obj'));
+
+checkConstraintValidationAPIDefaultValues(document.getElementById('f'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('i'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('b'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('s'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('t'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('o'));
+checkConstraintValidationAPIDefaultValues(document.getElementById('obj'));
+
+checkDefaultPseudoClass();
+
+checkSpecificWillValidate();
+
+// Not checking button, fieldset, output and object
+// because they are always barred from constraint validation.
+checkCommonWillValidate(document.getElementById('i'));
+checkCommonWillValidate(document.getElementById('s'));
+checkCommonWillValidate(document.getElementById('t'));
+
+checkCustomError(document.getElementById('i'), false);
+checkCustomError(document.getElementById('s'), false);
+checkCustomError(document.getElementById('t'), false);
+checkCustomError(document.getElementById('o'), true);
+checkCustomError(document.getElementById('b'), false);
+checkCustomError(document.getElementById('f'), true);
+checkCustomError(document.getElementById('obj'), true);
+
+// Not checking button, fieldset, output and object
+// because they are always barred from constraint validation.
+checkCheckValidity(document.getElementById('i'));
+checkCheckValidity(document.getElementById('s'));
+checkCheckValidity(document.getElementById('t'));
+
+checkValidityStateObjectAliveWithoutElement("fieldset");
+checkValidityStateObjectAliveWithoutElement("input");
+checkValidityStateObjectAliveWithoutElement("button");
+checkValidityStateObjectAliveWithoutElement("select");
+checkValidityStateObjectAliveWithoutElement("textarea");
+checkValidityStateObjectAliveWithoutElement("output");
+checkValidityStateObjectAliveWithoutElement("object");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_validation_not_in_doc.html b/dom/html/test/forms/test_validation_not_in_doc.html
new file mode 100644
index 0000000000..1500c60869
--- /dev/null
+++ b/dom/html/test/forms/test_validation_not_in_doc.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for constraint validation of form controls not in documents</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ var input = document.createElement('input');
+ input.required = true;
+ assert_false(input.checkValidity());
+}, "Should validate input not in document");
+
+test(function() {
+ var textarea = document.createElement('textarea');
+ textarea.required = true;
+ assert_false(textarea.checkValidity());
+}, "Should validate textarea not in document");
+</script>
diff --git a/dom/html/test/forms/test_valueasdate_attribute.html b/dom/html/test/forms/test_valueasdate_attribute.html
new file mode 100644
index 0000000000..9055879a85
--- /dev/null
+++ b/dom/html/test/forms/test_valueasdate_attribute.html
@@ -0,0 +1,751 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=769370
+-->
+<head>
+ <title>Test for input.valueAsDate</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=769370">Mozilla Bug 769370</a>
+<iframe name="testFrame" style="display: none"></iframe>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 769370**/
+
+/**
+ * This test is checking .valueAsDate.
+ */
+
+var element = document.createElement("input");
+
+var validTypes =
+[
+ ["text", false],
+ ["password", false],
+ ["search", false],
+ ["tel", false],
+ ["email", false],
+ ["url", false],
+ ["hidden", false],
+ ["checkbox", false],
+ ["radio", false],
+ ["file", false],
+ ["submit", false],
+ ["image", false],
+ ["reset", false],
+ ["button", false],
+ ["number", false],
+ ["range", false],
+ ["date", true],
+ ["time", true],
+ ["color", false],
+ ["month", true],
+ ["week", true],
+ ["datetime-local", true],
+];
+
+function checkAvailability()
+{
+ for (let data of validTypes) {
+ var exceptionCatched = false;
+ element.type = data[0];
+ try {
+ element.valueAsDate;
+ } catch (e) {
+ exceptionCatched = true;
+ }
+ is(exceptionCatched, false,
+ "valueAsDate shouldn't throw exception on getting");
+
+ exceptionCatched = false;
+ try {
+ element.valueAsDate = new Date();
+ } catch (e) {
+ exceptionCatched = true;
+ }
+ is(exceptionCatched, !data[1], "valueAsDate for " + data[0] +
+ " availability is not correct");
+ }
+}
+
+function checkGarbageValues()
+{
+ for (let type of validTypes) {
+ if (!type[1]) {
+ continue;
+ }
+ type = type[0];
+
+ var inputElement = document.createElement('input');
+ inputElement.type = type;
+
+ inputElement.value = "test";
+ inputElement.valueAsDate = null;
+ is(inputElement.value, "", "valueAsDate should set the value to the empty string");
+
+ inputElement.value = "test";
+ inputElement.valueAsDate = undefined;
+ is(inputElement.value, "", "valueAsDate should set the value to the empty string");
+
+ inputElement.value = "test";
+ inputElement.valueAsDate = new Date(NaN);
+ is(inputElement.value, "", "valueAsDate should set the value to the empty string");
+
+ var illegalValues = [
+ "foobar", 42, {}, function() { return 42; }, function() { return Date(); }
+ ];
+
+ for (let value of illegalValues) {
+ try {
+ var caught = false;
+ inputElement.valueAsDate = value;
+ } catch(e) {
+ is(e.name, "TypeError", "Exception should be 'TypeError'.");
+ caught = true;
+ }
+ ok(caught, "Assigning " + value + " to .valueAsDate should throw");
+ }
+ }
+}
+
+function checkDateGet()
+{
+ var validData =
+ [
+ [ "2012-07-12", 1342051200000 ],
+ [ "1970-01-01", 0 ],
+ [ "1970-01-02", 86400000 ],
+ [ "1969-12-31", -86400000 ],
+ [ "0311-01-31", -52350451200000 ],
+ [ "275760-09-13", 8640000000000000 ],
+ [ "0001-01-01", -62135596800000 ],
+ [ "2012-02-29", 1330473600000 ],
+ [ "2011-02-28", 1298851200000 ],
+ ];
+
+ var invalidData =
+ [
+ [ "invaliddate" ],
+ [ "-001-12-31" ],
+ [ "901-12-31" ],
+ [ "1901-13-31" ],
+ [ "1901-12-32" ],
+ [ "1901-00-12" ],
+ [ "1901-01-00" ],
+ [ "1900-02-29" ],
+ [ "0000-01-01" ],
+ [ "" ],
+ // This date is valid for the input element, but is out of
+ // the date object range. In this case, on getting valueAsDate,
+ // a Date object will be created, but it will have a NaN internal value,
+ // and will return the string "Invalid Date".
+ [ "275760-09-14", true ],
+ ];
+
+ element.type = "date";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsDate.valueOf(), data[1],
+ "valueAsDate should return the " +
+ "valid date object representing this date");
+ }
+
+ for (let data of invalidData) {
+ element.value = data[0];
+ if (data[1]) {
+ is(String(element.valueAsDate), "Invalid Date",
+ "valueAsDate should return an invalid Date object " +
+ "when the element value is not a valid date");
+ } else {
+ is(element.valueAsDate, null,
+ "valueAsDate should return null " +
+ "when the element value is not a valid date");
+ }
+ }
+}
+
+function checkDateSet()
+{
+ var testData =
+ [
+ [ 1342051200000, "2012-07-12" ],
+ [ 0, "1970-01-01" ],
+ // Maximum valid date (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09-13" ],
+ // Minimum valid date (limited by the input element minimum valid value).
+ [ -62135596800000 , "0001-01-01" ],
+ [ 1330473600000, "2012-02-29" ],
+ [ 1298851200000, "2011-02-28" ],
+ // "Values must be truncated to valid dates"
+ [ 42.1234, "1970-01-01" ],
+ [ 123.123456789123, "1970-01-01" ],
+ [ 1e-1, "1970-01-01" ],
+ [ 1298851200010, "2011-02-28" ],
+ [ -1, "1969-12-31" ],
+ [ -86400000, "1969-12-31" ],
+ [ 86400000, "1970-01-02" ],
+ // Negative years, this is out of range for the input element,
+ // the corresponding date string is the empty string
+ [ -62135596800001, "" ],
+ // Invalid dates.
+ ];
+
+ element.type = "date";
+ for (let data of testData) {
+ element.valueAsDate = new Date(data[0]);
+ is(element.value, data[1], "valueAsDate should set the value to "
+ + data[1]);
+ element.valueAsDate = new testFrame.Date(data[0]);
+ is(element.value, data[1], "valueAsDate with other-global date should " +
+ "set the value to " + data[1]);
+ }
+}
+
+function checkTimeGet()
+{
+ var tests = [
+ // Some invalid values to begin.
+ { value: "", result: null },
+ { value: "foobar", result: null },
+ { value: "00:", result: null },
+ { value: "24:00", result: null },
+ { value: "00:99", result: null },
+ { value: "00:00:", result: null },
+ { value: "00:00:99", result: null },
+ { value: "00:00:00:", result: null },
+ { value: "00:00:00.", result: null },
+ { value: "00:00:00.0000", result: null },
+ // Some simple valid values.
+ { value: "00:00", result: { time: 0, hours: 0, minutes: 0, seconds: 0, ms: 0 } },
+ { value: "00:01", result: { time: 60000, hours: 0, minutes: 1, seconds: 0, ms: 0 } },
+ { value: "01:00", result: { time: 3600000, hours: 1, minutes: 0, seconds: 0, ms: 0 } },
+ { value: "01:01", result: { time: 3660000, hours: 1, minutes: 1, seconds: 0, ms: 0 } },
+ { value: "13:37", result: { time: 49020000, hours: 13, minutes: 37, seconds: 0, ms: 0 } },
+ // Valid values including seconds.
+ { value: "00:00:01", result: { time: 1000, hours: 0, minutes: 0, seconds: 1, ms: 0 } },
+ { value: "13:37:42", result: { time: 49062000, hours: 13, minutes: 37, seconds: 42, ms: 0 } },
+ // Valid values including seconds fractions.
+ { value: "00:00:00.001", result: { time: 1, hours: 0, minutes: 0, seconds: 0, ms: 1 } },
+ { value: "00:00:00.123", result: { time: 123, hours: 0, minutes: 0, seconds: 0, ms: 123 } },
+ { value: "00:00:00.100", result: { time: 100, hours: 0, minutes: 0, seconds: 0, ms: 100 } },
+ { value: "00:00:00.000", result: { time: 0, hours: 0, minutes: 0, seconds: 0, ms: 0 } },
+ { value: "20:17:31.142", result: { time: 73051142, hours: 20, minutes: 17, seconds: 31, ms: 142 } },
+ // Highest possible value.
+ { value: "23:59:59.999", result: { time: 86399999, hours: 23, minutes: 59, seconds: 59, ms: 999 } },
+ // Some values with one or two digits for the fraction of seconds.
+ { value: "00:00:00.1", result: { time: 100, hours: 0, minutes: 0, seconds: 0, ms: 100 } },
+ { value: "00:00:00.14", result: { time: 140, hours: 0, minutes: 0, seconds: 0, ms: 140 } },
+ { value: "13:37:42.7", result: { time: 49062700, hours: 13, minutes: 37, seconds: 42, ms: 700 } },
+ { value: "23:31:12.23", result: { time: 84672230, hours: 23, minutes: 31, seconds: 12, ms: 230 } },
+ ];
+
+ var inputElement = document.createElement('input');
+ inputElement.type = 'time';
+
+ for (let test of tests) {
+ inputElement.value = test.value;
+ if (test.result === null) {
+ is(inputElement.valueAsDate, null, "element.valueAsDate should return null");
+ } else {
+ var date = inputElement.valueAsDate;
+ isnot(date, null, "element.valueAsDate should not be null");
+
+ is(date.getTime(), test.result.time);
+ is(date.getUTCHours(), test.result.hours);
+ is(date.getUTCMinutes(), test.result.minutes);
+ is(date.getUTCSeconds(), test.result.seconds);
+ is(date.getUTCMilliseconds(), test.result.ms);
+ }
+ }
+}
+
+function checkTimeSet()
+{
+ var tests = [
+ // Simple tests.
+ { value: 0, result: "00:00" },
+ { value: 1, result: "00:00:00.001" },
+ { value: 100, result: "00:00:00.100" },
+ { value: 1000, result: "00:00:01" },
+ { value: 60000, result: "00:01" },
+ { value: 3600000, result: "01:00" },
+ { value: 83622234, result: "23:13:42.234" },
+ // Some edge cases.
+ { value: 86400000, result: "00:00" },
+ { value: 86400001, result: "00:00:00.001" },
+ { value: 170022234, result: "23:13:42.234" },
+ { value: 432000000, result: "00:00" },
+ { value: -1, result: "23:59:59.999" },
+ { value: -86400000, result: "00:00" },
+ { value: -86400001, result: "23:59:59.999" },
+ { value: -56789, result: "23:59:03.211" },
+ { value: 0.9, result: "00:00" },
+ ];
+
+ var inputElement = document.createElement('input');
+ inputElement.type = 'time';
+
+ for (let test of tests) {
+ inputElement.valueAsDate = new Date(test.value);
+ is(inputElement.value, test.result,
+ "element.value should have been changed by setting valueAsDate");
+ }
+}
+
+function checkWithBustedPrototype()
+{
+ for (let type of validTypes) {
+ if (!type[1]) {
+ continue;
+ }
+
+ type = type[0];
+
+ var inputElement = document.createElement('input');
+ inputElement.type = type;
+
+ var backupPrototype = {};
+ backupPrototype.getUTCFullYear = Date.prototype.getUTCFullYear;
+ backupPrototype.getUTCMonth = Date.prototype.getUTCMonth;
+ backupPrototype.getUTCDate = Date.prototype.getUTCDate;
+ backupPrototype.getTime = Date.prototype.getTime;
+ backupPrototype.setUTCFullYear = Date.prototype.setUTCFullYear;
+
+ Date.prototype.getUTCFullYear = function() { return {}; };
+ Date.prototype.getUTCMonth = function() { return {}; };
+ Date.prototype.getUTCDate = function() { return {}; };
+ Date.prototype.getTime = function() { return {}; };
+ Date.prototype.setUTCFullYear = function(y,m,d) { };
+
+ inputElement.valueAsDate = new Date();
+
+ isnot(inputElement.valueAsDate, null, ".valueAsDate should not return null");
+ // The object returned by element.valueAsDate should return a Date object
+ // with the same prototype:
+ is(inputElement.valueAsDate.getUTCFullYear, Date.prototype.getUTCFullYear,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getUTCMonth, Date.prototype.getUTCMonth,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getUTCDate, Date.prototype.getUTCDate,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getTime, Date.prototype.getTime,
+ "prototype is the same");
+ is(inputElement.valueAsDate.setUTCFullYear, Date.prototype.setUTCFullYear,
+ "prototype is the same");
+
+ // However the Date should have the correct information.
+ // Skip type=month for now, since .valueAsNumber returns number of months
+ // and not milliseconds.
+ if (type != "month") {
+ var witnessDate = new Date(inputElement.valueAsNumber);
+ is(inputElement.valueAsDate.valueOf(), witnessDate.valueOf(), "correct Date");
+ }
+
+ // Same test as above but using NaN instead of {}.
+
+ Date.prototype.getUTCFullYear = function() { return NaN; };
+ Date.prototype.getUTCMonth = function() { return NaN; };
+ Date.prototype.getUTCDate = function() { return NaN; };
+ Date.prototype.getTime = function() { return NaN; };
+ Date.prototype.setUTCFullYear = function(y,m,d) { };
+
+ inputElement.valueAsDate = new Date();
+
+ isnot(inputElement.valueAsDate, null, ".valueAsDate should not return null");
+ // The object returned by element.valueAsDate should return a Date object
+ // with the same prototype:
+ is(inputElement.valueAsDate.getUTCFullYear, Date.prototype.getUTCFullYear,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getUTCMonth, Date.prototype.getUTCMonth,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getUTCDate, Date.prototype.getUTCDate,
+ "prototype is the same");
+ is(inputElement.valueAsDate.getTime, Date.prototype.getTime,
+ "prototype is the same");
+ is(inputElement.valueAsDate.setUTCFullYear, Date.prototype.setUTCFullYear,
+ "prototype is the same");
+
+ // However the Date should have the correct information.
+ // Skip type=month for now, since .valueAsNumber returns number of months
+ // and not milliseconds.
+ if (type != "month") {
+ var witnessDate = new Date(inputElement.valueAsNumber);
+ is(inputElement.valueAsDate.valueOf(), witnessDate.valueOf(), "correct Date");
+ }
+
+ Date.prototype.getUTCFullYear = backupPrototype.getUTCFullYear;
+ Date.prototype.getUTCMonth = backupPrototype.getUTCMonth;
+ Date.prototype.getUTCDate = backupPrototype.getUTCDate;
+ Date.prototype.getTime = backupPrototype.getTime;
+ Date.prototype.setUTCFullYear = backupPrototype.setUTCFullYear;
+ }
+}
+
+function checkMonthGet()
+{
+ var validData =
+ [
+ [ "2016-07", 1467331200000 ],
+ [ "1970-01", 0 ],
+ [ "1970-02", 2678400000 ],
+ [ "1969-12", -2678400000 ],
+ [ "0001-01", -62135596800000 ],
+ [ "275760-09", 8639998963200000 ],
+ ];
+
+ var invalidData =
+ [
+ [ "invalidmonth" ],
+ [ "0000-01" ],
+ [ "2016-00" ],
+ [ "123-01" ],
+ [ "2017-13" ],
+ [ "" ],
+ // This month is valid for the input element, but is out of
+ // the date object range. In this case, on getting valueAsDate,
+ // a Date object will be created, but it will have a NaN internal value,
+ // and will return the string "Invalid Date".
+ [ "275760-10", true ],
+ ];
+
+ element.type = "month";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsDate.valueOf(), data[1],
+ "valueAsDate should return the " +
+ "valid date object representing this month");
+ }
+
+ for (let data of invalidData) {
+ element.value = data[0];
+ if (data[1]) {
+ is(String(element.valueAsDate), "Invalid Date",
+ "valueAsDate should return an invalid Date object " +
+ "when the element value is not a valid month");
+ } else {
+ is(element.valueAsDate, null,
+ "valueAsDate should return null " +
+ "when the element value is not a valid month");
+ }
+ }
+}
+
+function checkMonthSet()
+{
+ var testData =
+ [
+ [ 1342051200000, "2012-07" ],
+ [ 0, "1970-01" ],
+ // Maximum valid month (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09" ],
+ // Minimum valid month (limited by the input element minimum valid value).
+ [ -62135596800000 , "0001-01" ],
+ [ 1330473600000, "2012-02" ],
+ [ 1298851200000, "2011-02" ],
+ // "Values must be truncated to valid months"
+ [ 42.1234, "1970-01" ],
+ [ 123.123456789123, "1970-01" ],
+ [ 1e-1, "1970-01" ],
+ [ 1298851200010, "2011-02" ],
+ [ -1, "1969-12" ],
+ [ -86400000, "1969-12" ],
+ [ 86400000, "1970-01" ],
+ // Negative years, this is out of range for the input element,
+ // the corresponding month string is the empty string
+ [ -62135596800001, "" ],
+ ];
+
+ element.type = "month";
+ for (let data of testData) {
+ element.valueAsDate = new Date(data[0]);
+ is(element.value, data[1], "valueAsDate should set the value to "
+ + data[1]);
+ element.valueAsDate = new testFrame.Date(data[0]);
+ is(element.value, data[1], "valueAsDate with other-global date should " +
+ "set the value to " + data[1]);
+ }
+}
+
+function checkWeekGet()
+{
+ var validData =
+ [
+ // Common years starting on different days of week.
+ [ "2007-W01", Date.UTC(2007, 0, 1) ], // Mon
+ [ "2013-W01", Date.UTC(2012, 11, 31) ], // Tue
+ [ "2014-W01", Date.UTC(2013, 11, 30) ], // Wed
+ [ "2015-W01", Date.UTC(2014, 11, 29) ], // Thu
+ [ "2010-W01", Date.UTC(2010, 0, 4) ], // Fri
+ [ "2011-W01", Date.UTC(2011, 0, 3) ], // Sat
+ [ "2017-W01", Date.UTC(2017, 0, 2) ], // Sun
+ // Common years ending on different days of week.
+ [ "2007-W52", Date.UTC(2007, 11, 24) ], // Mon
+ [ "2013-W52", Date.UTC(2013, 11, 23) ], // Tue
+ [ "2014-W52", Date.UTC(2014, 11, 22) ], // Wed
+ [ "2015-W53", Date.UTC(2015, 11, 28) ], // Thu
+ [ "2010-W52", Date.UTC(2010, 11, 27) ], // Fri
+ [ "2011-W52", Date.UTC(2011, 11, 26) ], // Sat
+ [ "2017-W52", Date.UTC(2017, 11, 25) ], // Sun
+ // Leap years starting on different days of week.
+ [ "1996-W01", Date.UTC(1996, 0, 1) ], // Mon
+ [ "2008-W01", Date.UTC(2007, 11, 31) ], // Tue
+ [ "2020-W01", Date.UTC(2019, 11, 30) ], // Wed
+ [ "2004-W01", Date.UTC(2003, 11, 29) ], // Thu
+ [ "2016-W01", Date.UTC(2016, 0, 4) ], // Fri
+ [ "2000-W01", Date.UTC(2000, 0, 3) ], // Sat
+ [ "2012-W01", Date.UTC(2012, 0, 2) ], // Sun
+ // Leap years ending on different days of week.
+ [ "2012-W52", Date.UTC(2012, 11, 24) ], // Mon
+ [ "2024-W52", Date.UTC(2024, 11, 23) ], // Tue
+ [ "1980-W52", Date.UTC(1980, 11, 22) ], // Wed
+ [ "1992-W53", Date.UTC(1992, 11, 28) ], // Thu
+ [ "2004-W53", Date.UTC(2004, 11, 27) ], // Fri
+ [ "1988-W52", Date.UTC(1988, 11, 26) ], // Sat
+ [ "2000-W52", Date.UTC(2000, 11, 25) ], // Sun
+ // Other normal cases.
+ [ "2016-W36", 1473033600000 ],
+ [ "1969-W52", -864000000 ],
+ [ "1970-W01", -259200000 ],
+ [ "275760-W37", 8639999568000000 ],
+ ];
+
+ var invalidData =
+ [
+ [ "invalidweek" ],
+ [ "0000-W01" ],
+ [ "2016-W00" ],
+ [ "123-W01" ],
+ [ "2016-W53" ],
+ [ "" ],
+ // This week is valid for the input element, but is out of
+ // the date object range. In this case, on getting valueAsDate,
+ // a Date object will be created, but it will have a NaN internal value,
+ // and will return the string "Invalid Date".
+ [ "275760-W38", true ],
+ ];
+
+ element.type = "week";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsDate.valueOf(), data[1],
+ "valueAsDate should return the " +
+ "valid date object representing this week");
+ }
+
+ for (let data of invalidData) {
+ element.value = data[0];
+ if (data[1]) {
+ is(String(element.valueAsDate), "Invalid Date",
+ "valueAsDate should return an invalid Date object " +
+ "when the element value is not a valid week");
+ } else {
+ is(element.valueAsDate, null,
+ "valueAsDate should return null " +
+ "when the element value is not a valid week");
+ }
+ }
+}
+
+function checkWeekSet()
+{
+ var testData =
+ [
+ // Common years starting on different days of week.
+ [ Date.UTC(2007, 0, 1), "2007-W01" ], // Mon
+ [ Date.UTC(2013, 0, 1), "2013-W01" ], // Tue
+ [ Date.UTC(2014, 0, 1), "2014-W01" ], // Wed
+ [ Date.UTC(2015, 0, 1), "2015-W01" ], // Thu
+ [ Date.UTC(2010, 0, 1), "2009-W53" ], // Fri
+ [ Date.UTC(2011, 0, 1), "2010-W52" ], // Sat
+ [ Date.UTC(2017, 0, 1), "2016-W52" ], // Sun
+ // Common years ending on different days of week.
+ [ Date.UTC(2007, 11, 31), "2008-W01" ], // Mon
+ [ Date.UTC(2013, 11, 31), "2014-W01" ], // Tue
+ [ Date.UTC(2014, 11, 31), "2015-W01" ], // Wed
+ [ Date.UTC(2015, 11, 31), "2015-W53" ], // Thu
+ [ Date.UTC(2010, 11, 31), "2010-W52" ], // Fri
+ [ Date.UTC(2011, 11, 31), "2011-W52" ], // Sat
+ [ Date.UTC(2017, 11, 31), "2017-W52" ], // Sun
+ // Leap years starting on different days of week.
+ [ Date.UTC(1996, 0, 1), "1996-W01" ], // Mon
+ [ Date.UTC(2008, 0, 1), "2008-W01" ], // Tue
+ [ Date.UTC(2020, 0, 1), "2020-W01" ], // Wed
+ [ Date.UTC(2004, 0, 1), "2004-W01" ], // Thu
+ [ Date.UTC(2016, 0, 1), "2015-W53" ], // Fri
+ [ Date.UTC(2000, 0, 1), "1999-W52" ], // Sat
+ [ Date.UTC(2012, 0, 1), "2011-W52" ], // Sun
+ // Leap years ending on different days of week.
+ [ Date.UTC(2012, 11, 31), "2013-W01" ], // Mon
+ [ Date.UTC(2024, 11, 31), "2025-W01" ], // Tue
+ [ Date.UTC(1980, 11, 31), "1981-W01" ], // Wed
+ [ Date.UTC(1992, 11, 31), "1992-W53" ], // Thu
+ [ Date.UTC(2004, 11, 31), "2004-W53" ], // Fri
+ [ Date.UTC(1988, 11, 31), "1988-W52" ], // Sat
+ [ Date.UTC(2000, 11, 31), "2000-W52" ], // Sun
+ // Other normal cases.
+ [ Date.UTC(2016, 8, 9), "2016-W36" ],
+ [ Date.UTC(2010, 0, 3), "2009-W53" ],
+ [ Date.UTC(2010, 0, 4), "2010-W01" ],
+ [ Date.UTC(2010, 0, 10), "2010-W01" ],
+ [ Date.UTC(2010, 0, 11), "2010-W02" ],
+ [ 0, "1970-W01" ],
+ // Maximum valid month (limited by the ecma date object range).
+ [ 8640000000000000, "275760-W37" ],
+ // Minimum valid month (limited by the input element minimum valid value).
+ [ -62135596800000 , "0001-W01" ],
+ // "Values must be truncated to valid week"
+ [ 42.1234, "1970-W01" ],
+ [ 123.123456789123, "1970-W01" ],
+ [ 1e-1, "1970-W01" ],
+ [ -1.1, "1970-W01" ],
+ [ -345600000, "1969-W52" ],
+ // Negative years, this is out of range for the input element,
+ // the corresponding week string is the empty string
+ [ -62135596800001, "" ],
+ ];
+
+ element.type = "week";
+ for (let data of testData) {
+ element.valueAsDate = new Date(data[0]);
+ is(element.value, data[1], "valueAsDate should set the value to "
+ + data[1]);
+ element.valueAsDate = new testFrame.Date(data[0]);
+ is(element.value, data[1], "valueAsDate with other-global date should " +
+ "set the value to " + data[1]);
+ }
+}
+
+function checkDatetimeLocalGet()
+{
+ var validData =
+ [
+ // Simple cases.
+ [ "2016-12-27T10:30", Date.UTC(2016, 11, 27, 10, 30, 0) ],
+ [ "2016-12-27T10:30:40", Date.UTC(2016, 11, 27, 10, 30, 40) ],
+ [ "2016-12-27T10:30:40.567", Date.UTC(2016, 11, 27, 10, 30, 40, 567) ],
+ [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ],
+ [ "1970-01-01T00:00", 0 ],
+ // Leap years.
+ [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ],
+ [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ],
+ [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ],
+ [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ],
+ [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ],
+ // Maximum valid datetime-local (limited by the ecma date object range).
+ [ "275760-09-13T00:00", 8640000000000000 ],
+ // Minimum valid datetime-local (limited by the input element minimum valid value).
+ [ "0001-01-01T00:00", -62135596800000 ],
+ ];
+
+ var invalidData =
+ [
+ [ "invaliddateime-local" ],
+ [ "0000-01-01T00:00" ],
+ [ "2016-12-25T00:00Z" ],
+ [ "2015-02-29T12:34" ],
+ [ "1-1-1T12:00" ],
+ [ "" ],
+ // This datetime-local is valid for the input element, but is out of the
+ // date object range. In this case, on getting valueAsDate, a Date object
+ // will be created, but it will have a NaN internal value, and will return
+ // the string "Invalid Date".
+ [ "275760-09-13T12:00", true ],
+ ];
+
+ element.type = "datetime-local";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsDate.valueOf(), data[1],
+ "valueAsDate should return the " +
+ "valid date object representing this datetime-local");
+ }
+
+ for (let data of invalidData) {
+ element.value = data[0];
+ if (data[1]) {
+ is(String(element.valueAsDate), "Invalid Date",
+ "valueAsDate should return an invalid Date object " +
+ "when the element value is not a valid datetime-local");
+ } else {
+ is(element.valueAsDate, null,
+ "valueAsDate should return null " +
+ "when the element value is not a valid datetime-local");
+ }
+ }
+}
+
+function checkDatetimeLocalSet()
+{
+ var testData =
+ [
+ // Simple cases.
+ [ Date.UTC(2016, 11, 27, 10, 30, 0), "2016-12-27T10:30" ],
+ [ Date.UTC(2016, 11, 27, 10, 30, 30), "2016-12-27T10:30:30" ],
+ [ Date.UTC(1999, 11, 31, 23, 59, 59), "1999-12-31T23:59:59" ],
+ [ Date.UTC(1999, 11, 31, 23, 59, 59, 999), "1999-12-31T23:59:59.999" ],
+ [ Date.UTC(123456, 7, 8, 9, 10), "123456-08-08T09:10" ],
+ [ 0, "1970-01-01T00:00" ],
+ // Maximum valid datetime-local (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09-13T00:00" ],
+ // Minimum valid datetime-local (limited by the input element minimum valid value).
+ [ -62135596800000, "0001-01-01T00:00" ],
+ // Leap years.
+ [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ],
+ [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ],
+ [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ],
+ [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ],
+ [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ],
+ // "Values must be truncated to valid datetime-local"
+ [ 123.123456789123, "1970-01-01T00:00:00.123" ],
+ [ 1e-1, "1970-01-01T00:00" ],
+ [ -1.1, "1969-12-31T23:59:59.999" ],
+ [ -345600000, "1969-12-28T00:00" ],
+ // Negative years, this is out of range for the input element,
+ // the corresponding datetime-local string is the empty string
+ [ -62135596800001, "" ],
+ ];
+
+ element.type = "datetime-local";
+ for (let data of testData) {
+ element.valueAsDate = new Date(data[0]);
+ is(element.value, data[1], "valueAsDate should set the value to " +
+ data[1]);
+ element.valueAsDate = new testFrame.Date(data[0]);
+ is(element.value, data[1], "valueAsDate with other-global date should " +
+ "set the value to " + data[1]);
+ }
+}
+
+checkAvailability();
+checkGarbageValues();
+checkWithBustedPrototype();
+
+// Test <input type='date'>.
+checkDateGet();
+checkDateSet();
+
+// Test <input type='time'>.
+checkTimeGet();
+checkTimeSet();
+
+// Test <input type='month'>.
+checkMonthGet();
+checkMonthSet();
+
+// Test <input type='week'>.
+checkWeekGet();
+checkWeekSet();
+
+// Test <input type='datetime-local'>.
+checkDatetimeLocalGet();
+checkDatetimeLocalSet();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_valueasnumber_attribute.html b/dom/html/test/forms/test_valueasnumber_attribute.html
new file mode 100644
index 0000000000..5f7537f7a8
--- /dev/null
+++ b/dom/html/test/forms/test_valueasnumber_attribute.html
@@ -0,0 +1,858 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=636737
+-->
+<head>
+ <title>Test for Bug input.valueAsNumber</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=636737">Mozilla Bug 636737</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 636737 **/
+
+/**
+ * This test is checking .valueAsNumber.
+ */
+
+function checkAvailability()
+{
+ var testData =
+ [
+ ["text", false],
+ ["password", false],
+ ["search", false],
+ ["tel", false],
+ ["email", false],
+ ["url", false],
+ ["hidden", false],
+ ["checkbox", false],
+ ["radio", false],
+ ["file", false],
+ ["submit", false],
+ ["image", false],
+ ["reset", false],
+ ["button", false],
+ ["number", true],
+ ["range", true],
+ ["date", true],
+ ["time", true],
+ ["color", false],
+ ["month", true],
+ ["week", true],
+ ["datetime-local", true],
+ ];
+
+ var element = document.createElement('input');
+
+ for (let data of testData) {
+ var exceptionCatched = false;
+ element.type = data[0];
+ try {
+ element.valueAsNumber;
+ } catch (e) {
+ exceptionCatched = true;
+ }
+ is(exceptionCatched, false,
+ "valueAsNumber shouldn't throw exception on getting");
+
+ exceptionCatched = false;
+ try {
+ element.valueAsNumber = 42;
+ } catch (e) {
+ exceptionCatched = true;
+ }
+ is(exceptionCatched, !data[1], "valueAsNumber for " + data[0] +
+ " availability is not correct");
+ }
+}
+
+function checkNumberGet()
+{
+ var testData =
+ [
+ ["42", 42],
+ ["-42", -42], // should work for negative values
+ ["42.1234", 42.1234],
+ ["123.123456789123", 123.123456789123], // double precision
+ ["1e2", 100], // e should be usable
+ ["2e1", 20],
+ ["1e-1", 0.1], // value after e can be negative
+ ["1E2", 100], // E can be used instead of e
+ ["e", null],
+ ["e2", null],
+ ["1e0.1", null],
+ ["", null], // the empty string is not a number
+ ["foo", null],
+ ["42,13", null], // comma can't be used as a decimal separator
+ ];
+
+ var element = document.createElement('input');
+ element.type = "number";
+ for (let data of testData) {
+ element.value = data[0];
+
+ // Given that NaN != NaN, we have to use null when the expected value is NaN.
+ if (data[1] != null) {
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "floating point representation of the value");
+ } else {
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a number");
+ }
+ }
+}
+
+function checkNumberSet()
+{
+ var testData =
+ [
+ [42, "42"],
+ [-42, "-42"], // should work for negative values
+ [42.1234, "42.1234"],
+ [123.123456789123, "123.123456789123"], // double precision
+ [1e2, "100"], // e should be usable
+ [2e1, "20"],
+ [1e-1, "0.1"], // value after e can be negative
+ [1E2, "100"], // E can be used instead of e
+ // Setting a string will set NaN.
+ ["foo", ""],
+ // "" is converted to 0.
+ ["", "0"],
+ [42, "42"], // Keep this here, it is used by the next test.
+ // Setting Infinity should throw and not change the current value.
+ [Infinity, "42", true],
+ [-Infinity, "42", true],
+ // Setting NaN should change the value to the empty string.
+ [NaN, ""],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "number";
+ for (let data of testData) {
+ var caught = false;
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1],
+ "valueAsNumber should be able to set the value");
+ } catch (e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+function checkRangeGet()
+{
+ // For type=range we should never get NaN since the user agent is required
+ // to fix up the input's value to be something sensible.
+
+ var min = -200;
+ var max = 200;
+ var defaultValue = min + (max - min)/2;
+
+ var testData =
+ [
+ ["42", 42],
+ ["-42", -42], // should work for negative values
+ ["42.1234", 42.1234],
+ ["123.123456789123", 123.123456789123], // double precision
+ ["1e2", 100], // e should be usable
+ ["2e1", 20],
+ ["1e-1", 0.1], // value after e can be negative
+ ["1E2", 100], // E can be used instead of e
+ ["e", defaultValue],
+ ["e2", defaultValue],
+ ["1e0.1", defaultValue],
+ ["", defaultValue],
+ ["foo", defaultValue],
+ ["42,13", defaultValue],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "range";
+ element.setAttribute("min", min); // avoids out of range sanitization
+ element.setAttribute("max", max);
+ element.setAttribute("step", "any"); // avoids step mismatch sanitization
+ for (let data of testData) {
+ element.value = data[0];
+
+ // Given that NaN != NaN, we have to use null when the expected value is NaN.
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "floating point representation of the value");
+ }
+}
+
+function checkRangeSet()
+{
+ var min = -200;
+ var max = 200;
+ var defaultValue = String(min + (max - min)/2);
+
+ var testData =
+ [
+ [42, "42"],
+ [-42, "-42"], // should work for negative values
+ [42.1234, "42.1234"],
+ [123.123456789123, "123.123456789123"], // double precision
+ [1e2, "100"], // e should be usable
+ [2e1, "20"],
+ [1e-1, "0.1"], // value after e can be negative
+ [1E2, "100"], // E can be used instead of e
+ ["foo", defaultValue],
+ ["", defaultValue],
+ [42, "42"], // Keep this here, it is used by the next test.
+ // Setting Infinity should throw and not change the current value.
+ [Infinity, "42", true],
+ [-Infinity, "42", true],
+ // Setting NaN should change the value to the empty string.
+ [NaN, defaultValue],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "range";
+ element.setAttribute("min", min); // avoids out of range sanitization
+ element.setAttribute("max", max);
+ element.setAttribute("step", "any"); // avoids step mismatch sanitization
+ for (let data of testData) {
+ var caught = false;
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1],
+ "valueAsNumber should be able to set the value");
+ } catch (e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+function checkDateGet()
+{
+ var validData =
+ [
+ [ "2012-07-12", 1342051200000 ],
+ [ "1970-01-01", 0 ],
+ // We are supposed to support at least until this date.
+ // (corresponding to the date object maximal value)
+ [ "275760-09-13", 8640000000000000 ],
+ // Minimum valid date (limited by the input element minimum valid value)
+ [ "0001-01-01", -62135596800000 ],
+ [ "2012-02-29", 1330473600000 ],
+ [ "2011-02-28", 1298851200000 ],
+ ];
+
+ var invalidData =
+ [
+ "invaliddate",
+ "",
+ "275760-09-14",
+ "999-12-31",
+ "-001-12-31",
+ "0000-01-01",
+ "2011-02-29",
+ "1901-13-31",
+ "1901-12-32",
+ "1901-00-12",
+ "1901-01-00",
+ "1900-02-29",
+ ];
+
+ var element = document.createElement('input');
+ element.type = "date";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "timestamp representing this date");
+ }
+
+ for (let data of invalidData) {
+ element.value = data;
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a valid date");
+ }
+}
+
+function checkDateSet()
+{
+ var testData =
+ [
+ [ 1342051200000, "2012-07-12" ],
+ [ 0, "1970-01-01" ],
+ // Maximum valid date (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09-13" ],
+ // Minimum valid date (limited by the input element minimum valid value)
+ [ -62135596800000, "0001-01-01" ],
+ [ 1330473600000, "2012-02-29" ],
+ [ 1298851200000, "2011-02-28" ],
+ // "Values must be truncated to valid dates"
+ [ 42.1234, "1970-01-01" ],
+ [ 123.123456789123, "1970-01-01" ],
+ [ 1e2, "1970-01-01" ],
+ [ 1E9, "1970-01-12" ],
+ [ 1e-1, "1970-01-01" ],
+ [ 2e10, "1970-08-20" ],
+ [ 1298851200010, "2011-02-28" ],
+ [ -1, "1969-12-31" ],
+ [ -86400000, "1969-12-31" ],
+ [ 86400000, "1970-01-02" ],
+ // Invalid numbers.
+ // Those are implicitly converted to numbers
+ [ "", "1970-01-01" ],
+ [ true, "1970-01-01" ],
+ [ false, "1970-01-01" ],
+ [ null, "1970-01-01" ],
+ // Those are converted to NaN, the corresponding date string is the empty string
+ [ "invaliddatenumber", "" ],
+ [ NaN, "" ],
+ [ undefined, "" ],
+ // Out of range, the corresponding date string is the empty string
+ [ -62135596800001, "" ],
+ // Infinity will keep the current value and throw (so we need to set a current value).
+ [ 1298851200010, "2011-02-28" ],
+ [ Infinity, "2011-02-28", true ],
+ [ -Infinity, "2011-02-28", true ],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "date";
+ for (let data of testData) {
+ var caught = false;
+
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1], "valueAsNumber should set the value to " + data[1]);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "the value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+
+}
+
+function checkTimeGet()
+{
+ var tests = [
+ // Some invalid values to begin.
+ { value: "", result: NaN },
+ { value: "foobar", result: NaN },
+ { value: "00:", result: NaN },
+ { value: "24:00", result: NaN },
+ { value: "00:99", result: NaN },
+ { value: "00:00:", result: NaN },
+ { value: "00:00:99", result: NaN },
+ { value: "00:00:00:", result: NaN },
+ { value: "00:00:00.", result: NaN },
+ { value: "00:00:00.0000", result: NaN },
+ // Some simple valid values.
+ { value: "00:00", result: 0 },
+ { value: "00:01", result: 60000 },
+ { value: "01:00", result: 3600000 },
+ { value: "01:01", result: 3660000 },
+ { value: "13:37", result: 49020000 },
+ // Valid values including seconds.
+ { value: "00:00:01", result: 1000 },
+ { value: "13:37:42", result: 49062000 },
+ // Valid values including seconds fractions.
+ { value: "00:00:00.001", result: 1 },
+ { value: "00:00:00.123", result: 123 },
+ { value: "00:00:00.100", result: 100 },
+ { value: "00:00:00.000", result: 0 },
+ { value: "20:17:31.142", result: 73051142 },
+ // Highest possible value.
+ { value: "23:59:59.999", result: 86399999 },
+ // Some values with one or two digits for the fraction of seconds.
+ { value: "00:00:00.1", result: 100 },
+ { value: "00:00:00.14", result: 140 },
+ { value: "13:37:42.7", result: 49062700 },
+ { value: "23:31:12.23", result: 84672230 },
+ ];
+
+ var element = document.createElement('input');
+ element.type = 'time';
+
+ for (let test of tests) {
+ element.value = test.value;
+ if (isNaN(test.result)) {
+ ok(isNaN(element.valueAsNumber),
+ "invalid value should have .valueAsNumber return NaN");
+ } else {
+ is(element.valueAsNumber, test.result,
+ ".valueAsNumber should return " + test.result);
+ }
+ }
+}
+
+function checkTimeSet()
+{
+ var tests = [
+ // Some NaN values (should set to empty string).
+ { value: NaN, result: "" },
+ { value: "foobar", result: "" },
+ { value() {}, result: "" },
+ // Inifinity (should throw).
+ { value: Infinity, throw: true },
+ { value: -Infinity, throw: true },
+ // "" converts to 0... JS is fun :)
+ { value: "", result: "00:00" },
+ // Simple tests.
+ { value: 0, result: "00:00" },
+ { value: 1, result: "00:00:00.001" },
+ { value: 100, result: "00:00:00.100" },
+ { value: 1000, result: "00:00:01" },
+ { value: 60000, result: "00:01" },
+ { value: 3600000, result: "01:00" },
+ { value: 83622234, result: "23:13:42.234" },
+ // Some edge cases.
+ { value: 86400000, result: "00:00" },
+ { value: 86400001, result: "00:00:00.001" },
+ { value: 170022234, result: "23:13:42.234" },
+ { value: 432000000, result: "00:00" },
+ { value: -1, result: "23:59:59.999" },
+ { value: -86400000, result: "00:00" },
+ { value: -86400001, result: "23:59:59.999" },
+ { value: -56789, result: "23:59:03.211" },
+ { value: 0.9, result: "00:00" },
+ ];
+
+ var element = document.createElement('input');
+ element.type = 'time';
+
+ for (let test of tests) {
+ try {
+ var caught = false;
+ element.valueAsNumber = test.value;
+ is(element.value, test.result, "value should return " + test.result);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (!test.throw) {
+ test.throw = false;
+ }
+
+ is(caught, test.throw, "the test throwing status should be " + test.throw);
+ }
+}
+
+function checkMonthGet()
+{
+ var validData =
+ [
+ [ "2016-07", 558 ],
+ [ "1970-01", 0 ],
+ [ "1969-12", -1 ],
+ [ "0001-01", -23628 ],
+ [ "10000-12", 96371 ],
+ [ "275760-09", 3285488 ],
+ ];
+
+ var invalidData =
+ [
+ "invalidmonth",
+ "0000-01",
+ "2000-00",
+ "2012-13",
+ // Out of range.
+ "275760-10",
+ ];
+
+ var element = document.createElement('input');
+ element.type = "month";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "integer value representing this month");
+ }
+
+ for (let data of invalidData) {
+ element.value = data;
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a valid month");
+ }
+}
+
+function checkMonthSet()
+{
+ var testData =
+ [
+ [ 558, "2016-07" ],
+ [ 0, "1970-01" ],
+ [ -1, "1969-12" ],
+ [ 96371, "10000-12" ],
+ [ 12, "1971-01" ],
+ [ -12, "1969-01" ],
+ // Maximum valid month (limited by the ecma date object range)
+ [ 3285488, "275760-09" ],
+ // Minimum valid month (limited by the input element minimum valid value)
+ [ -23628, "0001-01" ],
+ // "Values must be truncated to valid months"
+ [ 0.3, "1970-01" ],
+ [ -1.1, "1969-11" ],
+ [ 1e2, "1978-05" ],
+ [ 1e-1, "1970-01" ],
+ // Invalid numbers.
+ // Those are implicitly converted to numbers
+ [ "", "1970-01" ],
+ [ true, "1970-02" ],
+ [ false, "1970-01" ],
+ [ null, "1970-01" ],
+ // Those are converted to NaN, the corresponding month string is the empty string
+ [ "invalidmonth", "" ],
+ [ NaN, "" ],
+ [ undefined, "" ],
+ // Out of range, the corresponding month string is the empty string
+ [ -23629, "" ],
+ [ 3285489, "" ],
+ // Infinity will keep the current value and throw (so we need to set a current value)
+ [ 558, "2016-07" ],
+ [ Infinity, "2016-07", true ],
+ [ -Infinity, "2016-07", true ],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "month";
+ for (let data of testData) {
+ var caught = false;
+
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1], "valueAsNumber should set the value to " + data[1]);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "the value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+function checkWeekGet()
+{
+ var validData =
+ [
+ // Common years starting on different days of week.
+ [ "2007-W01", Date.UTC(2007, 0, 1) ], // Mon
+ [ "2013-W01", Date.UTC(2012, 11, 31) ], // Tue
+ [ "2014-W01", Date.UTC(2013, 11, 30) ], // Wed
+ [ "2015-W01", Date.UTC(2014, 11, 29) ], // Thu
+ [ "2010-W01", Date.UTC(2010, 0, 4) ], // Fri
+ [ "2011-W01", Date.UTC(2011, 0, 3) ], // Sat
+ [ "2017-W01", Date.UTC(2017, 0, 2) ], // Sun
+ // Common years ending on different days of week.
+ [ "2007-W52", Date.UTC(2007, 11, 24) ], // Mon
+ [ "2013-W52", Date.UTC(2013, 11, 23) ], // Tue
+ [ "2014-W52", Date.UTC(2014, 11, 22) ], // Wed
+ [ "2015-W53", Date.UTC(2015, 11, 28) ], // Thu
+ [ "2010-W52", Date.UTC(2010, 11, 27) ], // Fri
+ [ "2011-W52", Date.UTC(2011, 11, 26) ], // Sat
+ [ "2017-W52", Date.UTC(2017, 11, 25) ], // Sun
+ // Leap years starting on different days of week.
+ [ "1996-W01", Date.UTC(1996, 0, 1) ], // Mon
+ [ "2008-W01", Date.UTC(2007, 11, 31) ], // Tue
+ [ "2020-W01", Date.UTC(2019, 11, 30) ], // Wed
+ [ "2004-W01", Date.UTC(2003, 11, 29) ], // Thu
+ [ "2016-W01", Date.UTC(2016, 0, 4) ], // Fri
+ [ "2000-W01", Date.UTC(2000, 0, 3) ], // Sat
+ [ "2012-W01", Date.UTC(2012, 0, 2) ], // Sun
+ // Leap years ending on different days of week.
+ [ "2012-W52", Date.UTC(2012, 11, 24) ], // Mon
+ [ "2024-W52", Date.UTC(2024, 11, 23) ], // Tue
+ [ "1980-W52", Date.UTC(1980, 11, 22) ], // Wed
+ [ "1992-W53", Date.UTC(1992, 11, 28) ], // Thu
+ [ "2004-W53", Date.UTC(2004, 11, 27) ], // Fri
+ [ "1988-W52", Date.UTC(1988, 11, 26) ], // Sat
+ [ "2000-W52", Date.UTC(2000, 11, 25) ], // Sun
+ // Other normal cases.
+ [ "2015-W53", Date.UTC(2015, 11, 28) ],
+ [ "2016-W36", Date.UTC(2016, 8, 5) ],
+ [ "1970-W01", Date.UTC(1969, 11, 29) ],
+ [ "275760-W37", Date.UTC(275760, 8, 8) ],
+ ];
+
+ var invalidData =
+ [
+ "invalidweek",
+ "0000-W01",
+ "2016-W00",
+ "2016-W53",
+ // Out of range.
+ "275760-W38",
+ ];
+
+ var element = document.createElement('input');
+ element.type = "week";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "integer value representing this week");
+ }
+
+ for (let data of invalidData) {
+ element.value = data;
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a valid week");
+ }
+}
+
+function checkWeekSet()
+{
+ var testData =
+ [
+ // Common years starting on different days of week.
+ [ Date.UTC(2007, 0, 1), "2007-W01" ], // Mon
+ [ Date.UTC(2013, 0, 1), "2013-W01" ], // Tue
+ [ Date.UTC(2014, 0, 1), "2014-W01" ], // Wed
+ [ Date.UTC(2015, 0, 1), "2015-W01" ], // Thu
+ [ Date.UTC(2010, 0, 1), "2009-W53" ], // Fri
+ [ Date.UTC(2011, 0, 1), "2010-W52" ], // Sat
+ [ Date.UTC(2017, 0, 1), "2016-W52" ], // Sun
+ // Common years ending on different days of week.
+ [ Date.UTC(2007, 11, 31), "2008-W01" ], // Mon
+ [ Date.UTC(2013, 11, 31), "2014-W01" ], // Tue
+ [ Date.UTC(2014, 11, 31), "2015-W01" ], // Wed
+ [ Date.UTC(2015, 11, 31), "2015-W53" ], // Thu
+ [ Date.UTC(2010, 11, 31), "2010-W52" ], // Fri
+ [ Date.UTC(2011, 11, 31), "2011-W52" ], // Sat
+ [ Date.UTC(2017, 11, 31), "2017-W52" ], // Sun
+ // Leap years starting on different days of week.
+ [ Date.UTC(1996, 0, 1), "1996-W01" ], // Mon
+ [ Date.UTC(2008, 0, 1), "2008-W01" ], // Tue
+ [ Date.UTC(2020, 0, 1), "2020-W01" ], // Wed
+ [ Date.UTC(2004, 0, 1), "2004-W01" ], // Thu
+ [ Date.UTC(2016, 0, 1), "2015-W53" ], // Fri
+ [ Date.UTC(2000, 0, 1), "1999-W52" ], // Sat
+ [ Date.UTC(2012, 0, 1), "2011-W52" ], // Sun
+ // Leap years ending on different days of week.
+ [ Date.UTC(2012, 11, 31), "2013-W01" ], // Mon
+ [ Date.UTC(2024, 11, 31), "2025-W01" ], // Tue
+ [ Date.UTC(1980, 11, 31), "1981-W01" ], // Wed
+ [ Date.UTC(1992, 11, 31), "1992-W53" ], // Thu
+ [ Date.UTC(2004, 11, 31), "2004-W53" ], // Fri
+ [ Date.UTC(1988, 11, 31), "1988-W52" ], // Sat
+ [ Date.UTC(2000, 11, 31), "2000-W52" ], // Sun
+ // Other normal cases.
+ [ Date.UTC(2008, 8, 26), "2008-W39" ],
+ [ Date.UTC(2016, 0, 4), "2016-W01" ],
+ [ Date.UTC(2016, 0, 10), "2016-W01" ],
+ [ Date.UTC(2016, 0, 11), "2016-W02" ],
+ // Maximum valid week (limited by the ecma date object range).
+ [ 8640000000000000, "275760-W37" ],
+ // Minimum valid week (limited by the input element minimum valid value)
+ [ -62135596800000, "0001-W01" ],
+ // "Values must be truncated to valid weeks"
+ [ 0.3, "1970-W01" ],
+ [ 1e-1, "1970-W01" ],
+ [ -1.1, "1970-W01" ],
+ [ -345600000, "1969-W52" ],
+ // Invalid numbers.
+ // Those are implicitly converted to numbers
+ [ "", "1970-W01" ],
+ [ true, "1970-W01" ],
+ [ false, "1970-W01" ],
+ [ null, "1970-W01" ],
+ // Those are converted to NaN, the corresponding week string is the empty string
+ [ "invalidweek", "" ],
+ [ NaN, "" ],
+ [ undefined, "" ],
+ // Infinity will keep the current value and throw (so we need to set a current value).
+ [ Date.UTC(2016, 8, 8), "2016-W36" ],
+ [ Infinity, "2016-W36", true ],
+ [ -Infinity, "2016-W36", true ],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "week";
+ for (let data of testData) {
+ var caught = false;
+
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1], "valueAsNumber should set the value to " +
+ data[1]);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "the value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+function checkDatetimeLocalGet() {
+ var validData =
+ [
+ // Simple cases.
+ [ "2016-12-20T09:58", Date.UTC(2016, 11, 20, 9, 58) ],
+ [ "2016-12-20T09:58:30", Date.UTC(2016, 11, 20, 9, 58, 30) ],
+ [ "2016-12-20T09:58:30.123", Date.UTC(2016, 11, 20, 9, 58, 30, 123) ],
+ [ "2017-01-01T10:00", Date.UTC(2017, 0, 1, 10, 0, 0) ],
+ [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ],
+ [ "1970-01-01T00:00", 0 ],
+ // Leap years.
+ [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ],
+ [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ],
+ [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ],
+ [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ],
+ [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ],
+ // Maximum valid datetime-local (limited by the ecma date object range).
+ [ "275760-09-13T00:00", 8640000000000000 ],
+ // Minimum valid datetime-local (limited by the input element minimum valid value).
+ [ "0001-01-01T00:00", -62135596800000 ],
+ ];
+
+ var invalidData =
+ [
+ "invaliddatetime-local",
+ "0000-01-01T00:00",
+ "2016-12-25T00:00Z",
+ "2015-02-29T12:34",
+ "1-1-1T12:00",
+ // Out of range.
+ "275760-09-13T12:00",
+ ];
+
+ var element = document.createElement('input');
+ element.type = "datetime-local";
+ for (let data of validData) {
+ element.value = data[0];
+ is(element.valueAsNumber, data[1], "valueAsNumber should return the " +
+ "integer value representing this datetime-local");
+ }
+
+ for (let data of invalidData) {
+ element.value = data;
+ ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " +
+ "when the element value is not a valid datetime-local");
+ }
+}
+
+function checkDatetimeLocalSet()
+{
+ var testData =
+ [
+ // Simple cases.
+ [ Date.UTC(2016, 11, 20, 9, 58, 0), "2016-12-20T09:58", ],
+ [ Date.UTC(2016, 11, 20, 9, 58, 30), "2016-12-20T09:58:30" ],
+ [ Date.UTC(2016, 11, 20, 9, 58, 30, 123), "2016-12-20T09:58:30.123" ],
+ [ Date.UTC(2017, 0, 1, 10, 0, 0), "2017-01-01T10:00" ],
+ [ Date.UTC(1969, 11, 31, 12, 0, 0), "1969-12-31T12:00" ],
+ [ 0, "1970-01-01T00:00" ],
+ // Maximum valid week (limited by the ecma date object range).
+ [ 8640000000000000, "275760-09-13T00:00" ],
+ // Minimum valid datetime-local (limited by the input element minimum valid value).
+ [ -62135596800000, "0001-01-01T00:00" ],
+ // Leap years.
+ [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ],
+ [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ],
+ [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ],
+ [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ],
+ [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ],
+ // "Values must be truncated to valid datetime-local"
+ [ 0.3, "1970-01-01T00:00" ],
+ [ 1e-1, "1970-01-01T00:00" ],
+ [ -1 , "1969-12-31T23:59:59.999" ],
+ [ -345600000, "1969-12-28T00:00" ],
+ // Invalid numbers.
+ // Those are implicitly converted to numbers
+ [ "", "1970-01-01T00:00" ],
+ [ true, "1970-01-01T00:00:00.001" ],
+ [ false, "1970-01-01T00:00" ],
+ [ null, "1970-01-01T00:00" ],
+ // Those are converted to NaN, the corresponding week string is the empty string
+ [ "invaliddatetime-local", "" ],
+ [ NaN, "" ],
+ [ undefined, "" ],
+ // Infinity will keep the current value and throw (so we need to set a current value).
+ [ Date.UTC(2016, 11, 27, 15, 10, 0), "2016-12-27T15:10" ],
+ [ Infinity, "2016-12-27T15:10", true ],
+ [ -Infinity, "2016-12-27T15:10", true ],
+ ];
+
+ var element = document.createElement('input');
+ element.type = "datetime-local";
+ for (let data of testData) {
+ var caught = false;
+
+ try {
+ element.valueAsNumber = data[0];
+ is(element.value, data[1], "valueAsNumber should set the value to " +
+ data[1]);
+ } catch(e) {
+ caught = true;
+ }
+
+ if (data[2]) {
+ ok(caught, "valueAsNumber should have thrown");
+ is(element.value, data[1], "the value should not have changed");
+ } else {
+ ok(!caught, "valueAsNumber should not have thrown");
+ }
+ }
+}
+
+checkAvailability();
+
+// <input type='number'> test
+checkNumberGet();
+checkNumberSet();
+
+// <input type='range'> test
+checkRangeGet();
+checkRangeSet();
+
+// <input type='date'> test
+checkDateGet();
+checkDateSet();
+
+// <input type='time'> test
+checkTimeGet();
+checkTimeSet();
+
+// <input type='month'> test
+checkMonthGet();
+checkMonthSet();
+
+// <input type='week'> test
+checkWeekGet();
+checkWeekSet();
+
+// <input type='datetime-local'> test
+checkDatetimeLocalGet();
+checkDatetimeLocalSet();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/forms/without_selectionchange/mochitest.toml b/dom/html/test/forms/without_selectionchange/mochitest.toml
new file mode 100644
index 0000000000..8f019d8d80
--- /dev/null
+++ b/dom/html/test/forms/without_selectionchange/mochitest.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+prefs = ["dom.select_events.textcontrols.enabled=false"]
+
+["test_select.html"]
+
diff --git a/dom/html/test/forms/without_selectionchange/test_select.html b/dom/html/test/forms/without_selectionchange/test_select.html
new file mode 100644
index 0000000000..3d11611b1b
--- /dev/null
+++ b/dom/html/test/forms/without_selectionchange/test_select.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<meta charset="utf-8">
+<title>Test for Bug 1717435</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+
+<textarea id="textarea">foo</textarea>
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ textarea.addEventListener("select", ev => {
+ ok(true, "A select event must fire regardless of dom.select_events.textcontrols.enabled");
+ SimpleTest.finish();
+ });
+
+ textarea.focus();
+ textarea.select();
+ is(textarea.selectionStart, 0, "selectionStart")
+ is(textarea.selectionEnd, 3, "selectionEnd")
+</script>
diff --git a/dom/html/test/head.js b/dom/html/test/head.js
new file mode 100644
index 0000000000..1e3b435d0c
--- /dev/null
+++ b/dom/html/test/head.js
@@ -0,0 +1,65 @@
+function pushPrefs(...aPrefs) {
+ return SpecialPowers.pushPrefEnv({ set: aPrefs });
+}
+
+function promiseWaitForEvent(
+ object,
+ eventName,
+ capturing = false,
+ chrome = false
+) {
+ return new Promise(resolve => {
+ function listener(event) {
+ info("Saw " + eventName);
+ object.removeEventListener(eventName, listener, capturing, chrome);
+ resolve(event);
+ }
+
+ info("Waiting for " + eventName);
+ object.addEventListener(eventName, listener, capturing, chrome);
+ });
+}
+
+/**
+ * Waits for the next load to complete in any browser or the given browser.
+ * If a <tabbrowser> is given it waits for a load in any of its browsers.
+ *
+ * @return promise
+ */
+function waitForDocLoadComplete(aBrowser = gBrowser) {
+ return new Promise(resolve => {
+ let listener = {
+ onStateChange(webProgress, req, flags, status) {
+ let docStop =
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK |
+ Ci.nsIWebProgressListener.STATE_STOP;
+ info(
+ "Saw state " +
+ flags.toString(16) +
+ " and status " +
+ status.toString(16)
+ );
+ // When a load needs to be retargetted to a new process it is cancelled
+ // with NS_BINDING_ABORTED so ignore that case
+ if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) {
+ aBrowser.removeProgressListener(this);
+ waitForDocLoadComplete.listeners.delete(this);
+ let chan = req.QueryInterface(Ci.nsIChannel);
+ info("Browser loaded " + chan.originalURI.spec);
+ resolve();
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ aBrowser.addProgressListener(listener);
+ waitForDocLoadComplete.listeners.add(listener);
+ info("Waiting for browser load");
+ });
+}
+// Keep a set of progress listeners for waitForDocLoadComplete() to make sure
+// they're not GC'ed before we saw the page load.
+waitForDocLoadComplete.listeners = new Set();
+registerCleanupFunction(() => waitForDocLoadComplete.listeners.clear());
diff --git a/dom/html/test/image-allow-credentials.png b/dom/html/test/image-allow-credentials.png
new file mode 100644
index 0000000000..df24ac6d34
--- /dev/null
+++ b/dom/html/test/image-allow-credentials.png
Binary files differ
diff --git a/dom/html/test/image-allow-credentials.png^headers^ b/dom/html/test/image-allow-credentials.png^headers^
new file mode 100644
index 0000000000..a03f99a9c0
--- /dev/null
+++ b/dom/html/test/image-allow-credentials.png^headers^
@@ -0,0 +1,2 @@
+Access-Control-Allow-Origin: http://mochi.test:8888
+Access-Control-Allow-Credentials: true
diff --git a/dom/html/test/image.png b/dom/html/test/image.png
new file mode 100644
index 0000000000..d26878c9f2
--- /dev/null
+++ b/dom/html/test/image.png
Binary files differ
diff --git a/dom/html/test/image_yellow.png b/dom/html/test/image_yellow.png
new file mode 100644
index 0000000000..51e8aaf38c
--- /dev/null
+++ b/dom/html/test/image_yellow.png
Binary files differ
diff --git a/dom/html/test/mochitest.toml b/dom/html/test/mochitest.toml
new file mode 100644
index 0000000000..d1dd78705b
--- /dev/null
+++ b/dom/html/test/mochitest.toml
@@ -0,0 +1,990 @@
+[DEFAULT]
+prefs = ["gfx.font_loader.delay=0"]
+support-files = [
+ "347174transform.xsl",
+ "347174transformable.xml",
+ "allowMedia.sjs",
+ "bug100533_iframe.html",
+ "bug100533_load.html",
+ "bug196523-subframe.html",
+ "bug199692-nested-d2.html",
+ "bug199692-nested.html",
+ "bug199692-popup.html",
+ "bug199692-scrolled.html",
+ "bug242709_iframe.html",
+ "bug242709_load.html",
+ "bug277724_iframe1.html",
+ "bug277724_iframe2.xhtml",
+ "bug277890_iframe.html",
+ "bug277890_load.html",
+ "bug340800_iframe.txt",
+ "bug369370-popup.png",
+ "bug372098-link-target.html",
+ "bug441930_iframe.html",
+ "bug445004-inner.html",
+ "bug445004-inner.js",
+ "bug445004-outer-abs.html",
+ "bug445004-outer-rel.html",
+ "bug445004-outer-write.html",
+ "bug446483-iframe.html",
+ "bug448564-echo.sjs",
+ "bug448564-iframe-1.html",
+ "bug448564-iframe-2.html",
+ "bug448564-iframe-3.html",
+ "bug448564-submit.js",
+ "bug499092.html",
+ "bug499092.xml",
+ "bug514856_iframe.html",
+ "bug1260704_iframe.html",
+ "bug1260704_iframe_empty.html",
+ "bug1292522_iframe.html",
+ "bug1292522_page.html",
+ "bug1315146-iframe.html",
+ "bug1315146-main.html",
+ "dummy_page.html",
+ "test_non-ascii-cookie.html^headers^",
+ "file_bug209275_1.html",
+ "file_bug209275_2.html",
+ "file_bug209275_3.html",
+ "file_bug297761.html",
+ "file_bug417760.png",
+ "file_bug893537.html",
+ "file_bug1260704.png",
+ "file_formSubmission_img.jpg",
+ "file_formSubmission_text.txt",
+ "file_iframe_sandbox_a_if1.html",
+ "file_iframe_sandbox_a_if10.html",
+ "file_iframe_sandbox_a_if11.html",
+ "file_iframe_sandbox_a_if12.html",
+ "file_iframe_sandbox_a_if13.html",
+ "file_iframe_sandbox_a_if14.html",
+ "file_iframe_sandbox_a_if15.html",
+ "file_iframe_sandbox_a_if16.html",
+ "file_iframe_sandbox_a_if17.html",
+ "file_iframe_sandbox_a_if18.html",
+ "file_iframe_sandbox_a_if19.html",
+ "file_iframe_sandbox_a_if2.html",
+ "file_iframe_sandbox_a_if3.html",
+ "file_iframe_sandbox_a_if4.html",
+ "file_iframe_sandbox_a_if5.html",
+ "file_iframe_sandbox_a_if6.html",
+ "file_iframe_sandbox_a_if7.html",
+ "file_iframe_sandbox_a_if8.html",
+ "file_iframe_sandbox_a_if9.html",
+ "file_iframe_sandbox_b_if1.html",
+ "file_iframe_sandbox_b_if2.html",
+ "file_iframe_sandbox_b_if3.html",
+ "file_iframe_sandbox_c_if1.html",
+ "file_iframe_sandbox_c_if2.html",
+ "file_iframe_sandbox_c_if3.html",
+ "file_iframe_sandbox_c_if4.html",
+ "file_iframe_sandbox_c_if5.html",
+ "file_iframe_sandbox_c_if6.html",
+ "file_iframe_sandbox_c_if7.html",
+ "file_iframe_sandbox_c_if8.html",
+ "file_iframe_sandbox_c_if9.html",
+ "file_iframe_sandbox_close.html",
+ "file_iframe_sandbox_d_if1.html",
+ "file_iframe_sandbox_d_if10.html",
+ "file_iframe_sandbox_d_if11.html",
+ "file_iframe_sandbox_d_if12.html",
+ "file_iframe_sandbox_d_if13.html",
+ "file_iframe_sandbox_d_if14.html",
+ "file_iframe_sandbox_d_if15.html",
+ "file_iframe_sandbox_d_if16.html",
+ "file_iframe_sandbox_d_if17.html",
+ "file_iframe_sandbox_d_if18.html",
+ "file_iframe_sandbox_d_if19.html",
+ "file_iframe_sandbox_d_if2.html",
+ "file_iframe_sandbox_d_if20.html",
+ "file_iframe_sandbox_d_if21.html",
+ "file_iframe_sandbox_d_if22.html",
+ "file_iframe_sandbox_d_if23.html",
+ "file_iframe_sandbox_d_if3.html",
+ "file_iframe_sandbox_d_if4.html",
+ "file_iframe_sandbox_d_if5.html",
+ "file_iframe_sandbox_d_if6.html",
+ "file_iframe_sandbox_d_if7.html",
+ "file_iframe_sandbox_d_if8.html",
+ "file_iframe_sandbox_d_if9.html",
+ "file_iframe_sandbox_e_if1.html",
+ "file_iframe_sandbox_e_if10.html",
+ "file_iframe_sandbox_e_if11.html",
+ "file_iframe_sandbox_e_if12.html",
+ "file_iframe_sandbox_e_if13.html",
+ "file_iframe_sandbox_e_if14.html",
+ "file_iframe_sandbox_e_if15.html",
+ "file_iframe_sandbox_e_if16.html",
+ "file_iframe_sandbox_e_if2.html",
+ "file_iframe_sandbox_e_if3.html",
+ "file_iframe_sandbox_e_if4.html",
+ "file_iframe_sandbox_e_if5.html",
+ "file_iframe_sandbox_e_if6.html",
+ "file_iframe_sandbox_e_if7.html",
+ "file_iframe_sandbox_e_if8.html",
+ "file_iframe_sandbox_e_if9.html",
+ "file_iframe_sandbox_fail.js",
+ "file_iframe_sandbox_form_fail.html",
+ "file_iframe_sandbox_form_pass.html",
+ "file_iframe_sandbox_g_if1.html",
+ "file_iframe_sandbox_h_if1.html",
+ "file_iframe_sandbox_k_if1.html",
+ "file_iframe_sandbox_k_if2.html",
+ "file_iframe_sandbox_k_if3.html",
+ "file_iframe_sandbox_k_if4.html",
+ "file_iframe_sandbox_k_if5.html",
+ "file_iframe_sandbox_k_if6.html",
+ "file_iframe_sandbox_k_if7.html",
+ "file_iframe_sandbox_k_if8.html",
+ "file_iframe_sandbox_k_if9.html",
+ "file_iframe_sandbox_navigation_fail.html",
+ "file_iframe_sandbox_navigation_pass.html",
+ "file_iframe_sandbox_navigation_start.html",
+ "file_iframe_sandbox_open_window_fail.html",
+ "file_iframe_sandbox_open_window_pass.html",
+ "file_iframe_sandbox_pass.js",
+ "file_iframe_sandbox_redirect.html",
+ "file_iframe_sandbox_redirect.html^headers^",
+ "file_iframe_sandbox_redirect_target.html",
+ "file_iframe_sandbox_refresh.html",
+ "file_iframe_sandbox_refresh.html^headers^",
+ "file_iframe_sandbox_srcdoc_allow_scripts.html",
+ "file_iframe_sandbox_srcdoc_no_allow_scripts.html",
+ "file_iframe_sandbox_top_navigation_fail.html",
+ "file_iframe_sandbox_top_navigation_pass.html",
+ "file_iframe_sandbox_window_form_fail.html",
+ "file_iframe_sandbox_window_form_pass.html",
+ "file_iframe_sandbox_window_navigation_fail.html",
+ "file_iframe_sandbox_window_navigation_pass.html",
+ "file_iframe_sandbox_window_top_navigation_pass.html",
+ "file_iframe_sandbox_window_top_navigation_fail.html",
+ "file_iframe_sandbox_worker.js",
+ "file_srcdoc-2.html",
+ "file_srcdoc.html",
+ "file_srcdoc_iframe3.html",
+ "file_window_open_close_outer.html",
+ "file_window_open_close_inner.html",
+ "formSubmission_chrome.js",
+ "form_submit_server.sjs",
+ "formData_worker.js",
+ "formData_test.js",
+ "image.png",
+ "image-allow-credentials.png",
+ "image-allow-credentials.png^headers^",
+ "nnc_lockup.gif",
+ "reflect.js",
+ "simpleFileOpener.js",
+ "file_bug1166138_1x.png",
+ "file_bug1166138_2x.png",
+ "file_bug1166138_def.png",
+ "script_fakepath.js",
+ "sw_formSubmission.js",
+ "object_bug287465_o1.html",
+ "object_bug287465_o2.html",
+ "object_bug556645.html",
+ "file.webm",
+ "!/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+]
+
+["test_a_text.html"]
+
+["test_allowMedia.html"]
+skip-if = [
+ "verify && (os == 'linux' || os == 'win')",
+ "!debug && os == 'mac' && bits == 64",
+ "debug && os == 'win'",
+ "debug && os == 'linux' && os_version == '18.04'", #Bug 1434744
+]
+
+["test_anchor_href_cache_invalidation.html"]
+
+["test_base_attributes_reflection.html"]
+
+["test_bug589.html"]
+
+["test_bug691.html"]
+
+["test_bug694.html"]
+
+["test_bug696.html"]
+
+["test_bug1297.html"]
+
+["test_bug1366.html"]
+
+["test_bug1400.html"]
+
+["test_bug1682.html"]
+
+["test_bug1823.html"]
+
+["test_bug2082.html"]
+
+["test_bug3348.html"]
+
+["test_bug6296.html"]
+
+["test_bug24958.html"]
+
+["test_bug57600.html"]
+
+["test_bug95530.html"]
+
+["test_bug100533.html"]
+
+["test_bug109445.html"]
+
+["test_bug109445.xhtml"]
+
+["test_bug143220.html"]
+
+["test_bug182279.html"]
+
+["test_bug196523.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug199692.html"]
+
+["test_bug209275.xhtml"]
+skip-if = ["os == 'android'"] #TIMED_OUT
+
+["test_bug237071.html"]
+
+["test_bug242709.html"]
+
+["test_bug255820.html"]
+
+["test_bug259332.html"]
+
+["test_bug274626.html"]
+
+["test_bug277724.html"]
+
+["test_bug277890.html"]
+
+["test_bug287465.html"]
+
+["test_bug295561.html"]
+
+["test_bug297761.html"]
+
+["test_bug300691-1.html"]
+
+["test_bug300691-2.html"]
+
+["test_bug300691-3.xhtml"]
+
+["test_bug311681.html"]
+
+["test_bug311681.xhtml"]
+
+["test_bug324378.html"]
+
+["test_bug330705-1.html"]
+
+["test_bug332246.html"]
+
+["test_bug332848.xhtml"]
+
+["test_bug332893-1.html"]
+
+["test_bug332893-2.html"]
+
+["test_bug332893-3.html"]
+
+["test_bug332893-4.html"]
+
+["test_bug332893-5.html"]
+
+["test_bug332893-6.html"]
+
+["test_bug332893-7.html"]
+
+["test_bug340017.xhtml"]
+
+["test_bug340800.html"]
+
+["test_bug347174.html"]
+
+["test_bug347174_write.html"]
+
+["test_bug347174_xsl.html"]
+
+["test_bug347174_xslp.html"]
+
+["test_bug353415-1.html"]
+
+["test_bug353415-2.html"]
+
+["test_bug359657.html"]
+
+["test_bug369370.html"]
+skip-if = [
+ "os == 'android'",
+ "os == 'linux'", # disabled on linux bug 1258103
+]
+
+["test_bug371375.html"]
+
+["test_bug372098.html"]
+
+["test_bug373589.html"]
+
+["test_bug375003-1.html"]
+
+["test_bug375003-2.html"]
+
+["test_bug377624.html"]
+
+["test_bug380383.html"]
+
+["test_bug383383.html"]
+
+["test_bug383383_2.xhtml"]
+
+["test_bug384419.html"]
+
+["test_bug386496.html"]
+
+["test_bug386728.html"]
+
+["test_bug386996.html"]
+
+["test_bug388558.html"]
+
+["test_bug388746.html"]
+
+["test_bug388794.html"]
+
+["test_bug389797.html"]
+
+["test_bug390975.html"]
+
+["test_bug391994.html"]
+
+["test_bug394700.html"]
+
+["test_bug395107.html"]
+
+["test_bug401160.xhtml"]
+
+["test_bug402680.html"]
+
+["test_bug403868.html"]
+
+["test_bug403868.xhtml"]
+
+["test_bug405242.html"]
+
+["test_bug406596.html"]
+
+["test_bug417760.html"]
+
+["test_bug421640.html"]
+
+["test_bug424698.html"]
+
+["test_bug428135.xhtml"]
+
+["test_bug430351.html"]
+skip-if = ["os == 'android'"] # Bug 1525959
+
+["test_bug435128.html"]
+skip-if = ["true"] # Disabled for timeouts.
+
+["test_bug441930.html"]
+
+["test_bug442801.html"]
+
+["test_bug445004.html"]
+skip-if = ["true"] # Disabled permanently (bug 559932).
+
+["test_bug446483.html"]
+
+["test_bug448166.html"]
+
+["test_bug448564.html"]
+
+["test_bug456229.html"]
+
+["test_bug458037.xhtml"]
+allow_xul_xbl = true
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug460568.html"]
+
+["test_bug463104.html"]
+
+["test_bug478251.html"]
+
+["test_bug481335.xhtml"]
+skip-if = ["os == 'android'"] #TIMED_OUT
+
+["test_bug481440.html"]
+
+["test_bug481647.html"]
+
+["test_bug482659.html"]
+
+["test_bug486741.html"]
+
+["test_bug489532.html"]
+
+["test_bug497242.xhtml"]
+
+["test_bug499092.html"]
+
+["test_bug500885.html"]
+
+["test_bug512367.html"]
+
+["test_bug514856.html"]
+
+["test_bug518122.html"]
+
+["test_bug519987.html"]
+
+["test_bug523771.html"]
+
+["test_bug529819.html"]
+
+["test_bug529859.html"]
+
+["test_bug535043.html"]
+
+["test_bug536891.html"]
+
+["test_bug536895.html"]
+
+["test_bug546995.html"]
+
+["test_bug547850.html"]
+
+["test_bug551846.html"]
+
+["test_bug555567.html"]
+
+["test_bug556645.html"]
+
+["test_bug557087-1.html"]
+
+["test_bug557087-2.html"]
+
+["test_bug557087-3.html"]
+
+["test_bug557087-4.html"]
+
+["test_bug557087-5.html"]
+
+["test_bug557087-6.html"]
+
+["test_bug557620.html"]
+
+["test_bug558788-1.html"]
+
+["test_bug558788-2.html"]
+
+["test_bug560112.html"]
+
+["test_bug561634.html"]
+
+["test_bug561636.html"]
+
+["test_bug561640.html"]
+
+["test_bug564001.html"]
+
+["test_bug566046.html"]
+
+["test_bug567938-1.html"]
+
+["test_bug567938-2.html"]
+
+["test_bug567938-3.html"]
+
+["test_bug567938-4.html"]
+
+["test_bug569955.html"]
+
+["test_bug573969.html"]
+
+["test_bug579079.html"]
+
+["test_bug582412-1.html"]
+
+["test_bug582412-2.html"]
+
+["test_bug583514.html"]
+
+["test_bug583533.html"]
+
+["test_bug586763.html"]
+
+["test_bug586786.html"]
+
+["test_bug587469.html"]
+
+["test_bug590353-1.html"]
+
+["test_bug590353-2.html"]
+
+["test_bug590363.html"]
+
+["test_bug592802.html"]
+
+["test_bug593689.html"]
+
+["test_bug595429.html"]
+
+["test_bug595447.html"]
+
+["test_bug595449.html"]
+
+["test_bug596350.html"]
+
+["test_bug596511.html"]
+
+["test_bug598643.html"]
+
+["test_bug598833-1.html"]
+
+["test_bug600155.html"]
+
+["test_bug601030.html"]
+
+["test_bug605124-1.html"]
+
+["test_bug605124-2.html"]
+
+["test_bug605125-1.html"]
+
+["test_bug605125-2.html"]
+
+["test_bug606817.html"]
+
+["test_bug607145.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug610212.html"]
+
+["test_bug610687.html"]
+
+["test_bug611189.html"]
+
+["test_bug612730.html"]
+skip-if = ["os == 'android'"] # form control not selected/checked with synthesizeMouse
+
+["test_bug613019.html"]
+
+["test_bug613113.html"]
+
+["test_bug613722.html"]
+
+["test_bug613979.html"]
+
+["test_bug615595.html"]
+fail-if = ["xorigin"]
+
+["test_bug615833.html"]
+skip-if = [
+ "os == 'android'",
+ "os == 'mac'", #TIMED_OUT # form control not selected/checked with synthesizeMouse, osx(bug 1275664)
+]
+
+["test_bug618948.html"]
+
+["test_bug619278.html"]
+
+["test_bug622597.html"]
+
+["test_bug623291.html"]
+
+["test_bug629801.html"]
+
+["test_bug633058.html"]
+
+["test_bug636336.html"]
+
+["test_bug641219.html"]
+
+["test_bug643051.html"]
+
+["test_bug646157.html"]
+
+["test_bug649134.html"]
+# This extra subdirectory is needed due to the nature of this test.
+# With the bug, the test loads the base URL of the bug649134/file_*.sjs
+# files, and the mochitest server responds with the contents of index.html if
+# it exists in that case, which we use to detect failure.
+# We cannot have index.html in this directory because it would prevent
+# running the tests here.
+support-files = [
+ "bug649134/file_bug649134-1.sjs",
+ "bug649134/file_bug649134-2.sjs",
+ "bug649134/index.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug651956.html"]
+
+["test_bug658746.html"]
+
+["test_bug659596.html"]
+
+["test_bug659743.xml"]
+
+["test_bug660663.html"]
+
+["test_bug660959-1.html"]
+
+["test_bug660959-2.html"]
+
+["test_bug660959-3.html"]
+
+["test_bug666200.html"]
+
+["test_bug666666.html"]
+
+["test_bug669012.html"]
+
+["test_bug674558.html"]
+
+["test_bug674927.html"]
+
+["test_bug677495-1.html"]
+
+["test_bug677495.html"]
+
+["test_bug677658.html"]
+
+["test_bug682886.html"]
+
+["test_bug694503.html"]
+skip-if = ["os == 'android'"] # Bug 1525959
+
+["test_bug717819.html"]
+
+["test_bug741266.html"]
+skip-if = [
+ "os == 'android'", # Android: needs control of popup window size
+ "display == 'wayland' && os_version == '22.04' && debug", # Bug 1856975
+]
+
+["test_bug742030.html"]
+
+["test_bug742549.html"]
+
+["test_bug745685.html"]
+
+["test_bug763626.html"]
+
+["test_bug765780.html"]
+
+["test_bug780993.html"]
+
+["test_bug787134.html"]
+
+["test_bug797113.html"]
+
+["test_bug803677.html"]
+
+["test_bug821307.html"]
+
+["test_bug827126.html"]
+
+["test_bug838582.html"]
+
+["test_bug839371.html"]
+
+["test_bug839913.html"]
+
+["test_bug841466.html"]
+
+["test_bug845057.html"]
+
+["test_bug869040.html"]
+
+["test_bug870787.html"]
+
+["test_bug871161.html"]
+support-files = [
+ "file_bug871161-1.html",
+ "file_bug871161-2.html",
+]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug874758.html"]
+
+["test_bug879319.html"]
+
+["test_bug885024.html"]
+
+["test_bug893537.html"]
+
+["test_bug969346.html"]
+
+["test_bug982039.html"]
+
+["test_bug1003539.html"]
+
+["test_bug1013316.html"]
+
+["test_bug1045270.html"]
+
+["test_bug1089326.html"]
+
+["test_bug1146116.html"]
+
+["test_bug1166138.html"]
+
+["test_bug1203668.html"]
+
+["test_bug1230665.html"]
+
+["test_bug1250401.html"]
+
+["test_bug1260664.html"]
+
+["test_bug1260704.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1261673.html"]
+skip-if = [
+ "os == 'android'",
+ "os == 'mac'",
+]
+
+["test_bug1261674-1.html"]
+skip-if = [
+ "os == 'android'",
+ "os == 'mac'",
+]
+
+["test_bug1261674-2.html"]
+skip-if = ["os == 'mac'"]
+
+["test_bug1264157.html"]
+
+["test_bug1279218.html"]
+
+["test_bug1287321.html"]
+
+["test_bug1292522_same_domain_with_different_port_number.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1295719_event_sequence_for_arrow_keys.html"]
+skip-if = ["os == 'android'"] # up/down arrow keys not supported on android
+
+["test_bug1295719_event_sequence_for_number_keys.html"]
+
+["test_bug1310865.html"]
+
+["test_bug1315146.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_bug1322678.html"]
+skip-if = ["os == 'android'"]
+
+["test_bug1323815.html"]
+
+["test_bug1472426.html"]
+
+["test_bug1785739.html"]
+
+["test_change_crossorigin.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_checked.html"]
+
+["test_dir_attributes_reflection.html"]
+
+["test_dl_attributes_reflection.html"]
+
+["test_document-element-inserted.html"]
+
+["test_documentAll.html"]
+
+["test_element_prototype.html"]
+
+["test_embed_attributes_reflection.html"]
+
+["test_fakepath.html"]
+
+["test_filepicker_default_directory.html"]
+
+["test_focusshift_button.html"]
+
+["test_form-parsing.html"]
+
+["test_formData.html"]
+
+["test_formSubmission.html"]
+skip-if = ["os == 'android'"] #TIMED_OUT
+
+["test_formSubmission2.html"]
+skip-if = ["os == 'android'"]
+
+["test_formelements.html"]
+
+["test_fragment_form_pointer.html"]
+
+["test_frame_count_with_synthetic_doc.html"]
+
+["test_getElementsByName_after_mutation.html"]
+
+["test_hidden.html"]
+
+["test_html_attributes_reflection.html"]
+
+["test_htmlcollection.html"]
+
+["test_iframe_sandbox_general.html"]
+tags = "openwindow"
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_iframe_sandbox_inheritance.html"]
+tags = "openwindow"
+
+["test_iframe_sandbox_navigation.html"]
+tags = "openwindow"
+
+["test_iframe_sandbox_navigation2.html"]
+tags = "openwindow"
+
+["test_iframe_sandbox_popups.html"]
+tags = "openwindow"
+
+["test_iframe_sandbox_popups_inheritance.html"]
+tags = "openwindow"
+
+["test_iframe_sandbox_redirect.html"]
+
+["test_iframe_sandbox_refresh.html"]
+
+["test_iframe_sandbox_same_origin.html"]
+
+["test_iframe_sandbox_workers.html"]
+
+["test_imageSrcSet.html"]
+
+["test_image_clone_load.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_img_attributes_reflection.html"]
+
+["test_input_file_cancel_event.html"]
+
+["test_input_files_not_nsIFile.html"]
+
+["test_input_lastInteractiveValue.html"]
+
+["test_inputmode.html"]
+
+["test_li_attributes_reflection.html"]
+
+["test_link_attributes_reflection.html"]
+
+["test_link_sizes.html"]
+
+["test_map_attributes_reflection.html"]
+
+["test_meta_attributes_reflection.html"]
+
+["test_mod_attributes_reflection.html"]
+
+["test_multipleFilePicker.html"]
+
+["test_named_options.html"]
+
+["test_nested_invalid_fieldsets.html"]
+
+["test_nestediframe.html"]
+
+["test_non-ascii-cookie.html"]
+support-files = ["file_cookiemanager.js"]
+skip-if = [
+ "xorigin",
+ "http3",
+ "http2",
+]
+
+["test_object_attributes_reflection.html"]
+
+["test_ol_attributes_reflection.html"]
+
+["test_option_defaultSelected.html"]
+
+["test_option_selected_state.html"]
+
+["test_param_attributes_reflection.html"]
+
+["test_q_attributes_reflection.html"]
+
+["test_restore_from_parser_fragment.html"]
+
+["test_rowscollection.html"]
+
+["test_script_module.html"]
+support-files = ["file_script_module.html"]
+
+["test_set_input_files.html"]
+
+["test_srcdoc-2.html"]
+
+["test_srcdoc.html"]
+
+["test_style_attributes_reflection.html"]
+
+["test_track.html"]
+
+["test_ul_attributes_reflection.html"]
+
+["test_viewport_resize.html"]
+
+["test_window_open_close.html"]
+tags = "openwindow"
+skip-if = [
+ "os == 'android' && debug",
+ "os == 'linux'",
+ "os == 'win' && debug && bits == 64", # Bug 1533759
+]
+
+["test_window_open_from_closing.html"]
+skip-if = ["os == 'android'"] # test does not function on android due to aggressive background tab freezing
+support-files = [
+ "file_window_close_and_open.html",
+ "file_broadcast_load.html",
+]
diff --git a/dom/html/test/nnc_lockup.gif b/dom/html/test/nnc_lockup.gif
new file mode 100644
index 0000000000..f746bb71d9
--- /dev/null
+++ b/dom/html/test/nnc_lockup.gif
Binary files differ
diff --git a/dom/html/test/object_bug287465_o1.html b/dom/html/test/object_bug287465_o1.html
new file mode 100644
index 0000000000..0a65a7f9e1
--- /dev/null
+++ b/dom/html/test/object_bug287465_o1.html
@@ -0,0 +1 @@
+<svg xmlns='http://www.w3.org/2000/svg'></svg>
diff --git a/dom/html/test/object_bug287465_o2.html b/dom/html/test/object_bug287465_o2.html
new file mode 100644
index 0000000000..18ecdcb795
--- /dev/null
+++ b/dom/html/test/object_bug287465_o2.html
@@ -0,0 +1 @@
+<html></html>
diff --git a/dom/html/test/object_bug556645.html b/dom/html/test/object_bug556645.html
new file mode 100644
index 0000000000..773837502a
--- /dev/null
+++ b/dom/html/test/object_bug556645.html
@@ -0,0 +1 @@
+<body><button>Child</button></body>
diff --git a/dom/html/test/post_action_page.html b/dom/html/test/post_action_page.html
new file mode 100644
index 0000000000..ba6ae514f2
--- /dev/null
+++ b/dom/html/test/post_action_page.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Submission Flush Test Post Action Page</title>
+ </head>
+ <body>
+ <h1>Post Action Page</h1>
+ </body>
+</html>
diff --git a/dom/html/test/reflect.js b/dom/html/test/reflect.js
new file mode 100644
index 0000000000..44f73ae4a2
--- /dev/null
+++ b/dom/html/test/reflect.js
@@ -0,0 +1,1078 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * reflect.js is a collection of methods to test HTML attribute reflection.
+ * Each of attribute is reflected differently, depending on various parameters,
+ * see:
+ * http://www.whatwg.org/html/#reflecting-content-attributes-in-idl-attributes
+ *
+ * Do not forget to add these line at the beginning of each new reflect* method:
+ * ok(attr in element, attr + " should be an IDL attribute of this element");
+ * is(typeof element[attr], <type>, attr + " IDL attribute should be a <type>");
+ */
+
+/**
+ * Checks that a given attribute is correctly reflected as a string.
+ *
+ * @param aParameters Object object containing the parameters, which are:
+ * - element Element node to test
+ * - attribute String name of the attribute
+ * OR
+ * attribute Object object containing two attributes, 'content' and 'idl'
+ * - otherValues Array [optional] other values to test in addition of the default ones
+ * - extendedAttributes Object object which can have 'TreatNullAs': "EmptyString"
+ */
+function reflectString(aParameters) {
+ var element = aParameters.element;
+ var contentAttr =
+ typeof aParameters.attribute === "string"
+ ? aParameters.attribute
+ : aParameters.attribute.content;
+ var idlAttr =
+ typeof aParameters.attribute === "string"
+ ? aParameters.attribute
+ : aParameters.attribute.idl;
+ var otherValues =
+ aParameters.otherValues !== undefined ? aParameters.otherValues : [];
+ var treatNullAs = aParameters.extendedAttributes
+ ? aParameters.extendedAttributes.TreatNullAs
+ : null;
+
+ ok(
+ idlAttr in element,
+ idlAttr + " should be an IDL attribute of this element"
+ );
+ is(
+ typeof element[idlAttr],
+ "string",
+ "'" + idlAttr + "' IDL attribute should be a string"
+ );
+
+ // Tests when the attribute isn't set.
+ is(
+ element.getAttribute(contentAttr),
+ null,
+ "When not set, the content attribute should be null."
+ );
+ is(
+ element[idlAttr],
+ "",
+ "When not set, the IDL attribute should return the empty string"
+ );
+
+ /**
+ * TODO: as long as null stringification doesn't follow the WebIDL
+ * specifications, don't add it to the loop below and keep it here.
+ */
+ element.setAttribute(contentAttr, null);
+ is(
+ element.getAttribute(contentAttr),
+ "null",
+ "null should have been stringified to 'null' for '" + contentAttr + "'"
+ );
+ is(
+ element[idlAttr],
+ "null",
+ "null should have been stringified to 'null' for '" + idlAttr + "'"
+ );
+ element.removeAttribute(contentAttr);
+
+ element[idlAttr] = null;
+ if (treatNullAs == "EmptyString") {
+ is(
+ element.getAttribute(contentAttr),
+ "",
+ "null should have been stringified to '' for '" + contentAttr + "'"
+ );
+ is(
+ element[idlAttr],
+ "",
+ "null should have been stringified to '' for '" + idlAttr + "'"
+ );
+ } else {
+ is(
+ element.getAttribute(contentAttr),
+ "null",
+ "null should have been stringified to 'null' for '" + contentAttr + "'"
+ );
+ is(
+ element[idlAttr],
+ "null",
+ "null should have been stringified to 'null' for '" + contentAttr + "'"
+ );
+ }
+ element.removeAttribute(contentAttr);
+
+ // Tests various strings.
+ var stringsToTest = [
+ // [ test value, expected result ]
+ ["", ""],
+ ["null", "null"],
+ ["undefined", "undefined"],
+ ["foo", "foo"],
+ [contentAttr, contentAttr],
+ [idlAttr, idlAttr],
+ // TODO: uncomment this when null stringification will follow the specs.
+ // [ null, "null" ],
+ [undefined, "undefined"],
+ [true, "true"],
+ [false, "false"],
+ [42, "42"],
+ // ES5, verse 8.12.8.
+ [
+ {
+ toString() {
+ return "foo";
+ },
+ },
+ "foo",
+ ],
+ [
+ {
+ valueOf() {
+ return "foo";
+ },
+ },
+ "[object Object]",
+ ],
+ [
+ {
+ valueOf() {
+ return "quux";
+ },
+ toString: undefined,
+ },
+ "quux",
+ ],
+ [
+ {
+ valueOf() {
+ return "foo";
+ },
+ toString() {
+ return "bar";
+ },
+ },
+ "bar",
+ ],
+ ];
+
+ otherValues.forEach(function (v) {
+ stringsToTest.push([v, v]);
+ });
+
+ stringsToTest.forEach(function ([v, r]) {
+ element.setAttribute(contentAttr, v);
+ is(
+ element[idlAttr],
+ r,
+ "IDL attribute '" +
+ idlAttr +
+ "' should return the value it has been set to."
+ );
+ is(
+ element.getAttribute(contentAttr),
+ r,
+ "Content attribute '" +
+ contentAttr +
+ "'should return the value it has been set to."
+ );
+ element.removeAttribute(contentAttr);
+
+ element[idlAttr] = v;
+ is(
+ element[idlAttr],
+ r,
+ "IDL attribute '" +
+ idlAttr +
+ "' should return the value it has been set to."
+ );
+ is(
+ element.getAttribute(contentAttr),
+ r,
+ "Content attribute '" +
+ contentAttr +
+ "' should return the value it has been set to."
+ );
+ element.removeAttribute(contentAttr);
+ });
+
+ // Tests after removeAttribute() is called. Should be equivalent with not set.
+ is(
+ element.getAttribute(contentAttr),
+ null,
+ "When not set, the content attribute should be null."
+ );
+ is(
+ element[idlAttr],
+ "",
+ "When not set, the IDL attribute should return the empty string"
+ );
+}
+
+/**
+ * Checks that a given attribute name for a given element is correctly reflected
+ * as an unsigned int.
+ *
+ * @param aParameters Object object containing the parameters, which are:
+ * - element Element node to test on
+ * - attribute String name of the attribute
+ * - nonZero Boolean whether the attribute should be non-null
+ * - defaultValue Integer [optional] default value, if different from the default one
+ */
+function reflectUnsignedInt(aParameters) {
+ var element = aParameters.element;
+ var attr = aParameters.attribute;
+ var nonZero = aParameters.nonZero;
+ var defaultValue = aParameters.defaultValue;
+ var fallback = aParameters.fallback;
+
+ if (defaultValue === undefined) {
+ if (nonZero) {
+ defaultValue = 1;
+ } else {
+ defaultValue = 0;
+ }
+ }
+
+ if (fallback === undefined) {
+ fallback = false;
+ }
+
+ ok(attr in element, attr + " should be an IDL attribute of this element");
+ is(
+ typeof element[attr],
+ "number",
+ attr + " IDL attribute should be a number"
+ );
+
+ // Check default value.
+ is(element[attr], defaultValue, "default value should be " + defaultValue);
+ ok(!element.hasAttribute(attr), attr + " shouldn't be present");
+
+ var values = [1, 3, 42, 2147483647];
+
+ for (var value of values) {
+ element[attr] = value;
+ is(element[attr], value, "." + attr + " should be equals " + value);
+ is(
+ element.getAttribute(attr),
+ String(value),
+ "@" + attr + " should be equals " + value
+ );
+
+ element.setAttribute(attr, value);
+ is(element[attr], value, "." + attr + " should be equals " + value);
+ is(
+ element.getAttribute(attr),
+ String(value),
+ "@" + attr + " should be equals " + value
+ );
+ }
+
+ // -3000000000 is equivalent to 1294967296 when using the IDL attribute.
+ element[attr] = -3000000000;
+ is(element[attr], 1294967296, "." + attr + " should be equals to 1294967296");
+ is(
+ element.getAttribute(attr),
+ "1294967296",
+ "@" + attr + " should be equals to 1294967296"
+ );
+
+ // When setting the content attribute, it's a string so it will be invalid.
+ element.setAttribute(attr, -3000000000);
+ is(
+ element.getAttribute(attr),
+ "-3000000000",
+ "@" + attr + " should be equals to " + -3000000000
+ );
+ is(
+ element[attr],
+ defaultValue,
+ "." + attr + " should be equals to " + defaultValue
+ );
+
+ // When interpreted as unsigned 32-bit integers, all of these fall between
+ // 2^31 and 2^32 - 1, so per spec they return the default value.
+ var nonValidValues = [-2147483648, -1, 3147483647];
+
+ for (var value of nonValidValues) {
+ element[attr] = value;
+ is(
+ element.getAttribute(attr),
+ String(defaultValue),
+ "@" + attr + " should be equals to " + defaultValue
+ );
+ is(
+ element[attr],
+ defaultValue,
+ "." + attr + " should be equals to " + defaultValue
+ );
+ }
+
+ for (var values of nonValidValues) {
+ element.setAttribute(attr, values[0]);
+ is(
+ element.getAttribute(attr),
+ String(values[0]),
+ "@" + attr + " should be equals to " + values[0]
+ );
+ is(
+ element[attr],
+ defaultValue,
+ "." + attr + " should be equals to " + defaultValue
+ );
+ }
+
+ // Setting to 0 should throw an error if nonZero is true.
+ var caught = false;
+ try {
+ element[attr] = 0;
+ } catch (e) {
+ caught = true;
+ is(e.name, "IndexSizeError", "exception should be IndexSizeError");
+ is(
+ e.code,
+ DOMException.INDEX_SIZE_ERR,
+ "exception code should be INDEX_SIZE_ERR"
+ );
+ }
+
+ if (nonZero && !fallback) {
+ ok(caught, "an exception should have been caught");
+ } else {
+ ok(!caught, "no exception should have been caught");
+ }
+
+ // If 0 is set in @attr, it will be ignored when calling .attr.
+ element.setAttribute(attr, "0");
+ is(element.getAttribute(attr), "0", "@" + attr + " should be equals to 0");
+ if (nonZero) {
+ is(
+ element[attr],
+ defaultValue,
+ "." + attr + " should be equals to " + defaultValue
+ );
+ } else {
+ is(element[attr], 0, "." + attr + " should be equals to 0");
+ }
+}
+
+/**
+ * Checks that a given attribute is correctly reflected as limited to known
+ * values enumerated attribute.
+ *
+ * @param aParameters Object object containing the parameters, which are:
+ * - element Element node to test on
+ * - attribute String name of the attribute
+ * OR
+ * attribute Object object containing two attributes, 'content' and 'idl'
+ * - validValues Array valid values we support
+ * - invalidValues Array invalid values
+ * - defaultValue String [optional] default value when no valid value is set
+ * OR
+ * defaultValue Object [optional] object containing two attributes, 'invalid' and 'missing'
+ * - unsupportedValues Array [optional] valid values we do not support
+ * - nullable boolean [optional] whether the attribute is nullable
+ */
+function reflectLimitedEnumerated(aParameters) {
+ var element = aParameters.element;
+ var contentAttr =
+ typeof aParameters.attribute === "string"
+ ? aParameters.attribute
+ : aParameters.attribute.content;
+ var idlAttr =
+ typeof aParameters.attribute === "string"
+ ? aParameters.attribute
+ : aParameters.attribute.idl;
+ var validValues = aParameters.validValues;
+ var invalidValues = aParameters.invalidValues;
+ var defaultValueInvalid =
+ aParameters.defaultValue === undefined
+ ? ""
+ : typeof aParameters.defaultValue === "string"
+ ? aParameters.defaultValue
+ : aParameters.defaultValue.invalid;
+ var defaultValueMissing =
+ aParameters.defaultValue === undefined
+ ? ""
+ : typeof aParameters.defaultValue === "string"
+ ? aParameters.defaultValue
+ : aParameters.defaultValue.missing;
+ var unsupportedValues =
+ aParameters.unsupportedValues !== undefined
+ ? aParameters.unsupportedValues
+ : [];
+ var nullable = aParameters.nullable;
+
+ ok(
+ idlAttr in element,
+ idlAttr + " should be an IDL attribute of this element"
+ );
+ if (nullable) {
+ // The missing value default is null, which is typeof == "object"
+ is(
+ typeof element[idlAttr],
+ "object",
+ "'" +
+ idlAttr +
+ "' IDL attribute should be null, which has typeof == object"
+ );
+ is(
+ element[idlAttr],
+ null,
+ "'" + idlAttr + "' IDL attribute should be null"
+ );
+ } else {
+ is(
+ typeof element[idlAttr],
+ "string",
+ "'" + idlAttr + "' IDL attribute should be a string"
+ );
+ }
+
+ if (nullable) {
+ element.setAttribute(contentAttr, "something");
+ // Now it will be a string
+ is(
+ typeof element[idlAttr],
+ "string",
+ "'" + idlAttr + "' IDL attribute should be a string"
+ );
+ }
+
+ // Explicitly check the default value.
+ element.removeAttribute(contentAttr);
+ is(
+ element[idlAttr],
+ defaultValueMissing,
+ "When no attribute is set, the value should be the default value."
+ );
+
+ // Check valid values.
+ validValues.forEach(function (v) {
+ element.setAttribute(contentAttr, v);
+ is(
+ element[idlAttr],
+ v,
+ "'" + v + "' should be accepted as a valid value for " + idlAttr
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v,
+ "Content attribute should return the value it has been set to."
+ );
+ element.removeAttribute(contentAttr);
+
+ element.setAttribute(contentAttr, v.toUpperCase());
+ is(
+ element[idlAttr],
+ v,
+ "Enumerated attributes should be case-insensitive."
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v.toUpperCase(),
+ "Content attribute should not be lower-cased."
+ );
+ element.removeAttribute(contentAttr);
+
+ element[idlAttr] = v;
+ is(
+ element[idlAttr],
+ v,
+ "'" + v + "' should be accepted as a valid value for " + idlAttr
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v,
+ "Content attribute should return the value it has been set to."
+ );
+ element.removeAttribute(contentAttr);
+
+ element[idlAttr] = v.toUpperCase();
+ is(
+ element[idlAttr],
+ v,
+ "Enumerated attributes should be case-insensitive."
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v.toUpperCase(),
+ "Content attribute should not be lower-cased."
+ );
+ element.removeAttribute(contentAttr);
+ });
+
+ // Check invalid values.
+ invalidValues.forEach(function (v) {
+ element.setAttribute(contentAttr, v);
+ is(
+ element[idlAttr],
+ defaultValueInvalid,
+ "When the content attribute is set to an invalid value, the default value should be returned."
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v,
+ "Content attribute should not have been changed."
+ );
+ element.removeAttribute(contentAttr);
+
+ element[idlAttr] = v;
+ is(
+ element[idlAttr],
+ defaultValueInvalid,
+ "When the value is set to an invalid value, the default value should be returned."
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v,
+ "Content attribute should not have been changed."
+ );
+ element.removeAttribute(contentAttr);
+ });
+
+ // Check valid values we currently do not support.
+ // Basically, it's like the checks for the valid values but with some todo's.
+ unsupportedValues.forEach(function (v) {
+ element.setAttribute(contentAttr, v);
+ todo_is(
+ element[idlAttr],
+ v,
+ "'" + v + "' should be accepted as a valid value for " + idlAttr
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v,
+ "Content attribute should return the value it has been set to."
+ );
+ element.removeAttribute(contentAttr);
+
+ element.setAttribute(contentAttr, v.toUpperCase());
+ todo_is(
+ element[idlAttr],
+ v,
+ "Enumerated attributes should be case-insensitive."
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v.toUpperCase(),
+ "Content attribute should not be lower-cased."
+ );
+ element.removeAttribute(contentAttr);
+
+ element[idlAttr] = v;
+ todo_is(
+ element[idlAttr],
+ v,
+ "'" + v + "' should be accepted as a valid value for " + idlAttr
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v,
+ "Content attribute should return the value it has been set to."
+ );
+ element.removeAttribute(contentAttr);
+
+ element[idlAttr] = v.toUpperCase();
+ todo_is(
+ element[idlAttr],
+ v,
+ "Enumerated attributes should be case-insensitive."
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v.toUpperCase(),
+ "Content attribute should not be lower-cased."
+ );
+ element.removeAttribute(contentAttr);
+ });
+
+ if (nullable) {
+ is(
+ defaultValueMissing,
+ null,
+ "Missing default value should be null for nullable attributes"
+ );
+ ok(validValues.length, "We better have at least one valid value");
+ element.setAttribute(contentAttr, validValues[0]);
+ ok(
+ element.hasAttribute(contentAttr),
+ "Should have content attribute: we just set it"
+ );
+ element[idlAttr] = null;
+ ok(
+ !element.hasAttribute(contentAttr),
+ "Should have removed content attribute"
+ );
+ }
+}
+
+/**
+ * Checks that a given attribute is correctly reflected as a boolean.
+ *
+ * @param aParameters Object object containing the parameters, which are:
+ * - element Element node to test on
+ * - attribute String name of the attribute
+ * OR
+ * attribute Object object containing two attributes, 'content' and 'idl'
+ */
+function reflectBoolean(aParameters) {
+ var element = aParameters.element;
+ var contentAttr =
+ typeof aParameters.attribute === "string"
+ ? aParameters.attribute
+ : aParameters.attribute.content;
+ var idlAttr =
+ typeof aParameters.attribute === "string"
+ ? aParameters.attribute
+ : aParameters.attribute.idl;
+
+ ok(
+ idlAttr in element,
+ idlAttr + " should be an IDL attribute of this element"
+ );
+ is(
+ typeof element[idlAttr],
+ "boolean",
+ idlAttr + " IDL attribute should be a boolean"
+ );
+
+ // Tests when the attribute isn't set.
+ is(
+ element.getAttribute(contentAttr),
+ null,
+ "When not set, the content attribute should be null."
+ );
+ is(
+ element[idlAttr],
+ false,
+ "When not set, the IDL attribute should return false"
+ );
+
+ /**
+ * Test various values.
+ * Each value to test is actually an object containing a 'value' property
+ * containing the value to actually test, a 'stringified' property containing
+ * the stringified value and a 'result' property containing the expected
+ * result when the value is set to the IDL attribute.
+ */
+ var valuesToTest = [
+ { value: true, stringified: "true", result: true },
+ { value: false, stringified: "false", result: false },
+ { value: "true", stringified: "true", result: true },
+ { value: "false", stringified: "false", result: true },
+ { value: "foo", stringified: "foo", result: true },
+ { value: idlAttr, stringified: idlAttr, result: true },
+ { value: contentAttr, stringified: contentAttr, result: true },
+ { value: "null", stringified: "null", result: true },
+ { value: "undefined", stringified: "undefined", result: true },
+ { value: "", stringified: "", result: false },
+ { value: undefined, stringified: "undefined", result: false },
+ { value: null, stringified: "null", result: false },
+ { value: +0, stringified: "0", result: false },
+ { value: -0, stringified: "0", result: false },
+ { value: NaN, stringified: "NaN", result: false },
+ { value: 42, stringified: "42", result: true },
+ { value: Infinity, stringified: "Infinity", result: true },
+ { value: -Infinity, stringified: "-Infinity", result: true },
+ // ES5, verse 9.2.
+ {
+ value: {
+ toString() {
+ return "foo";
+ },
+ },
+ stringified: "foo",
+ result: true,
+ },
+ {
+ value: {
+ valueOf() {
+ return "foo";
+ },
+ },
+ stringified: "[object Object]",
+ result: true,
+ },
+ {
+ value: {
+ valueOf() {
+ return "quux";
+ },
+ toString: undefined,
+ },
+ stringified: "quux",
+ result: true,
+ },
+ {
+ value: {
+ valueOf() {
+ return "foo";
+ },
+ toString() {
+ return "bar";
+ },
+ },
+ stringified: "bar",
+ result: true,
+ },
+ {
+ value: {
+ valueOf() {
+ return false;
+ },
+ },
+ stringified: "[object Object]",
+ result: true,
+ },
+ {
+ value: { foo: false, bar: false },
+ stringified: "[object Object]",
+ result: true,
+ },
+ { value: {}, stringified: "[object Object]", result: true },
+ ];
+
+ valuesToTest.forEach(function (v) {
+ element.setAttribute(contentAttr, v.value);
+ is(
+ element[idlAttr],
+ true,
+ "IDL attribute should return always return 'true' if the content attribute has been set"
+ );
+ is(
+ element.getAttribute(contentAttr),
+ v.stringified,
+ "Content attribute should return the stringified value it has been set to."
+ );
+ element.removeAttribute(contentAttr);
+
+ element[idlAttr] = v.value;
+ is(element[idlAttr], v.result, "IDL attribute should return " + v.result);
+ is(
+ element.getAttribute(contentAttr),
+ v.result ? "" : null,
+ v.result
+ ? "Content attribute should return the empty string."
+ : "Content attribute should return null."
+ );
+ is(
+ element.hasAttribute(contentAttr),
+ v.result,
+ v.result
+ ? contentAttr + " should not be present"
+ : contentAttr + " should be present"
+ );
+ element.removeAttribute(contentAttr);
+ });
+
+ // Tests after removeAttribute() is called. Should be equivalent with not set.
+ is(
+ element.getAttribute(contentAttr),
+ null,
+ "When not set, the content attribute should be null."
+ );
+ is(
+ element[contentAttr],
+ false,
+ "When not set, the IDL attribute should return false"
+ );
+}
+
+/**
+ * Checks that a given attribute name for a given element is correctly reflected
+ * as an signed integer.
+ *
+ * @param aParameters Object object containing the parameters, which are:
+ * - element Element node to test on
+ * - attribute String name of the attribute
+ * - nonNegative Boolean true if the attribute is limited to 'non-negative numbers', false otherwise
+ * - defaultValue Integer [optional] default value, if one exists
+ */
+function reflectInt(aParameters) {
+ // Expected value returned by .getAttribute() when |value| has been previously passed to .setAttribute().
+ function expectedGetAttributeResult(value) {
+ return String(value);
+ }
+
+ function stringToInteger(value, nonNegative, defaultValue) {
+ // Parse: Ignore leading whitespace, find [+/-][numbers]
+ var result = /^[ \t\n\f\r]*([\+\-]?[0-9]+)/.exec(value);
+ if (result) {
+ var resultInt = parseInt(result[1], 10);
+ if (
+ (nonNegative ? 0 : -0x80000000) <= resultInt &&
+ resultInt <= 0x7fffffff
+ ) {
+ // If the value is within allowed value range for signed/unsigned
+ // integer, return it -- but add 0 to it to convert a possible -0 into
+ // +0, the only zero present in the signed integer range.
+ return resultInt + 0;
+ }
+ }
+ return defaultValue;
+ }
+
+ // Expected value returned by .getAttribute(attr) or .attr if |value| has been set via the IDL attribute.
+ function expectedIdlAttributeResult(value) {
+ // This returns the result of calling the ES ToInt32 algorithm on value.
+ return value << 0;
+ }
+
+ var element = aParameters.element;
+ var attr = aParameters.attribute;
+ var nonNegative = aParameters.nonNegative;
+
+ var defaultValue =
+ aParameters.defaultValue !== undefined
+ ? aParameters.defaultValue
+ : nonNegative
+ ? -1
+ : 0;
+
+ ok(attr in element, attr + " should be an IDL attribute of this element");
+ is(
+ typeof element[attr],
+ "number",
+ attr + " IDL attribute should be a number"
+ );
+
+ // Check default value.
+ is(element[attr], defaultValue, "default value should be " + defaultValue);
+ ok(!element.hasAttribute(attr), attr + " shouldn't be present");
+
+ /**
+ * Test various values.
+ * value: The test value that will be set using both setAttribute(value) and
+ * element[attr] = value
+ */
+ var valuesToTest = [
+ // Test numeric inputs up to max signed integer
+ 0,
+ 1,
+ 55555,
+ 2147483647,
+ +42,
+ // Test string inputs up to max signed integer
+ "0",
+ "1",
+ "777777",
+ "2147483647",
+ "+42",
+ // Test negative numeric inputs up to min signed integer
+ -0,
+ -1,
+ -3333,
+ -2147483648,
+ // Test negative string inputs up to min signed integer
+ "-0",
+ "-1",
+ "-222",
+ "-2147483647",
+ "-2147483648",
+ // Test numeric inputs that are outside legal 32 bit signed values
+ -2147483649,
+ -3000000000,
+ -4294967296,
+ 2147483649,
+ 4000000000,
+ -4294967297,
+ // Test string inputs with extra padding
+ " 1111111",
+ " 23456 ",
+ // Test non-numeric string inputs
+ "",
+ " ",
+ "+",
+ "-",
+ "foo",
+ "+foo",
+ "-foo",
+ "+ foo",
+ "- foo",
+ "+-2",
+ "-+2",
+ "++2",
+ "--2",
+ "hello1234",
+ "1234hello",
+ "444 world 555",
+ "why 567 what",
+ "-3 nots",
+ "2e5",
+ "300e2",
+ "42+-$",
+ "+42foo",
+ "-514not",
+ "\vblah",
+ "0x10FFFF",
+ "-0xABCDEF",
+ // Test decimal numbers
+ 1.2345,
+ 42.0,
+ 3456789.1,
+ -2.3456,
+ -6789.12345,
+ -2147483649.1234,
+ // Test decimal strings
+ "1.2345",
+ "42.0",
+ "3456789.1",
+ "-2.3456",
+ "-6789.12345",
+ "-2147483649.1234",
+ // Test special values
+ undefined,
+ null,
+ NaN,
+ Infinity,
+ -Infinity,
+ ];
+
+ valuesToTest.forEach(function (v) {
+ var intValue = stringToInteger(v, nonNegative, defaultValue);
+
+ element.setAttribute(attr, v);
+
+ is(
+ element.getAttribute(attr),
+ expectedGetAttributeResult(v),
+ element.localName +
+ ".setAttribute(" +
+ attr +
+ ", " +
+ v +
+ "), " +
+ element.localName +
+ ".getAttribute(" +
+ attr +
+ ") "
+ );
+
+ is(
+ element[attr],
+ intValue,
+ element.localName +
+ ".setAttribute(" +
+ attr +
+ ", " +
+ v +
+ "), " +
+ element.localName +
+ "[" +
+ attr +
+ "] "
+ );
+ element.removeAttribute(attr);
+
+ if (nonNegative && expectedIdlAttributeResult(v) < 0) {
+ try {
+ element[attr] = v;
+ ok(
+ false,
+ element.localName +
+ "[" +
+ attr +
+ "] = " +
+ v +
+ " should throw IndexSizeError"
+ );
+ } catch (e) {
+ is(
+ e.name,
+ "IndexSizeError",
+ element.localName +
+ "[" +
+ attr +
+ "] = " +
+ v +
+ " should throw IndexSizeError"
+ );
+ is(
+ e.code,
+ DOMException.INDEX_SIZE_ERR,
+ element.localName +
+ "[" +
+ attr +
+ "] = " +
+ v +
+ " should throw INDEX_SIZE_ERR"
+ );
+ }
+ } else {
+ element[attr] = v;
+ is(
+ element[attr],
+ expectedIdlAttributeResult(v),
+ element.localName +
+ "[" +
+ attr +
+ "] = " +
+ v +
+ ", " +
+ element.localName +
+ "[" +
+ attr +
+ "] "
+ );
+ is(
+ element.getAttribute(attr),
+ String(expectedIdlAttributeResult(v)),
+ element.localName +
+ "[" +
+ attr +
+ "] = " +
+ v +
+ ", " +
+ element.localName +
+ ".getAttribute(" +
+ attr +
+ ") "
+ );
+ }
+ element.removeAttribute(attr);
+ });
+
+ // Tests after removeAttribute() is called. Should be equivalent with not set.
+ is(
+ element.getAttribute(attr),
+ null,
+ "When not set, the content attribute should be null."
+ );
+ is(
+ element[attr],
+ defaultValue,
+ "When not set, the IDL attribute should return default value."
+ );
+}
+
+/**
+ * Checks that a given attribute is correctly reflected as a url.
+ *
+ * @param aParameters Object object containing the parameters, which are:
+ * - element Element node to test
+ * - attribute String name of the attribute
+ * OR
+ * attribute Object object containing two attributes, 'content' and 'idl'
+ */
+function reflectURL(aParameters) {
+ var element = aParameters.element;
+ var contentAttr =
+ typeof aParameters.attribute === "string"
+ ? aParameters.attribute
+ : aParameters.attribute.content;
+ var idlAttr =
+ typeof aParameters.attribute === "string"
+ ? aParameters.attribute
+ : aParameters.attribute.idl;
+
+ element[idlAttr] = "";
+ is(
+ element[idlAttr],
+ document.URL,
+ "Empty string should resolve to document URL"
+ );
+}
diff --git a/dom/html/test/script_fakepath.js b/dom/html/test/script_fakepath.js
new file mode 100644
index 0000000000..f95ac493d2
--- /dev/null
+++ b/dom/html/test/script_fakepath.js
@@ -0,0 +1,16 @@
+/* eslint-env mozilla/chrome-script */
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["File"]);
+
+addMessageListener("file.open", function (e) {
+ var tmpFile = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIDirectoryService)
+ .QueryInterface(Ci.nsIProperties)
+ .get("ProfD", Ci.nsIFile);
+ tmpFile.append("prefs.js");
+
+ File.createFromNsIFile(tmpFile).then(file => {
+ sendAsyncMessage("file.opened", { data: [file] });
+ });
+});
diff --git a/dom/html/test/simpleFileOpener.js b/dom/html/test/simpleFileOpener.js
new file mode 100644
index 0000000000..cb98f0c64e
--- /dev/null
+++ b/dom/html/test/simpleFileOpener.js
@@ -0,0 +1,38 @@
+/* eslint-env mozilla/chrome-script */
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["File"]);
+
+var file;
+
+addMessageListener("file.open", function (stem) {
+ try {
+ if (!file) {
+ file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+ file.append(stem);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ }
+
+ File.createFromNsIFile(file).then(function (domFile) {
+ sendAsyncMessage("file.opened", {
+ fullPath: file.path,
+ leafName: file.leafName,
+ domFile,
+ });
+ });
+ } catch (e) {
+ sendAsyncMessage("fail", e.toString());
+ }
+});
+
+addMessageListener("file.remove", function () {
+ try {
+ file.remove(/* recursive: */ false);
+ file = undefined;
+ sendAsyncMessage("file.removed", null);
+ } catch (e) {
+ sendAsyncMessage("fail", e.toString());
+ }
+});
diff --git a/dom/html/test/submission_flush.html b/dom/html/test/submission_flush.html
new file mode 100644
index 0000000000..f70884c66a
--- /dev/null
+++ b/dom/html/test/submission_flush.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Submission Flush Test</title>
+ </head>
+ <body>
+ <form id="test_form" action="post_action_page.html" target="form_target" method="POST" onsubmit="return false;">
+ <button type="submit" id="submit_button">Submit</button>
+ </form>
+ <iframe name="form_target" id="test_frame"></iframe>
+ </body>
+</html>
diff --git a/dom/html/test/sw_formSubmission.js b/dom/html/test/sw_formSubmission.js
new file mode 100644
index 0000000000..2e102ac74c
--- /dev/null
+++ b/dom/html/test/sw_formSubmission.js
@@ -0,0 +1,36 @@
+/**
+ * We are used by test_formSubmission.html to immediately activate and start
+ * controlling its page. We operate in 3 modes, conveyed via ?MODE appended to
+ * our URL.
+ *
+ * - "no-fetch": Don't register a fetch listener so that the optimized fetch
+ * event bypass happens.
+ * - "reset-fetch": Do register a fetch listener, reset every interception.
+ * - "proxy-fetch": Do register a fetch listener, resolve every interception
+ * with fetch(event.request).
+ */
+
+const mode = location.search.slice(1);
+
+// Fetch handling.
+if (mode !== "no-fetch") {
+ addEventListener("fetch", function (event) {
+ if (mode === "reset-fetch") {
+ // Don't invoke respondWith, resetting the interception.
+ return;
+ }
+ if (mode === "proxy-fetch") {
+ // Per the spec, there's an automatic waitUntil() on this too.
+ event.respondWith(fetch(event.request));
+ }
+ });
+}
+
+// Go straight to activation, bypassing waiting.
+addEventListener("install", function (event) {
+ event.waitUntil(skipWaiting());
+});
+// Control the test document ASAP.
+addEventListener("activate", function (event) {
+ event.waitUntil(clients.claim());
+});
diff --git a/dom/html/test/test_a_text.html b/dom/html/test/test_a_text.html
new file mode 100644
index 0000000000..5ffc1995f8
--- /dev/null
+++ b/dom/html/test/test_a_text.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for a.text</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <link rel="help" href="http://www.whatwg.org/html/#dom-a-text"/>
+</head>
+<body>
+<div id="content">
+<a href="a">a b c</a>
+<a href="b">a <!--b--> c</a>
+<a href="c">a <b>b</b> c</a>
+</div>
+<pre id="test">
+<script>
+var d = document.getElementById("content")
+ .appendChild(document.createElement("a"));
+d.href = "d";
+d.appendChild(document.createTextNode("a "));
+d.appendChild(document.createTextNode("b "));
+d.appendChild(document.createTextNode("c "));
+var expected = ["a b c", "a c", "a b c", "a b c "];
+var list = document.getElementById("content").getElementsByTagName("a");
+for (var i = 0, il = list.length; i < il; ++i) {
+ is(list[i].text, list[i].textContent);
+ is(list[i].text, expected[i]);
+
+ list[i].text = "x";
+ is(list[i].text, "x");
+ is(list[i].textContent, "x");
+ is(list[i].firstChild.data, "x");
+ is(list[i].childNodes.length, 1);
+
+ list[i].textContent = "y";
+ is(list[i].text, "y");
+ is(list[i].textContent, "y");
+ is(list[i].firstChild.data, "y");
+ is(list[i].childNodes.length, 1);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_allowMedia.html b/dom/html/test/test_allowMedia.html
new file mode 100644
index 0000000000..46a692283a
--- /dev/null
+++ b/dom/html/test/test_allowMedia.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=759964
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 759964</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 759964 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runNextTest);
+
+var SJS = `${location.origin}/tests/dom/html/test/allowMedia.sjs`;
+var TEST_PAGE = "data:text/html,<audio src='" + SJS + "?audio'></audio>";
+
+function runNextTest() {
+ var test = tests.shift();
+ if (!test) {
+ SimpleTest.finish();
+ return;
+ }
+ test();
+}
+
+var tests = [
+
+ // Set allowMedia = false, load a page with <audio>, verify the <audio>
+ // doesn't load its source.
+ function basic() {
+ var iframe = insertIframe();
+ SpecialPowers.allowMedia(iframe.contentWindow, false);
+ loadIframe(iframe, TEST_PAGE, function () {
+ verifyPass();
+ iframe.remove();
+ runNextTest();
+ });
+ },
+
+ // Set allowMedia = false on parent docshell, load a page with <audio> in a
+ // child iframe, verify the <audio> doesn't load its source.
+ function inherit() {
+ SpecialPowers.allowMedia(window, false);
+
+ var iframe = insertIframe();
+ loadIframe(iframe, TEST_PAGE, function () {
+ verifyPass();
+ iframe.remove();
+ SpecialPowers.allowMedia(window, true);
+ runNextTest();
+ });
+ },
+
+ // In a display:none iframe, set allowMedia = false, load a page with <audio>,
+ // verify the <audio> doesn't load its source.
+ function displayNone() {
+ var iframe = insertIframe();
+ iframe.style.display = "none";
+ SpecialPowers.allowMedia(iframe.contentWindow, false);
+ loadIframe(iframe, TEST_PAGE, function () {
+ verifyPass();
+ iframe.remove();
+ runNextTest();
+ });
+ },
+];
+
+function insertIframe() {
+ var iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ return iframe;
+}
+
+function loadIframe(iframe, url, onDone) {
+ iframe.setAttribute("src", url);
+ iframe.addEventListener("load", onDone);
+}
+
+function verifyPass() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", SJS, false);
+ xhr.send();
+ is(xhr.responseText, "PASS", "<audio> source should not have been loaded.");
+}
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=759964">Mozilla Bug 759964</a>
+<p id="display">
+</p>
+</body>
+</html>
diff --git a/dom/html/test/test_anchor_href_cache_invalidation.html b/dom/html/test/test_anchor_href_cache_invalidation.html
new file mode 100644
index 0000000000..c1a8327e62
--- /dev/null
+++ b/dom/html/test/test_anchor_href_cache_invalidation.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for anchor cache invalidation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <a id="x" href="http://example.com"></a>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+is($("x").href, "http://example.com/");
+is($("x").host, "example.com");
+
+$("x").href = "http://www.example.com";
+
+is($("x").href, "http://www.example.com/");
+is($("x").host, "www.example.com");
+
+$("x").setAttribute("href", "http://www.example.net/");
+is($("x").host, "www.example.net");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_anchor_ping.html b/dom/html/test/test_anchor_ping.html
new file mode 100644
index 0000000000..513e5ee05f
--- /dev/null
+++ b/dom/html/test/test_anchor_ping.html
@@ -0,0 +1,304 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=786347
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 786347</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 786347 **/
+
+SimpleTest.waitForExplicitFinish();
+
+const {NetUtil} = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+const {HttpServer} = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+addLoadEvent(function () {
+ (async function run_tests() {
+ while (tests.length) {
+ let test = tests.shift();
+ info("-- running " + test.name);
+ await test();
+ }
+
+ SimpleTest.finish();
+ })();
+});
+
+let tests = [
+
+ // Ensure that sending pings is enabled.
+ function setup() {
+ Services.prefs.setBoolPref("browser.send_pings", true);
+ Services.prefs.setIntPref("browser.send_pings.max_per_link", -1);
+
+ SimpleTest.registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.send_pings");
+ Services.prefs.clearUserPref("browser.send_pings.max_per_link");
+ });
+ },
+
+ // If both the address of the document containing the hyperlink being audited
+ // and ping URL have the same origin then the request must include a Ping-From
+ // HTTP header with, as its value, the address of the document containing the
+ // hyperlink, and a Ping-To HTTP header with, as its value, the target URL.
+ // The request must not include a Referer (sic) HTTP header.
+ async function same_origin() {
+ let from = "/ping-from/" + Math.random();
+ let to = "/ping-to/" + Math.random();
+ let ping = "/ping/" + Math.random();
+
+ let base;
+ let server = new HttpServer();
+
+ // The page that contains the link.
+ createFromPathHandler(server, from, to, () => ping);
+
+ // The page that the link's href points to.
+ let promiseHref = createToPathHandler(server, to);
+
+ // The ping we want to receive.
+ let promisePing = createPingPathHandler(server, ping, () => {
+ return {from: base + from, to: base + to};
+ });
+
+ // Start the server, get its base URL and run the test.
+ server.start(-1);
+ base = "http://localhost:" + server.identity.primaryPort;
+ navigate(base + from);
+
+ // Wait until the target and ping url have loaded.
+ await Promise.all([promiseHref, promisePing]);
+
+ // Cleanup.
+ await stopServer(server);
+ },
+
+ // If the origins are different, but the document containing the hyperlink
+ // being audited was not retrieved over an encrypted connection then the
+ // request must include a Referer (sic) HTTP header with, as its value, the
+ // address of the document containing the hyperlink, a Ping-From HTTP header
+ // with the same value, and a Ping-To HTTP header with, as its value, target
+ // URL.
+ async function diff_origin() {
+ let from = "/ping-from/" + Math.random();
+ let to = "/ping-to/" + Math.random();
+ let ping = "/ping/" + Math.random();
+
+ // We will use two servers to simulate two different origins.
+ let base, base2;
+ let server = new HttpServer();
+ let server2 = new HttpServer();
+
+ // The page that contains the link.
+ createFromPathHandler(server, from, to, () => base2 + ping);
+
+ // The page that the link's href points to.
+ let promiseHref = createToPathHandler(server, to);
+
+ // Start the first server and get its base URL.
+ server.start(-1);
+ base = "http://localhost:" + server.identity.primaryPort;
+
+ // The ping we want to receive.
+ let promisePing = createPingPathHandler(server2, ping, () => {
+ return {referrer: base + from, from: base + from, to: base + to};
+ });
+
+ // Start the second server, get its base URL and run the test.
+ server2.start(-1);
+ base2 = "http://localhost:" + server2.identity.primaryPort;
+ navigate(base + from);
+
+ // Wait until the target and ping url have loaded.
+ await Promise.all([promiseHref, promisePing]);
+
+ // Cleanup.
+ await stopServer(server);
+ await stopServer(server2);
+ },
+
+ // If the origins are different and the document containing the hyperlink
+ // being audited was retrieved over an encrypted connection then the request
+ // must include a Ping-To HTTP header with, as its value, target URL. The
+ // request must neither include a Referer (sic) HTTP header nor include a
+ // Ping-From HTTP header.
+ async function diff_origin_secure_referrer() {
+ let ping = "/ping/" + Math.random();
+ let server = new HttpServer();
+
+ // The ping we want to receive.
+ let promisePing = createPingPathHandler(server, ping, () => {
+ return {to: "https://example.com/"};
+ });
+
+ // Start the server and run the test.
+ server.start(-1);
+
+ // The referrer will be loaded using a secure channel.
+ navigate("https://example.com/chrome/dom/html/test/" +
+ "file_anchor_ping.html?" + "http://127.0.0.1:" +
+ server.identity.primaryPort + ping);
+
+ // Wait until the ping has been sent.
+ await promisePing;
+
+ // Cleanup.
+ await stopServer(server);
+ },
+
+ // Test that the <a ping> attribute is properly tokenized using ASCII white
+ // space characters as separators.
+ async function tokenize_white_space() {
+ let from = "/ping-from/" + Math.random();
+ let to = "/ping-to/" + Math.random();
+
+ let base;
+ let server = new HttpServer();
+
+ let pings = [
+ "/ping1/" + Math.random(),
+ "/ping2/" + Math.random(),
+ "/ping3/" + Math.random(),
+ "/ping4/" + Math.random()
+ ];
+
+ // The page that contains the link.
+ createFromPathHandler(server, from, to, () => {
+ return " " + pings[0] + " \r " + pings[1] + " \t " +
+ pings[2] + " \n " + pings[3] + " ";
+ });
+
+ // The page that the link's href points to.
+ let promiseHref = createToPathHandler(server, to);
+
+ // The pings we want to receive.
+ let pingPathHandlers = createPingPathHandlers(server, pings, () => {
+ return {from: base + from, to: base + to};
+ });
+
+ // Start the server, get its base URL and run the test.
+ server.start(-1);
+ base = "http://localhost:" + server.identity.primaryPort;
+ navigate(base + from);
+
+ // Wait until the target and ping url have loaded.
+ await Promise.all([promiseHref, ...pingPathHandlers]);
+
+ // Cleanup.
+ await stopServer(server);
+ }
+];
+
+// Navigate the iframe used for testing to a new URL.
+function navigate(uri) {
+ document.getElementById("frame").src = uri;
+}
+
+// Registers a path handler for the given server that will serve a page
+// containing an <a ping> element. The page will automatically simulate
+// clicking the link after it has loaded.
+function createFromPathHandler(server, path, href, lazyPing) {
+ server.registerPathHandler(path, function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ let body = '<body onload="document.body.firstChild.click()">' +
+ '<a href="' + href + '" ping="' + lazyPing() + '"></a></body>';
+ response.write(body);
+ });
+}
+
+// Registers a path handler for the given server that will serve a simple empty
+// page we can use as the href attribute for links. It returns a promise that
+// will be resolved once the page has been requested.
+function createToPathHandler(server, path) {
+ return new Promise(resolve => {
+
+ server.registerPathHandler(path, function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write("OK");
+
+ resolve();
+ });
+
+ });
+}
+
+// Register multiple path handlers for the given server that will receive
+// pings as sent when an <a ping> element is clicked. This method uses
+// createPingPathHandler() defined below to ensure all headers are sent
+// and received as expected.
+function createPingPathHandlers(server, paths, lazyHeaders) {
+ return Array.from(paths, (path) => createPingPathHandler(server, path, lazyHeaders));
+}
+
+// Registers a path handler for the given server that will receive pings as
+// sent when an <a ping> element has been clicked. It will check that the
+// correct http method has been used, the post data is correct and all headers
+// are given as expected. It returns a promise that will be resolved once the
+// ping has been received.
+function createPingPathHandler(server, path, lazyHeaders) {
+ return new Promise(resolve => {
+
+ server.registerPathHandler(path, function (request, response) {
+ let headers = lazyHeaders();
+
+ is(request.method, "POST", "correct http method used");
+ is(request.getHeader("Ping-To"), headers.to, "valid ping-to header");
+
+ if ("from" in headers) {
+ is(request.getHeader("Ping-From"), headers.from, "valid ping-from header");
+ } else {
+ ok(!request.hasHeader("Ping-From"), "no ping-from header");
+ }
+
+ if ("referrer" in headers) {
+ let expectedReferrer = headers.referrer.match(/https?:\/\/[^\/]+\/?/i)[0];
+ is(request.getHeader("Referer"), expectedReferrer, "valid referer header");
+ } else {
+ ok(!request.hasHeader("Referer"), "no referer header");
+ }
+
+ let bs = request.bodyInputStream;
+ let body = NetUtil.readInputStreamToString(bs, bs.available());
+ is(body, "PING", "correct body sent");
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html;charset=utf-8", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write("OK");
+
+ resolve();
+ });
+
+ });
+}
+
+// Returns a promise that is resolved when the given http server instance has
+// been stopped.
+function stopServer(server) {
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+}
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=786347">Mozilla Bug 786347</a>
+<p id="display"></p>
+<iframe id="frame" />
+</body>
+</html>
diff --git a/dom/html/test/test_base_attributes_reflection.html b/dom/html/test/test_base_attributes_reflection.html
new file mode 100644
index 0000000000..cbb8955d5c
--- /dev/null
+++ b/dom/html/test/test_base_attributes_reflection.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLBaseElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLBaseElement attributes reflection **/
+
+// .href is sort of like a URL reflection, but with some special rules. Watch
+// out for that!
+reflectURL({
+ element: document.createElement("base"),
+ attribute: "href"
+});
+
+// .target
+reflectString({
+ element: document.createElement("base"),
+ attribute: "target"
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1003539.html b/dom/html/test/test_bug1003539.html
new file mode 100644
index 0000000000..cbdc1e9fbe
--- /dev/null
+++ b/dom/html/test/test_bug1003539.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1003539
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1003539</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1003539 **/
+// Refering to this specification: http://www.whatwg.org/specs/web-apps/current-work/multipage/tabular-data.html#dom-table-insertrow
+var tab;
+tab = document.createElement("table");
+tab.createTHead();
+tab.insertRow();
+is(tab.innerHTML, '<thead></thead><tbody><tr></tr></tbody>', "Row should be inserted in the tbody.");
+
+tab = document.createElement("table");
+tab.createTBody();
+tab.createTBody();
+tab.insertRow();
+is(tab.innerHTML, '<tbody></tbody><tbody><tr></tr></tbody>', "Row should be inserted in the last tbody.");
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1003539">Mozilla Bug 1003539</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug100533.html b/dom/html/test/test_bug100533.html
new file mode 100644
index 0000000000..29c52f4f0a
--- /dev/null
+++ b/dom/html/test/test_bug100533.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=100533
+-->
+<head>
+ <title>Test for Bug 100533</title>
+ <script type="text/javascript" src="/MochiKit/Base.js"></script>
+ <script type="text/javascript" src="/MochiKit/DOM.js"></script>
+ <script type="text/javascript" src="/MochiKit/Style.js"></script>
+ <script type="text/javascript" src="/MochiKit/Signal.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=100533">Mozilla Bug 100533</a>
+<p id="display"></p>
+<div id="content" >
+
+<button id="thebutton">Test</button>
+<iframe style='display: none;' src='bug100533_iframe.html' id='a'></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/** Test for Bug 100533 **/
+var submitIframeForm = function() {
+ $('a').contentDocument.getElementById('b').submit();
+}
+
+submitted = function() {
+ ok(true, "Finished. Form submits when located in iframe set to display:none;");
+ SimpleTest.finish();
+};
+
+addLoadEvent(function() {
+ connect("thebutton", "click", submitIframeForm);
+ signal("thebutton", "click");
+});
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug1013316.html b/dom/html/test/test_bug1013316.html
new file mode 100644
index 0000000000..fdb9e5363d
--- /dev/null
+++ b/dom/html/test/test_bug1013316.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1013316
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1013316</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1013316 **/
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(function() {
+ is(Object.keys(document.all).length, 15, "We have 15 indexed props");
+ var props = Object.getOwnPropertyNames(document.all);
+ is(props.length, 20, "Should have five names");
+ is(props[15], "display", "display first");
+ is(props[16], "content", "content second");
+ is(props[17], "bar", "bar third");
+ is(props[18], "foo", "foo fourth");
+ is(props[19], "test", "test fifth");
+
+ is(Object.keys(document.images).length, 2, "We have 2 indexed props");
+ props = Object.getOwnPropertyNames(document.images);
+ is(props.length, 5, "Should have 3 names");
+ is(props[2], "display", "display first on document.images");
+ is(props[3], "bar", "bar second on document.images");
+ is(props[4], "foo", "foo third on document.images");
+ SimpleTest.finish();
+ })
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1013316">Mozilla Bug 1013316</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <img id="display">
+ <img name="foo" id="bar">
+ <div name="baz">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1045270.html b/dom/html/test/test_bug1045270.html
new file mode 100644
index 0000000000..b0c81daf61
--- /dev/null
+++ b/dom/html/test/test_bug1045270.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+ <!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1045270
+-->
+ <head>
+ <title>Test for Bug 583514</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045270">Mozilla Bug 1045270</a>
+ <p id="display"></p>
+ <div id="content">
+ <input type=number>
+ </div>
+ <pre id="test">
+ <script type="application/javascript">
+
+ /** Test for Bug 1045270 **/
+
+ var input = document.querySelector("input");
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(function() {
+ input.focus();
+ input.addEventListener("input", function() {
+ // reframe
+ document.body.style.display = "none";
+ document.body.style.display = "";
+ document.body.offsetLeft; // flush
+ });
+ sendString("1");
+ SimpleTest.executeSoon(function() {
+ sendString("2");
+ SimpleTest.executeSoon(function() {
+ is(input.value, "12", "Reframe should restore focus and selection properly");
+ SimpleTest.finish();
+ });
+ });
+ });
+
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/html/test/test_bug1089326.html b/dom/html/test/test_bug1089326.html
new file mode 100644
index 0000000000..fed0a467cd
--- /dev/null
+++ b/dom/html/test/test_bug1089326.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1089326
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1089326</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1089326 **/
+ function test() {
+ var b = document.getElementById("button");
+ var b_rect = b.getBoundingClientRect();
+ var a = document.getElementById("anchor");
+ var a_rect = a.getBoundingClientRect();
+
+ is(document.elementFromPoint(b_rect.x + 1, b_rect.y + 1), b,
+ "Should find button when doing hit test on top of it.");
+ is(document.elementFromPoint(a_rect.x + 1, a_rect.y + 1), a,
+ "Should find anchor when doing hit test on top of it.");
+
+ var expectedTarget;
+ var clickCount = 0;
+ var container = document.getElementById("interactiveContentContainer");
+ container.addEventListener("click", function(event) {
+ is(event.target, expectedTarget, "Got expected click event target.");
+ ++clickCount;
+ }, true);
+ var i1 = document.getElementById("interactiveContent1");
+ var s11 = document.getElementById("s11");
+ var s12 = document.getElementById("s12");
+
+ var i2 = document.getElementById("interactiveContent2");
+ var s21 = document.getElementById("s21");
+
+ expectedTarget = i1;
+ synthesizeMouseAtCenter(s11, { type: "mousedown" });
+ synthesizeMouseAtCenter(s12, { type: "mouseup" });
+ is(clickCount, 1, "Should have got a click event.");
+
+ expectedTarget = container;
+ synthesizeMouseAtCenter(s11, { type: "mousedown" });
+ synthesizeMouseAtCenter(s21, { type: "mouseup" });
+ is(clickCount, 2, "Should not have got a click event.");
+
+ expectedTarget = container;
+ synthesizeMouseAtCenter(s21, { type: "mousedown" });
+ synthesizeMouseAtCenter(s11, { type: "mouseup" });
+ is(clickCount, 3, "Should not have got a click event.");
+
+ var span1 = document.getElementById("span1");
+ var span2 = document.getElementById("span2");
+ expectedTarget = container;
+ synthesizeMouseAtCenter(span1, { type: "mousedown" });
+ synthesizeMouseAtCenter(span2, { type: "mouseup" });
+ is(clickCount, 4, "Should not have got a click event.");
+
+ button.addEventListener("click", function(event) {
+ is(event.target, expectedTarget, "Got expected click event target.");
+ ++clickCount;
+ }, true);
+
+ expectedTarget = a;
+ synthesizeMouseAtCenter(a, { type: "mousedown" });
+ synthesizeMouseAtCenter(a, { type: "mouseup" });
+ is(clickCount, 5, "Should have got a click event.");
+
+ expectedTarget = a;
+ synthesizeMouseAtCenter(b, { type: "mousedown" });
+ synthesizeMouseAtCenter(b, { type: "mouseup" });
+ is(clickCount, 6, "Should have got a click event.");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(test);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1089326">Mozilla Bug 1089326</a>
+<p id="display"></p>
+<button id="button">button <a id="anchor" href="#">anchor</a>button</button>
+
+<div id="interactiveContentContainer">
+ <a id="interactiveContent1" href="#">foo <span id="s11">s11</span><span id="s12">s12</span> bar</a>
+ <a id="interactiveContent2" href="#">foo <span id="s21">s21</span><span id="s22">s22</span> bar</a>
+
+ <div>
+ <span>
+ <span id="span1">span1</span>
+ </span>
+ </div>
+
+ <div>
+ <span>
+ <span id="span2">span2</span>
+ </span>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug109445.html b/dom/html/test/test_bug109445.html
new file mode 100644
index 0000000000..27ffe22948
--- /dev/null
+++ b/dom/html/test/test_bug109445.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=109445
+-->
+<head>
+ <title>Test for Bug 109445</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=109445">Mozilla Bug 109445</a>
+<p id="display">
+<map name=a>
+<area shape=rect coords=25,25,75,75 href=#x>
+</map>
+<map id=b>
+<area shape=rect coords=25,25,75,75 href=#y>
+</map>
+<map name=a>
+<area shape=rect coords=25,25,75,75 href=#FAIL>
+</map>
+<map id=b>
+<area shape=rect coords=25,25,75,75 href=#FAIL>
+</map>
+
+<img usemap=#a src=image.png>
+<img usemap=#b src=image.png>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 109445 **/
+SimpleTest.waitForExplicitFinish();
+var images = document.getElementsByTagName("img");
+var second = false;
+onhashchange = function() {
+ if (!second) {
+ second = true;
+ is(location.hash, "#x", "First map");
+ SimpleTest.waitForFocus(() => synthesizeMouse(images[1], 50, 50, {}));
+ } else {
+ is(location.hash, "#y", "Second map");
+ SimpleTest.finish();
+ }
+};
+SimpleTest.waitForFocus(() => synthesizeMouse(images[0], 50, 50, {}));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug109445.xhtml b/dom/html/test/test_bug109445.xhtml
new file mode 100644
index 0000000000..b1524c8ead
--- /dev/null
+++ b/dom/html/test/test_bug109445.xhtml
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=109445
+-->
+<head>
+ <title>Test for Bug 109445</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=109445">Mozilla Bug 109445</a>
+<p id="display">
+<map name="a">
+<area shape="rect" coords="25,25,75,75" href="#x"/>
+</map>
+<map id="b">
+<area shape="rect" coords="25,25,75,75" href="#y"/>
+</map>
+<map name="a">
+<area shape="rect" coords="25,25,75,75" href="#FAIL"/>
+</map>
+<map id="b">
+<area shape="rect" coords="25,25,75,75" href="#FAIL"/>
+</map>
+
+<img usemap="#a" src="image.png"/>
+<img usemap="#b" src="image.png"/>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 109445 **/
+SimpleTest.waitForExplicitFinish();
+var images = document.getElementsByTagName("img");
+var second = false;
+onhashchange = function() {
+ if (!second) {
+ second = true;
+ is(location.hash, "#x", "First map");
+ SimpleTest.waitForFocus(() => synthesizeMouse(images[1], 50, 50, {}));
+ } else {
+ is(location.hash, "#y", "Second map");
+ SimpleTest.finish();
+ }
+};
+SimpleTest.waitForFocus(() => synthesizeMouse(images[0], 50, 50, {}));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1146116.html b/dom/html/test/test_bug1146116.html
new file mode 100644
index 0000000000..95d52af9eb
--- /dev/null
+++ b/dom/html/test/test_bug1146116.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1146116
+-->
+<head>
+ <title>Test for Bug 1146116</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1146116">Mozilla Bug 1146116</a>
+<p id="display">
+ <input type="file" id="file">
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for bug 1146116 **/
+
+SimpleTest.waitForExplicitFinish();
+
+const helperURL = SimpleTest.getTestFileURL("simpleFileOpener.js");
+const helper = SpecialPowers.loadChromeScript(helperURL);
+helper.addMessageListener("fail", function onFail(message) {
+ is(message, null, "chrome script failed");
+ SimpleTest.finish();
+});
+helper.addMessageListener("file.opened", onFileOpened);
+helper.sendAsyncMessage("file.open", "test_bug1146116.txt");
+
+function getGlobal(thing) {
+ return SpecialPowers.unwrap(SpecialPowers.Cu.getGlobalForObject(thing));
+}
+
+function onFileOpened(message) {
+ const file = message.domFile;
+ const elem = document.getElementById("file");
+ is(getGlobal(elem), window,
+ "getGlobal() works as expected");
+ is(getGlobal(file), window,
+ "File from MessageManager is not wrapped");
+ SpecialPowers.wrap(elem).mozSetFileArray([file]);
+ is(getGlobal(elem.files[0]), window,
+ "File read back from input element is not wrapped");
+ helper.addMessageListener("file.removed", onFileRemoved);
+ helper.sendAsyncMessage("file.remove", null);
+}
+
+function onFileRemoved() {
+ helper.destroy();
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1166138.html b/dom/html/test/test_bug1166138.html
new file mode 100644
index 0000000000..5b65db6c04
--- /dev/null
+++ b/dom/html/test/test_bug1166138.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1166138
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1166138</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1166138">Mozilla Bug 1166138</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+
+ <script type="application/javascript">
+ var img1x = `${location.origin}/tests/dom/html/test/file_bug1166138_1x.png`;
+ var img2x = `${location.origin}/tests/dom/html/test/file_bug1166138_2x.png`;
+ var imgdef = `${location.origin}/tests/dom/html/test/file_bug1166138_def.png`;
+ var onLoadCallback = null;
+ var done = false;
+
+ var startPromise = new Promise((a) => {
+ onLoadCallback = () => {
+ var image = document.querySelector('img');
+ // If we aren't starting at 2x scale, resize to 2x scale, and wait for a load
+ if (image.currentSrc != img2x) {
+ onLoadCallback = a;
+ SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 2]]});
+ } else {
+ a();
+ }
+ };
+ });
+
+ // if aLoad is true, waits for a load event. Otherwise, spins the event loop twice to
+ // ensure that no events were queued to be fired.
+ function spin(aLoad) {
+ if (aLoad) {
+ return new Promise((a) => {
+ ok(!onLoadCallback, "Shouldn't be an existing callback");
+ onLoadCallback = a;
+ });
+ } else {
+ return new Promise((a) => SimpleTest.executeSoon(() => SimpleTest.executeSoon(a)));
+ }
+ }
+
+ function onLoad() {
+ if (done) return;
+ ok(onLoadCallback, "Expected a load event");
+ if (onLoadCallback) {
+ var cb = onLoadCallback;
+ onLoadCallback = null;
+ cb();
+ }
+ }
+
+ add_task(async function() {
+ await startPromise;
+ var image = document.querySelector('img');
+ is(image.currentSrc, img2x, "initial scale must be 2x");
+
+ SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 1]]});
+ await spin(true);
+ is(image.currentSrc, img1x, "pre-existing img tag to 1x");
+
+ SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 2]]});
+ await spin(true);
+ is(image.currentSrc, img2x, "pre-existing img tag to 2x");
+
+ // Try removing & re-adding the image
+ document.body.removeChild(image);
+
+ SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 1]]});
+ await spin(false); // No load should occur because the element is unbound
+
+ document.body.appendChild(image);
+ await spin(true);
+ is(image.currentSrc, img1x, "remove and re-add tag after changing to 1x");
+
+ document.body.removeChild(image);
+ SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 2]]});
+ await spin(false); // No load should occur because the element is unbound
+
+ document.body.appendChild(image);
+ await spin(true);
+ is(image.currentSrc, img2x, "remove and re-add tag after changing to 2x");
+
+ // get rid of the srcset attribute! It should become the default
+ image.removeAttribute('srcset');
+ await spin(true);
+ is(image.currentSrc, imgdef, "remove srcset attribute");
+
+ // Setting srcset again should return it to the correct value
+ image.setAttribute('srcset', "file_bug1166138_1x.png 1x, file_bug1166138_2x.png 2x");
+ await spin(true);
+ is(image.currentSrc, img2x, "restore srcset attribute");
+
+ // Create a new image
+ var newImage = document.createElement('img');
+ // Switch load listening over to newImage
+ newImage.addEventListener('load', onLoad);
+ image.removeEventListener('load', onLoad);
+
+ document.body.appendChild(newImage);
+ await spin(false); // no load event should fire - as the image has no attributes
+ is(newImage.currentSrc, "", "New element with no attributes");
+ newImage.setAttribute('srcset', "file_bug1166138_1x.png 1x, file_bug1166138_2x.png 2x");
+ await spin(true);
+ is(newImage.currentSrc, img2x, "Adding srcset attribute");
+
+ SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 1]]});
+ await spin(true);
+ is(newImage.currentSrc, img1x, "new image after switching to 1x");
+ is(image.currentSrc, img1x, "old image after switching to 1x");
+
+ // Clear the listener
+ done = true;
+ });
+ </script>
+
+ <img srcset="file_bug1166138_1x.png 1x, file_bug1166138_2x.png 2x"
+ src="file_bug1166138_def.png"
+ onload="onLoad()">
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug1203668.html b/dom/html/test/test_bug1203668.html
new file mode 100644
index 0000000000..41249d90ab
--- /dev/null
+++ b/dom/html/test/test_bug1203668.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1203668
+-->
+<head>
+ <title>Test for Bug 1203668</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1203668">Mozilla Bug 1203668</a>
+<p id="display"></p>
+<div id="content">
+ <select class="select" multiple>
+ <option value="foo" selected>foo</option>
+ <option value="bar" selected>bar</option>
+ </select>
+ <select class="select" multiple>
+ <option value="foo">foo</option>
+ <option value="bar" selected>bar</option>
+ </select>
+ <select class="select" multiple>
+ <option value="foo">foo</option>
+ <option value="bar">bar</option>
+ </select>
+ <select class="select" size=1>
+ <option value="foo">foo</option>
+ <option value="bar" selected>bar</option>
+ </select>
+ <select class="select" size=1>
+ <option value="foo">foo</option>
+ <option value="bar">bar</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1203668 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function runTest()
+{
+ var selects = document.querySelectorAll('.select');
+ for (i=0; i < selects.length; i++) {
+ var select = selects[i];
+ select.value = "bogus"
+ is(select.selectedIndex, -1, "no option is selected");
+ is(select.children[0].selected, false, "first option is not selected");
+ is(select.children[1].selected, false, "second option is not selected");
+ }
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1230665.html b/dom/html/test/test_bug1230665.html
new file mode 100644
index 0000000000..cbe9c91d30
--- /dev/null
+++ b/dom/html/test/test_bug1230665.html
@@ -0,0 +1,46 @@
+<html>
+<head>
+ <title>Test for Bug 1230665</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ document.getElementById("flexbutton1").focus();
+ synthesizeKey("KEY_Tab");
+ var e = document.getElementById("flexbutton2");
+ is(document.activeElement, e, "focus in flexbutton2 after TAB");
+
+ document.getElementById("gridbutton1").focus();
+ synthesizeKey("KEY_Tab");
+ e = document.getElementById("gridbutton2");
+ is(document.activeElement, e, "focus in gridbutton2 after TAB");
+
+ SimpleTest.finish();
+});
+
+</script>
+
+<div tabindex="0" style="display:flex">
+ <button id="flexbutton1"></button>
+ text <!-- this text will force a :-moz-anonymous-flex-item frame -->
+ <div style="">
+ <button id="flexbutton2"></button>
+ </div>
+</div>
+
+
+<div tabindex="0" style="display:grid">
+ <button id="gridbutton1"></button>
+ text <!-- this text will force a :-moz-anonymous-grid-item frame -->
+ <div style="">
+ <button id="gridbutton2"></button>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug1250401.html b/dom/html/test/test_bug1250401.html
new file mode 100644
index 0000000000..d4a1073856
--- /dev/null
+++ b/dom/html/test/test_bug1250401.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1250401
+-->
+<head>
+ <title>Test for Bug 1250401</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1250401">Bug 1250401</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1250401 **/
+function test_add() {
+ var select = document.createElement("select");
+
+ var g1 = document.createElement("optgroup");
+ var o1 = document.createElement("option");
+ g1.appendChild(o1);
+ select.appendChild(g1);
+
+ var g2 = document.createElement("optgroup");
+ var o2 = document.createElement("option");
+ g2.appendChild(o2);
+ select.add(g2, 0);
+
+ is(select.children.length, 1, "Select has 1 item");
+ is(select.firstChild, g1, "First item is g1");
+ is(select.firstChild.children.length, 2, "g2 has 2 children");
+ is(select.firstChild.children[0], g2, "g1 has 2 children: g2");
+ is(select.firstChild.children[1], o1, "g1 has 2 children: o1");
+ is(o1.index, 0, "o1.index should be 0");
+ is(o2.index, 0, "o2.index should be 0");
+}
+
+function test_append() {
+ var select = document.createElement("select");
+
+ var g1 = document.createElement("optgroup");
+ var o1 = document.createElement("option");
+ g1.appendChild(o1);
+ select.appendChild(g1);
+
+ var g2 = document.createElement("optgroup");
+ var o2 = document.createElement("option");
+ g2.appendChild(o2);
+ g1.appendChild(g2);
+
+ is(select.children.length, 1, "Select has 1 item");
+ is(select.firstChild, g1, "First item is g1");
+ is(select.firstChild.children.length, 2, "g2 has 2 children");
+ is(select.firstChild.children[0], o1, "g1 has 2 children: o1");
+ is(select.firstChild.children[1], g2, "g1 has 2 children: g1");
+ is(o1.index, 0, "o1.index should be 0");
+ is(o2.index, 0, "o2.index should be 0");
+}
+
+function test_no_select() {
+ var g1 = document.createElement("optgroup");
+ var o1 = document.createElement("option");
+ g1.appendChild(o1);
+
+ var g2 = document.createElement("optgroup");
+ var o2 = document.createElement("option");
+ g2.appendChild(o2);
+ g1.appendChild(g2);
+
+ is(g1.children.length, 2, "g2 has 2 children");
+ is(g1.children[0], o1, "g1 has 2 children: o1");
+ is(g1.children[1], g2, "g1 has 2 children: g1");
+ is(o1.index, 0, "o1.index should be 0");
+ is(o2.index, 0, "o2.index should be 0");
+}
+
+function test_no_parent() {
+ var o1 = document.createElement("option");
+ var o2 = document.createElement("option");
+
+ is(o1.index, 0, "o1.index should be 0");
+ is(o2.index, 0, "o2.index should be 0");
+}
+
+test_add();
+test_append();
+test_no_select();
+test_no_parent();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1260664.html b/dom/html/test/test_bug1260664.html
new file mode 100644
index 0000000000..99878a46b6
--- /dev/null
+++ b/dom/html/test/test_bug1260664.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1260664
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1260664</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1260664">Mozilla Bug 1260664</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1260664 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+ var elements = [ "iframe", "img", "a", "area", "link", "script"];
+
+ for (var i = 0; i < elements.length; ++i) {
+ reflectLimitedEnumerated({
+ element: document.createElement(elements[i]),
+ attribute: { content: "referrerpolicy", idl: "referrerPolicy" },
+ validValues: [ "no-referrer",
+ "origin",
+ /** These 2 below values are still invalid, please see
+ Bug 1178337 - Valid referrer attribute values **/
+ /** "no-referrer-when-downgrade",
+ "origin-when-cross-origin", **/
+ "unsafe-url" ],
+ invalidValues: [
+ "", " orIgin ", " unsafe-uRl ", " No-RefeRRer ", " fOoBaR "
+ ],
+ defaultValue: "",
+ });
+ }
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1260704.html b/dom/html/test/test_bug1260704.html
new file mode 100644
index 0000000000..ca576051b0
--- /dev/null
+++ b/dom/html/test/test_bug1260704.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1260704
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1260704</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript">
+ /** Test for Bug 1260704 **/
+
+function runTests() {
+ let testIdx = -1;
+ let testUrls = [
+ "bug1260704_iframe.html?noDefault=true&isMap=true",
+ "bug1260704_iframe.html?noDefault=true&isMap=false",
+ "bug1260704_iframe.html?noDefault=false&isMap=true",
+ "bug1260704_iframe.html?noDefault=false&isMap=false"
+ ];
+
+ let runningTest = false;
+ let iframe = document.getElementById("testFrame");
+ let iframeWin = iframe.contentWindow;
+ let rect;
+ let x;
+ let y;
+
+ window.addEventListener("message", event => {
+ if (event.data == "started") {
+ ok(!runningTest, "Start to test " + testIdx);
+ runningTest = true;
+ rect = iframeWin.document.getElementById("testImage").getBoundingClientRect();
+ x = rect.width / 2;
+ y = rect.height / 2;
+ synthesizeMouseAtPoint(rect.left + x, rect.top + y, { type: 'mousedown' }, iframeWin);
+ synthesizeMouseAtPoint(rect.left + x, rect.top + y, { type: 'mouseup' }, iframeWin);
+ }
+ else if (runningTest && event.data == "empty_frame_loaded") {
+ ok(testUrls[testIdx].includes("noDefault=false"), "Page unload");
+ let search = iframeWin.location.search;
+ if (testUrls[testIdx].includes("isMap=true")) {
+ // url trigger by image with ismap attribute should contains coordinates
+ // try to parse coordinates and check them with small tolerance
+ let coorStr = search.split("?");
+ let coordinates = coorStr[1].split(",");
+ ok(Math.abs(coordinates[0] - x) <= 1, "expect X=" + x + " got " + coordinates[0]);
+ ok(Math.abs(coordinates[1] - y) <= 1, "expect Y=" + y + " got " + coordinates[1]);
+ } else {
+ ok(search == "", "expect empty search string got:" + search);
+ }
+ nextTest();
+ }
+ else if (runningTest && event.data == "finished") {
+ ok(testUrls[testIdx].includes("noDefault=true"), "Page should not leave");
+ nextTest();
+ }
+ });
+
+ function nextTest() {
+ testIdx++;
+ runningTest = false;
+ if (testIdx >= testUrls.length) {
+ SimpleTest.finish();
+ } else {
+ ok(true, "Test " + testIdx + " - Set url to " + testUrls[testIdx]);
+ iframeWin.location.href = testUrls[testIdx];
+ }
+ }
+ nextTest();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+ </script>
+</head>
+<body>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<iframe id="testFrame" src="about:blank" width="400" height="400">
+</iframe>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1261673.html b/dom/html/test/test_bug1261673.html
new file mode 100644
index 0000000000..c574967dd7
--- /dev/null
+++ b/dom/html/test/test_bug1261673.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1261673
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1261673</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261673">Mozilla Bug 1261673</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id="test_number" type="number" value=5>
+<script type="text/javascript">
+
+/** Test for Bug 1261673 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+ let input = window.document.getElementById("test_number");
+
+ // focus: whether the target input element is focused
+ // deltaY: deltaY of WheelEvent
+ // deltaMode: deltaMode of WheelEvent
+ // valueChanged: expected value changes after input element handled the wheel event
+ let params = [
+ {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: -1},
+ {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 1},
+ {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: -1},
+ {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: 1},
+ {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0},
+ {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0},
+ {focus: false, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0},
+ {focus: false, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0}
+ ];
+
+ let testIdx = 0;
+ let result = parseInt(input.value);
+ let numberChange = 0;
+ let expectChange = 0;
+
+ input.addEventListener("change", () => {
+ ++numberChange;
+ });
+
+ function runNext() {
+ let p = params[testIdx];
+ (p.focus) ? input.focus() : input.blur();
+ expectChange = p.valueChanged == 0 ? expectChange : expectChange + 1;
+ result += parseInt(p.valueChanged);
+ sendWheelAndPaint(input, 1, 1, { deltaY: p.deltaY, deltaMode: p.deltaMode }, () => {
+ ok(input.value == result,
+ "Handle wheel in number input test-" + testIdx + " expect " + result +
+ " get " + input.value);
+ ok(numberChange == expectChange,
+ "UA should fire change event when input's value changed, expect " + expectChange + " get " + numberChange);
+ (++testIdx >= params.length) ? SimpleTest.finish() : runNext();
+ });
+ }
+ runNext();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1261674-1.html b/dom/html/test/test_bug1261674-1.html
new file mode 100644
index 0000000000..a9042be733
--- /dev/null
+++ b/dom/html/test/test_bug1261674-1.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1261674
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1261674</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261674">Mozilla Bug 1261674</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id="test_input" type="range" value=5 max=10 min=0>
+<script type="text/javascript">
+
+/** Test for Bug 1261674 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+ let input = window.document.getElementById("test_input");
+
+ // focus: whether the target input element is focused
+ // deltaY: deltaY of WheelEvent
+ // deltaMode: deltaMode of WheelEvent
+ // valueChanged: expected value changes after input element handled the wheel event
+ let params = [
+ {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: -1},
+ {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 1},
+ {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: -1},
+ {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: 1},
+ {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0},
+ {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0},
+ {focus: false, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0},
+ {focus: false, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0}
+ ];
+
+ let testIdx = 0;
+ let result = parseInt(input.value);
+ let rangeChange = 0;
+ let expectChange = 0;
+
+ input.addEventListener("change", () => {
+ ++rangeChange;
+ });
+
+ function runNext() {
+ let p = params[testIdx];
+ (p.focus) ? input.focus() : input.blur();
+ expectChange = p.valueChanged == 0 ? expectChange : expectChange + 1;
+ result += parseInt(p.valueChanged);
+ sendWheelAndPaint(input, 1, 1, { deltaY: p.deltaY, deltaMode: p.deltaMode }, () => {
+ ok(input.value == result,
+ "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value);
+ ok(rangeChange == expectChange,
+ "UA should fire change event when input's value changed, expect " + expectChange + " get " + rangeChange);
+ (++testIdx >= params.length) ? SimpleTest.finish() : runNext();
+ });
+ }
+
+ input.addEventListener("input", () => {
+ ok(input.value == result,
+ "Test-" + testIdx + " receive input event, expect " + result + " get " + input.value);
+ });
+
+ runNext();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1261674-2.html b/dom/html/test/test_bug1261674-2.html
new file mode 100644
index 0000000000..cfda243749
--- /dev/null
+++ b/dom/html/test/test_bug1261674-2.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1261674
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1261674</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261674">Mozilla Bug 1261674</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id="test_input" type="range" max=0 min=10>
+<script type="text/javascript">
+
+/** Test for Bug 1261674 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+ let input = window.document.getElementById("test_input");
+
+ // deltaY: deltaY of WheelEvent
+ // deltaMode: deltaMode of WheelEvent
+ let params = [
+ {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE},
+ {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE},
+ {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE},
+ {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE},
+ {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL},
+ {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL},
+ {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE},
+ {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}
+ ];
+
+ let testIdx = 0;
+ let result = parseInt(input.value);
+ let rangeChange = 0;
+
+ input.addEventListener("change", () => {
+ ++rangeChange;
+ });
+
+ function runNext() {
+ let p = params[testIdx];
+ (p.focus) ? input.focus() : input.blur();
+ sendWheelAndPaint(input, 1, 1, { deltaY: p.deltaY, deltaMode: p.deltaMode }, () => {
+ ok(input.value == result,
+ "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value);
+ ok(rangeChange == 0, "Wheel event should not trigger change event when max < min");
+ testIdx++;
+ (testIdx >= params.length) ? SimpleTest.finish() : runNext();
+ });
+ }
+
+ input.addEventListener("input", () => {
+ ok(false, "Wheel event should be no effect to range input element with max < min");
+ });
+
+ runNext();
+}
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1264157.html b/dom/html/test/test_bug1264157.html
new file mode 100644
index 0000000000..1bede807da
--- /dev/null
+++ b/dom/html/test/test_bug1264157.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=535043
+-->
+<head>
+ <title>Test for Bug 535043</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input {
+ outline: 2px solid lime;
+ }
+ input:in-range {
+ outline: 2px solid red;
+ }
+ input:out-of-range {
+ outline: 2px solid orange;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=535043">Mozilla Bug 535043</a>
+<p id="display"></p>
+<div id="content">
+
+</head>
+<body>
+ <input type="number" value=0 min=0 max=10> Active in-range
+ <br><br>
+ <input type="number" value=0 min=0 max=10 disabled> Disabled in-range
+ <br><br>
+ <input type="number" value=0 min=0 max=10 readonly> Read-only in-range
+ <br><br>
+ <input type="number" value=11 min=0 max=10> Active out-of-range
+ <br><br>
+ <input type="number" value=11 min=0 max=10 disabled> Disabled out-of-range
+ <br><br>
+ <input type="number" value=11 min=0 max=10 readonly> Read-only out-of-range
+</div>
+<pre id="test">
+<script>
+
+/** Test for Bug 1264157 **/
+SimpleTest.waitForFocus(function() {
+ // Check the initial values.
+ let active = [].slice.call(document.querySelectorAll("input:not(:disabled):not(:read-only)"));
+ let disabled = [].slice.call(document.querySelectorAll("input:disabled"));
+ let readonly = [].slice.call(document.querySelectorAll("input:read-only:not(:disabled)"));
+ is(active.length, 2, "Test is messed up: missing non-disabled/non-readonly inputs");
+ is(disabled.length, 2, "Test is messed up: missing disabled inputs");
+ is(readonly.length, 2, "Test is messed up: missing readonly inputs");
+
+ is(document.querySelectorAll("input:in-range").length, 1,
+ "Wrong number of in-range elements selected.");
+ is(document.querySelectorAll("input:out-of-range").length, 1,
+ "Wrong number of out-of-range elements selected.");
+
+ // Dynamically change the values to see if that works too.
+ active[0].value = -1;
+ is(document.querySelectorAll("input:in-range").length, 0,
+ "Wrong number of in-range elements selected after value changed.");
+ is(document.querySelectorAll("input:out-of-range").length, 2,
+ "Wrong number of out-of-range elements selected after value changed.");
+ active[0].value = 0;
+ is(document.querySelectorAll("input:in-range").length, 1,
+ "Wrong number of in-range elements selected after value changed back.");
+ is(document.querySelectorAll("input:out-of-range").length, 1,
+ "Wrong number of out-of-range elements selected after value changed back.");
+
+ // Dynamically change the attributes to see if that works too.
+ disabled.forEach(function(e) { e.removeAttribute("disabled"); });
+ readonly.forEach(function(e) { e.removeAttribute("readonly"); });
+ active.forEach(function(e) { e.setAttribute("readonly", true); });
+
+ is(document.querySelectorAll("input:in-range").length, 2,
+ "Wrong number of in-range elements selected after attribute changed.");
+ is(document.querySelectorAll("input:out-of-range").length, 2,
+ "Wrong number of out-of-range elements selected after attribute changed.");
+
+ SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1279218.html b/dom/html/test/test_bug1279218.html
new file mode 100644
index 0000000000..0d8386280d
--- /dev/null
+++ b/dom/html/test/test_bug1279218.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test for Bug 1279218</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript">
+ function load() {
+ let applets = document.applets;
+ is(applets.length, 0, "Applet list length should be 0, even with applet tag in body");
+ SimpleTest.finish();
+ }
+
+ window.onload=load;
+
+ SimpleTest.waitForExplicitFinish();
+ </script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1279218">Mozilla Bug 1279218</a>
+ <applet id="applet-test"></applet>
+ </body>
+</html>
diff --git a/dom/html/test/test_bug1287321.html b/dom/html/test/test_bug1287321.html
new file mode 100644
index 0000000000..142b06d104
--- /dev/null
+++ b/dom/html/test/test_bug1287321.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1287321
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1287321</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1287321 **/
+
+ function test() {
+ var r = document.getElementById("range");
+ var rect = r.getBoundingClientRect();
+ var y = parseInt((rect.height / 2));
+ var movement = parseInt(rect.width / 10);
+ var x = movement;
+ synthesizeMouse(r, x, y, { type: "mousedown" });
+ x += movement;
+ var eventCount = 0;
+ r.oninput = function() {
+ ++eventCount;
+ }
+ synthesizeMouse(r, x, y, { type: "mousemove" });
+ is(eventCount, 1, "Got the expected input event");
+
+ x += movement;
+ synthesizeMouse(r, x, y, { type: "mousemove" });
+ is(eventCount, 2, "Got the expected input event");
+
+ synthesizeMouse(r, x, y, { type: "mousemove" });
+ is(eventCount, 2, "Got the expected input event");
+
+ x += movement;
+ synthesizeMouse(r, x, y, { type: "mousemove" });
+ is(eventCount, 3, "Got the expected input event");
+
+ synthesizeMouse(r, x, y, { type: "mouseup" });
+ is(eventCount, 3, "Got the expected input event");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.waitForFocus(test);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1287321">Mozilla Bug 1287321</a>
+<input type="range" id="range">
+</body>
+</html>
diff --git a/dom/html/test/test_bug1292522_same_domain_with_different_port_number.html b/dom/html/test/test_bug1292522_same_domain_with_different_port_number.html
new file mode 100644
index 0000000000..b7a443f6a7
--- /dev/null
+++ b/dom/html/test/test_bug1292522_same_domain_with_different_port_number.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1292522
+If we set domain using document.domain = "...", a page and iframe must be
+treated as the same domain if they differ in port number,
+e.g. test1.example.org:8000 and test2.example.org:80 are the same domain if
+document.domain = "example.org".
+-->
+<head>
+ <title>Test for Bug 1292522</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1292522">Mozilla Bug 1292522</a>
+ <p id="display"></p>
+
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+
+ if (navigator.platform.startsWith("Linux")) {
+ SimpleTest.expectAssertions(0, 1);
+ }
+ SimpleTest.waitForExplicitFinish();
+ window.addEventListener("message", onMessageReceived);
+
+ var page;
+
+ function onMessageReceived(event)
+ {
+ is(event.data, "testiframe", "Must be able to access the variable," +
+ " because page and iframe are the " +
+ "same domain.");
+ page.close();
+ SimpleTest.finish();
+ }
+
+ page = window.open("http://test1.example.org:8000/tests/dom/html/test/bug1292522_page.html");
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/html/test/test_bug1295719_event_sequence_for_arrow_keys.html b/dom/html/test/test_bug1295719_event_sequence_for_arrow_keys.html
new file mode 100644
index 0000000000..4e622391e8
--- /dev/null
+++ b/dom/html/test/test_bug1295719_event_sequence_for_arrow_keys.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1295719
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1295719</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1295719">Mozilla Bug 1295719</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id="test_number" type="number" value=50>
+<input id="test_range" type="range" value=50 max=100 min=0>
+<script type="text/javascript">
+
+/** Test for Bug 1295719 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+ let number = window.document.getElementById("test_number");
+ let range = window.document.getElementById("test_range");
+ let waiting_event_sequence = ["keydown", "input", "change"];
+ let waiting_event_idx = 0;
+ waiting_event_sequence.forEach((eventType) => {
+ number.addEventListener(eventType, (event) => {
+ let waiting_event = waiting_event_sequence[waiting_event_idx];
+ is(waiting_event, eventType, "Waiting " + waiting_event + " get " + eventType);
+ // Input element will fire input and change events when handling keypress
+ // with keycode=arrows. When user press and hold the keyboard, we expect
+ // that input element repeatedly fires "keydown"(, "keypress"), "input", and
+ // "change" events until user release the keyboard. Using
+ // waiting_event_sequence as a circular buffer and reset waiting_event_idx
+ // when it point to the end of buffer.
+ waiting_event_idx = waiting_event_idx == waiting_event_sequence.length -1 ? 0 : waiting_event_idx + 1;
+ });
+ range.addEventListener(eventType, (event) => {
+ let waiting_event = waiting_event_sequence[waiting_event_idx];
+ is(waiting_event, eventType, "Waiting " + waiting_event + " get " + eventType);
+ waiting_event_idx = waiting_event_idx == waiting_event_sequence.length - 1 ? 0 : waiting_event_idx + 1;
+ });
+ });
+
+ number.focus();
+ synthesizeKey("KEY_ArrowDown", {type: "keydown"});
+ synthesizeKey("KEY_ArrowDown", {type: "keydown"});
+ synthesizeKey("KEY_ArrowDown", {type: "keyup"});
+ number.blur();
+ range.focus();
+ waiting_event_idx = 0;
+ synthesizeKey("KEY_ArrowDown", {type: "keydown"});
+ synthesizeKey("KEY_ArrowDown", {type: "keydown"});
+ synthesizeKey("KEY_ArrowDown", {type: "keyup"});
+
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1295719_event_sequence_for_number_keys.html b/dom/html/test/test_bug1295719_event_sequence_for_number_keys.html
new file mode 100644
index 0000000000..f8f0537ddb
--- /dev/null
+++ b/dom/html/test/test_bug1295719_event_sequence_for_number_keys.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1295719
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1295719</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1295719">Mozilla Bug 1295719</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id="test_number" type="number" value=50>
+<script type="text/javascript">
+
+/** Test for Bug 1295719 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+ let number = window.document.getElementById("test_number");
+ let waiting_event_sequence = ["keydown", "keypress", "input"];
+ let change_event_of_number = 0;
+ let keyup_event_of_number = 0;
+ let waiting_event_idx = 0;
+ waiting_event_sequence.forEach((eventType) => {
+ number.addEventListener(eventType, (event) => {
+ let waiting_event = waiting_event_sequence[waiting_event_idx];
+ is(eventType, waiting_event, "Waiting " + waiting_event + " get " + eventType);
+ // Input element will fire input event when handling keypress with
+ // keycode=numbers. When user press and hold the keyboard, we expect that
+ // input element repeatedly fires "keydown", "keypress", and "input" until
+ // user release the keyboard. Input element will fire change event when
+ // it's blurred. Using waiting_event_sequence as a circular buffer and
+ // reset waiting_event_idx when it point to the end of buffer.
+ waiting_event_idx = waiting_event_idx == waiting_event_sequence.length - 1 ? 0 : waiting_event_idx + 1;
+ });
+ });
+ number.addEventListener("change", (event) => {
+ is(keyup_event_of_number, 1, "change event should be fired after blurred");
+ ++change_event_of_number;
+ });
+ number.addEventListener("keyup", (event) => {
+ is(keyup_event_of_number, 0, "keyup event should be fired once");
+ is(change_event_of_number, 0, "keyup event should be fired before change event");
+ ++keyup_event_of_number;
+ });
+ number.focus();
+ synthesizeKey("5", {type: "keydown"});
+ synthesizeKey("5", {type: "keydown"});
+ synthesizeKey("5", {type: "keyup"});
+ is(change_event_of_number, 0, "change event shouldn't be fired when input element is focused");
+ number.blur();
+ is(change_event_of_number, 1, "change event should be fired when input element is blurred");
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1297.html b/dom/html/test/test_bug1297.html
new file mode 100644
index 0000000000..d0c96c87d4
--- /dev/null
+++ b/dom/html/test/test_bug1297.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1297
+-->
+<head>
+ <title>Test for Bug 1297</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1297">Mozilla Bug 1297</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<table border=1>
+<tr>
+<td id="td1" onmousedown="alert(this.cellIndex)">cellIndex=0</td>
+<td id="td2" onmousedown="alert(this.cellIndex)">cellIndex=1</td>
+<td id="td3" onmousedown="alert(this.cellIndex)">cellIndex=2</td>
+<tr id="tr1"
+onmousedown="alert(this.rowIndex)"><td>rowIndex=1<td>rowIndex=1<td>rowIndex=1</t
+r>
+<tr id="tr2"
+onmousedown="alert(this.rowIndex)"><td>rowIndex=2<td>rowIndex=2<td>rowIndex=2</t
+r>
+</tr>
+</table>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1297 **/
+is($('td1').cellIndex, 0, "cellIndex / rowIndex working td1");
+is($('td2').cellIndex, 1, "cellIndex / rowIndex working td2");
+is($('td3').cellIndex, 2, "cellIndex / rowIndex working td3");
+is($('tr1').rowIndex, 1, "cellIndex / rowIndex working tr1");
+is($('tr2').rowIndex, 2, "cellIndex / rowIndex working tr2");
+
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug1310865.html b/dom/html/test/test_bug1310865.html
new file mode 100644
index 0000000000..4dcccbfa0d
--- /dev/null
+++ b/dom/html/test/test_bug1310865.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>Test for Bug 1310865</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<input value="a
+b" type="hidden">
+<input type="hidden" value="a
+b">
+<script>
+var input1 = document.querySelector("input");
+var input2 = document.querySelector("input + input");
+var clone1 = input1.cloneNode(false);
+var clone2 = input2.cloneNode(false);
+// Newlines must not be stripped
+is(clone1.value, "a\nb");
+is(clone2.value, "a\nb");
+</script>
diff --git a/dom/html/test/test_bug1315146.html b/dom/html/test/test_bug1315146.html
new file mode 100644
index 0000000000..0cf25b36bf
--- /dev/null
+++ b/dom/html/test/test_bug1315146.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1315146
+-->
+<head>
+ <title>Test for Bug 1315146</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315146">Mozilla Bug 1315146</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1315146 **/
+
+SimpleTest.waitForExplicitFinish();
+onmessage = function(e) {
+ win.close();
+ is(e.data.start, 2, "Correct start offset expected");
+ is(e.data.end, 2, "Correct end offset expected");
+ SimpleTest.finish();
+};
+let win = window.open("http://test1.example.org/tests/dom/html/test/bug1315146-main.html", "_blank");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1322678.html b/dom/html/test/test_bug1322678.html
new file mode 100644
index 0000000000..57b43f039c
--- /dev/null
+++ b/dom/html/test/test_bug1322678.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1322678
+-->
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Test for Bug 1322678</title>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript">
+
+const CUSTOM_TITLE = "Custom Title";
+
+async function openNewWindowForTest() {
+ let win = window.open("bug369370-popup.png", "bug1322678",
+ "width=400,height=300,scrollbars=no");
+ ok(win, "opened child window");
+
+ await new Promise(resolve => {
+ win.onload = function() {
+ ok(true, "window loaded");
+ resolve();
+ };
+ });
+
+ return win;
+}
+
+async function testCustomTitle(aWin, aTitle) {
+ let doc = aWin.document;
+ let elements = doc.getElementsByTagName("img");
+ is(elements.length, 1, "looking for img in ImageDocument");
+ let img = elements[0];
+
+ // Click to zoom in
+ synthesizeMouse(img, 25, 25, { }, aWin);
+ is(doc.title, aTitle, "Checking title");
+
+ // Click there again to zoom out
+ synthesizeMouse(img, 25, 25, { }, aWin);
+ is(doc.title, aTitle, "Checking title");
+
+ // Now try resizing the window so the image fits vertically and horizontally.
+ await new Promise(resolve => {
+ aWin.addEventListener("resize", function() {
+ // Give the image document time to respond
+ SimpleTest.executeSoon(function() {
+ is(doc.title, aTitle, "Checking title");
+ resolve();
+ });
+ }, {once: true});
+
+ let decorationSize = aWin.outerHeight - aWin.innerHeight;
+ aWin.resizeTo(800 + 50 + decorationSize, 600 + 50 + decorationSize);
+ });
+
+ // Now try resizing the window so the image no longer fits.
+ await new Promise(resolve => {
+ aWin.addEventListener("resize", function() {
+ // Give the image document time to respond
+ SimpleTest.executeSoon(function() {
+ is(doc.title, aTitle, "Checking title");
+ resolve();
+ });
+ }, {once: true});
+
+ aWin.resizeTo(400, 300);
+ });
+}
+
+// eslint-disable-next-line mozilla/no-addtask-setup
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["browser.enable_automatic_image_resizing", true],
+ ]});
+});
+
+add_task(async function testUpdateDocumentTitle() {
+ let win = await openNewWindowForTest();
+ // Set custom title.
+ win.document.title = CUSTOM_TITLE;
+ await testCustomTitle(win, CUSTOM_TITLE);
+ win.close();
+});
+
+add_task(async function testUpdateTitleElement() {
+ let win = await openNewWindowForTest();
+ // Set custom title.
+ let title = win.document.getElementsByTagName("title")[0];
+ title.text = CUSTOM_TITLE;
+ await testCustomTitle(win, CUSTOM_TITLE);
+ win.close();
+});
+
+add_task(async function testAppendNewTitleElement() {
+ let win = await openNewWindowForTest();
+ // Set custom title.
+ let doc = win.document;
+ doc.getElementsByTagName("title")[0].remove();
+ let title = doc.createElement("title");
+ title.text = CUSTOM_TITLE;
+ doc.head.appendChild(title);
+ await testCustomTitle(win, CUSTOM_TITLE);
+ win.close();
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1323815.html b/dom/html/test/test_bug1323815.html
new file mode 100644
index 0000000000..47e223aa7b
--- /dev/null
+++ b/dom/html/test/test_bug1323815.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1323815
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1323815</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1323815 **/
+
+SimpleTest.waitForExplicitFinish();
+function test() {
+ var n = document.getElementById("number");
+ var t = document.getElementById("text");
+ t.focus();
+ var gotBlur = false;
+ t.onblur = function(e) {
+ try {
+ is(e.relatedTarget.localName, "input");
+ } catch(ex) {
+ ok(false, "Accessing properties on the relatedTarget shouldn't throw! " + ex);
+ }
+ gotBlur = true;
+ }
+
+ n.focus();
+ ok(gotBlur);
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(test);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1323815">Mozilla Bug 1323815</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<input type="number" id="number"><input type="text" id="text">
+</body>
+</html>
diff --git a/dom/html/test/test_bug1366.html b/dom/html/test/test_bug1366.html
new file mode 100644
index 0000000000..f29179509f
--- /dev/null
+++ b/dom/html/test/test_bug1366.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1366
+-->
+<head>
+ <title>Test for Bug 1366</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1366">Mozilla Bug 1366</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<table id="testtable" width=150 border>
+ <tbody id="testbody">
+ <tr>
+ <td>cell content</td>
+ </tr>
+ </tbody>
+</table>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1366 **/
+$('testtable').removeChild($('testbody'));
+$('display').innerHTML = "SCRIPT: deleted first ROWGROUP\n";
+is($('testbody'), null, "deleting tbody works");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug1400.html b/dom/html/test/test_bug1400.html
new file mode 100644
index 0000000000..38e87a56da
--- /dev/null
+++ b/dom/html/test/test_bug1400.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1400
+-->
+<head>
+ <title>Test for Bug 1400</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1400">Mozilla Bug 1400</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1400 **/
+
+table = document.createElement("TABLE");
+thead = table.createTHead();
+thead2 = table.createTHead();
+
+table.appendChild(thead);
+table.appendChild(thead);
+table.appendChild(thead);
+table.appendChild(thead2);
+table.appendChild(thead2);
+table.appendChild(thead2);
+table.appendChild(thead);
+table.appendChild(thead2);
+
+is(table.childNodes.length, 1,
+ "adding multiple theads results in one thead child");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug1414077.html b/dom/html/test/test_bug1414077.html
new file mode 100644
index 0000000000..aa430c2737
--- /dev/null
+++ b/dom/html/test/test_bug1414077.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1414077
+-->
+<head>
+<meta charset="utf-8">
+<title>Test for Bug 1414077</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+
+/** Test for Bug 1414077 **/
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({"set": [["browser.enable_automatic_image_resizing", true]]});
+
+ return new Promise(resolve => {
+ var testWin = document.querySelector("iframe");
+ testWin.src = "image.png";
+ testWin.onload = function() {
+ var testDoc = testWin.contentDocument;
+
+ // testDoc should be a image document.
+ ok(testDoc.imageIsOverflowing, "image is overflowing");
+ ok(testDoc.imageIsResized, "image is resized to fit visible area by default");
+
+ // Restore image to original size.
+ testDoc.restoreImage();
+ ok(testDoc.imageIsOverflowing, "image is overflowing");
+ ok(!testDoc.imageIsResized, "image is restored to original size");
+
+ // Resize the image to fit visible area
+ testDoc.shrinkToFit();
+ ok(testDoc.imageIsOverflowing, "image is overflowing");
+ ok(testDoc.imageIsResized, "image is resized to fit visible area");
+
+ resolve();
+ };
+ })
+});
+
+</script>
+</head>
+
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414077">Mozilla Bug 1414077</a>
+<iframe width="0" height="0"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/test_bug143220.html b/dom/html/test/test_bug143220.html
new file mode 100644
index 0000000000..f94ec5571e
--- /dev/null
+++ b/dom/html/test/test_bug143220.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=143220
+-->
+<head>
+ <title>Test for Bug 143220</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=143220">Mozilla Bug 143220</a>
+<p id="display">
+ <input type="file" id="i1">
+ <input type="file" id="i2">
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 143220 **/
+SimpleTest.waitForExplicitFinish();
+const helperURL = SimpleTest.getTestFileURL("simpleFileOpener.js");
+const helper = SpecialPowers.loadChromeScript(helperURL);
+helper.addMessageListener("fail", function onFail(message) {
+ is(message, null, "chrome script failed");
+ SimpleTest.finish();
+});
+helper.addMessageListener("file.opened", onFileOpened);
+helper.sendAsyncMessage("file.open", "test_bug143220.txt");
+
+function onFileOpened(message) {
+ const { leafName, fullPath, domFile } = message;
+
+ function initControl1() {
+ SpecialPowers.wrap($("i1")).mozSetFileArray([domFile]);
+ }
+
+ function initControl2() {
+ SpecialPowers.wrap($("i2")).mozSetFileArray([domFile]);
+ }
+
+ // Check that we can't just set the value
+ try {
+ $("i1").value = fullPath;
+ is(0, 1, "Should have thrown exception on set!");
+ } catch(e) {
+ is($("i1").value, "", "Shouldn't have value here");
+ }
+
+ initControl1();
+ initControl2();
+
+ is($("i1").value, 'C:\\fakepath\\' + leafName, "Leaking full value?");
+ is($("i2").value, 'C:\\fakepath\\' + leafName, "Leaking full value?");
+
+ helper.addMessageListener("file.removed", onFileRemoved);
+ helper.sendAsyncMessage("file.remove", null);
+}
+
+function onFileRemoved() {
+ helper.destroy();
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug1472426.html b/dom/html/test/test_bug1472426.html
new file mode 100644
index 0000000000..6f891184b8
--- /dev/null
+++ b/dom/html/test/test_bug1472426.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1472426
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1472426</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1472426 **/
+
+ var shadowIframe;
+ var targetIframe;
+ var form;
+ var sr;
+
+ function checkMPSubmission(sub, expected, test) {
+ function getPropCount(o) {
+ var x, l = 0;
+ for (x in o) ++l;
+ return l;
+ }
+ function mpquote(s) {
+ return s.replace(/\r\n/g, " ")
+ .replace(/\r/g, " ")
+ .replace(/\n/g, " ")
+ .replace(/\"/g, "\\\"");
+ }
+
+ is(sub.length, expected.length,
+ "Correct number of multipart items in " + test);
+
+ if (sub.length != expected.length) {
+ alert(JSON.stringify(sub));
+ }
+
+ var i;
+ for (i = 0; i < expected.length; ++i) {
+ if (!("fileName" in expected[i])) {
+ is(sub[i].headers["Content-Disposition"],
+ "form-data; name=\"" + mpquote(expected[i].name) + "\"",
+ "Correct name in " + test);
+ is (getPropCount(sub[i].headers), 1,
+ "Wrong number of headers in " + test);
+ is(sub[i].body,
+ expected[i].value.replace(/\r\n|\r|\n/, "\r\n"),
+ "Correct value in " + test);
+ }
+ else {
+ is(sub[i].headers["Content-Disposition"],
+ "form-data; name=\"" + mpquote(expected[i].name) + "\"; filename=\"" +
+ mpquote(expected[i].fileName) + "\"",
+ "Correct name in " + test);
+ is(sub[i].headers["Content-Type"],
+ expected[i].contentType,
+ "Correct content type in " + test);
+ is (getPropCount(sub[i].headers), 2,
+ "Wrong number of headers in " + test);
+ is(sub[i].body,
+ expected[i].value,
+ "Correct value in " + test);
+ }
+ }
+ }
+
+ function testFormSubmissionInShadowDOM() {
+ targetIframe = document.getElementById("target_iframe");
+ shadowIframe = document.createElement("iframe");
+ shadowIframe.src = "about:blank";
+ shadowIframe.onload = shadowFrameCreated;
+ document.body.appendChild(shadowIframe);
+ }
+
+ function shadowFrameCreated() {
+ var doc = shadowIframe.contentDocument;
+ var body = doc.body;
+ var host = doc.createElement("div");
+ body.appendChild(host);
+ sr = host.attachShadow({ mode: "open" });
+ sr.appendChild(document.getElementById('template').content.cloneNode(true));
+ targetIframe.onload = checkSubmitValues;
+ sr.getElementById("form").submit();
+ }
+
+ function checkSubmitValues() {
+ submission = JSON.parse(targetIframe.contentDocument.documentElement.textContent);
+ var expected = [
+ { name: "text", value: "textvalue" },
+ { name: "hidden", value: "hiddenvalue" },
+ { name: "select", value: "selectvalue" },
+ { name: "textarea", value: "textareavalue" }
+ ];
+ checkMPSubmission(submission, expected, "form submission inside shadow DOM");
+ SimpleTest.finish();
+ }
+
+ window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ testFormSubmissionInShadowDOM();
+ }
+
+ </script>
+ <template id="template">
+ <form action="form_submit_server.sjs" target="target_iframe" id="form"
+ method="POST" enctype="multipart/form-data">
+ <input name="text" value="textvalue">
+ <input name="hidden" value="hiddenvalue" type="hidden">
+ <select name="select"><option selected>selectvalue</option></select>
+ <textarea name="textarea">textareavalue</textarea>
+ </form>
+ </template>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1472426">Mozilla Bug 1472426</a>
+<iframe name="target_iframe" id="target_iframe"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1682.html b/dom/html/test/test_bug1682.html
new file mode 100644
index 0000000000..8a0b7abf19
--- /dev/null
+++ b/dom/html/test/test_bug1682.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1682
+-->
+<head>
+ <title>Test for Bug 1682</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1682">Mozilla Bug 1682</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1682 **/
+var count = 1;
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function () {
+ is(count, 1, "onload executes once");
+ ++count;
+});
+addLoadEvent(SimpleTest.finish);
+
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug1785739.html b/dom/html/test/test_bug1785739.html
new file mode 100644
index 0000000000..2c87c57bd0
--- /dev/null
+++ b/dom/html/test/test_bug1785739.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>nsFind::Find() should initialize the editor</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<input value="1abc1
+ 2abc2
+ 3abc3
+ 4abc4
+ 5abc5
+ 6abc6
+ 7abc7
+ 8abc8
+ 9abc9" id="input">
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ // The current window.find() impl does not support text controls, so import the internal component
+ const finder =
+ SpecialPowers
+ .Cc["@mozilla.org/typeaheadfind;1"]
+ .getService(SpecialPowers.Ci.nsITypeAheadFind);
+
+ finder.init(SpecialPowers.wrap(window).docShell);
+
+ function find() {
+ return finder.find(
+ "abc",
+ false,
+ SpecialPowers.Ci.nsITypeAheadFind.FIND_NEXT,
+ true);
+ }
+
+ async function runTests() {
+ finder.find("abc", false, SpecialPowers.Ci.nsITypeAheadFind.FIND_FIRST, true);
+ // Wait until layout flush as the bug repro needs it
+ await new Promise(requestAnimationFrame);
+
+ for (let i = 0; i < 9; i++) {
+ find();
+ await new Promise(requestAnimationFrame);
+ is(input.selectionStart, (i * 19) + 1);
+ }
+
+ SimpleTest.finish();
+ }
+ window.addEventListener("load", runTests);
+</script>
diff --git a/dom/html/test/test_bug182279.html b/dom/html/test/test_bug182279.html
new file mode 100644
index 0000000000..1421c86ee0
--- /dev/null
+++ b/dom/html/test/test_bug182279.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=182279
+-->
+<head>
+ <title>Test for Bug 182279</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=182279">Moozilla Bug 182279</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Bug 182279 **/
+var sel = document.createElement("select");
+var opt1 = new Option();
+var opt2 = new Option();
+var opt3 = new Option();
+opt1.value = 1;
+opt2.value = 2;
+opt3.value = 3;
+sel.add(opt1, null);
+sel.add(opt2, opt1);
+sel.add(opt3);
+is(sel[0], opt2, "1st item should be 2");
+is(sel[1], opt1, "2nd item should be 1");
+is(sel[2], opt3, "3rd item should be 3");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug1823.html b/dom/html/test/test_bug1823.html
new file mode 100644
index 0000000000..0f42b49980
--- /dev/null
+++ b/dom/html/test/test_bug1823.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1823
+-->
+<head>
+ <title>Test for Bug 1823</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1823">Mozilla Bug 1823</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1823 **/
+ok(!(document.location + "").includes("["), "location object has a toString()");
+
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug196523.html b/dom/html/test/test_bug196523.html
new file mode 100644
index 0000000000..edd71247a7
--- /dev/null
+++ b/dom/html/test/test_bug196523.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=196523
+-->
+<head>
+ <title>Test for Bug 196523</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=196523">Mozilla Bug 196523</a>
+<script>
+ var expectedMessages = 2;
+ SimpleTest.waitForExplicitFinish();
+ window.addEventListener("message", function(e) {
+ --expectedMessages;
+ var str = e.data;
+ var idx = str.indexOf(';');
+ var val = str.substring(0, idx);
+ var msg = str.substring(idx+1);
+ ok(val == "true", msg);
+ if (!expectedMessages) { SimpleTest.finish(); }
+ });
+</script>
+<p id="display">
+ <iframe src="http://test1.example.org/tests/dom/html/test/bug196523-subframe.html"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 196523 **/
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug199692.html b/dom/html/test/test_bug199692.html
new file mode 100644
index 0000000000..0be6d7ed47
--- /dev/null
+++ b/dom/html/test/test_bug199692.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=199692
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Test for Bug 199692</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ // The popup calls MochiTest methods in this window through window.opener
+ window.open("bug199692-popup.html", "bug199692", "width=600,height=600");
+ </script>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug2082.html b/dom/html/test/test_bug2082.html
new file mode 100644
index 0000000000..5c1ec8f8ec
--- /dev/null
+++ b/dom/html/test/test_bug2082.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=2082
+-->
+<head>
+ <title>Test for Bug 2082</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=2082">Mozilla Bug 2082</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<FORM name="gui" id="gui">
+<INPUT TYPE="text" NAME="field" VALUE="some value">
+</FORM>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 2082 **/
+var guiform = document.getElementById("gui");
+ok(document.getElementById("gui").hasChildNodes(), "form elements should be treated as form's children");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug209275.xhtml b/dom/html/test/test_bug209275.xhtml
new file mode 100644
index 0000000000..0cdb64ea00
--- /dev/null
+++ b/dom/html/test/test_bug209275.xhtml
@@ -0,0 +1,258 @@
+<!DOCTYPE html [
+<!ATTLIST foo:base
+ id ID #IMPLIED
+>
+]>
+<html xmlns:foo="http://foo.com" xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=209275
+-->
+<head>
+ <title>Test for Bug 209275</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <style>
+ @namespace svg url("http://www.w3.org/2000/svg");
+ svg|a { fill:blue; }
+ svg|a:visited { fill:purple; }
+ </style>
+
+ <!--
+ base0 should be ignored because it's not in the XHTML namespace
+ -->
+ <foo:base id="base0" href="http://www.foo.com" />
+
+ <!--
+ baseEmpty should be ignored because it has no href and never gets one.
+ -->
+ <base id="baseEmpty" />
+
+ <!--
+ baseWrongAttrNS should be ignored because its href attribute isn't in the empty
+ namespace.
+ -->
+ <base id="baseWrongAttrNS" foo:href="http://foo.com" />
+
+ <base id="base1" />
+ <base id="base2" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=209275">Mozilla Bug 209275</a>
+<p id="display">
+</p>
+<div id="content">
+ <a href="/" id="link1">link1</a>
+ <div style="display:none">
+ <a href="/" id="link2">link2</a>
+ </div>
+ <a href="/" id="link3" style="display:none">link3</a>
+ <a href="#" id="link4">link4</a>
+ <a href="" id="colorlink">colorlink</a>
+ <a href="#" id="link5">link5</a>
+ <iframe id="iframe"></iframe>
+
+ <svg width="5cm" height="3cm" viewBox="0 0 5 3" version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <a xlink:href="" id="ellipselink">
+ <ellipse cx="2.5" cy="1.5" rx="2" ry="1" id="ellipse" />
+ </a>
+ </svg>
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+<![CDATA[
+
+/** Test for Bug 209275 **/
+SimpleTest.waitForExplicitFinish();
+
+function link123HrefIs(href, testNum) {
+ is($('link1').href, href, "link1 test " + testNum);
+ is($('link2').href, href, "link2 test " + testNum);
+ is($('link3').href, href, "link3 test " + testNum);
+}
+
+var gGen;
+
+function visitedDependentComputedStyle(win, elem, property) {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ return utils.getVisitedDependentComputedStyle(elem, "", property);
+}
+
+function getColor(elem) {
+ return visitedDependentComputedStyle(document.defaultView, elem, "color");
+}
+
+function getFill(elem) {
+ return visitedDependentComputedStyle(document.defaultView, elem, "fill");
+}
+
+function setXlinkHref(elem, href) {
+ elem.setAttributeNS("http://www.w3.org/1999/xlink", "href", href);
+}
+
+function continueTest() {
+ gGen.next();
+}
+
+function* run() {
+ var iframe = document.getElementById("iframe");
+ var iframeCw = iframe.contentWindow;
+
+ // First, set the visited/unvisited link/ellipse colors.
+ const unvisitedColor = "rgb(0, 0, 238)";
+ const visitedColor = "rgb(85, 26, 139)";
+ const unvisitedFill = "rgb(0, 0, 255)";
+ const visitedFill = "rgb(128, 0, 128)";
+
+ const rand = Date.now() + "-" + Math.random();
+
+ // Now we can start the tests in earnest.
+
+ var loc = location;
+ // everything from the location up to and including the final forward slash
+ var path = /(.*\/)[^\/]*/.exec(location)[1];
+
+ // Set colorlink's href so we can check that it changes colors after we
+ // change the base href.
+ $('colorlink').href = "http://example.com/" + rand;
+ setXlinkHref($("ellipselink"), "http://example.com/" + rand);
+
+ // Load http://example.com/${rand} into a new window so we can test that
+ // changing the document's base changes the visitedness of our links.
+ //
+ // cross-origin window.open'd windows don't fire load / error events, so we
+ // wait to close it until we observed the visited color.
+ let win = window.open("http://example.com/" + rand, "_blank");
+
+ // Make sure things are what as we expect them at the beginning.
+ link123HrefIs(`${location.origin}/`, 1);
+ is($('link4').href, loc + "#", "link 4 test 1");
+ is($('link5').href, loc + "#", "link 5 test 1");
+
+ // Remove link5 from the document. We're going to test that its href changes
+ // properly when we change our base.
+ var link5 = $('link5');
+ link5.remove();
+
+ $('base1').href = "http://example.com";
+
+ // Were the links' hrefs updated after the base change?
+ link123HrefIs("http://example.com/", 2);
+ is($('link4').href, "http://example.com/#", "link 4 test 2");
+ is(link5.href, "http://example.com/#", "link 5 test 2");
+
+ // Were colorlink's color and ellipse's fill updated appropriately?
+ // Because link coloring is asynchronous, we wait until it is updated (or we
+ // timeout and fail anyway).
+ while (getColor($('colorlink')) != visitedColor) {
+ requestIdleCallback(continueTest);
+ yield undefined;
+ }
+ is(getColor($('colorlink')), visitedColor,
+ "Wrong link color after base change.");
+ while (getFill($('ellipselink')) != visitedFill) {
+ requestIdleCallback(continueTest);
+ yield undefined;
+ }
+ is(getFill($('ellipselink')), visitedFill,
+ "Wrong ellipse fill after base change.");
+
+ win.close();
+
+ $('base1').href = "foo/";
+ // Should be interpreted relative to current URI (not the current base), so
+ // base should now be http://mochi.test:8888/foo/
+
+ link123HrefIs(`${location.origin}/`, 3);
+ is($('link4').href, path + "foo/#", "link 4 test 3");
+
+ // Changing base2 shouldn't affect anything, because it's not the first base
+ // tag.
+ $('base2').href = "http://example.org/bar/";
+ link123HrefIs(`${location.origin}/`, 4);
+ is($('link4').href, path + "foo/#", "link 4 test 4");
+
+ // If we unset base1's href attribute, the document's base should come from
+ // base2, whose href is http://example.org/bar/.
+ $('base1').removeAttribute("href");
+ link123HrefIs("http://example.org/", 5);
+ is($('link4').href, "http://example.org/bar/#", "link 4 test 5");
+
+ // If we remove base1, base2 should become the first base tag, and the hrefs
+ // of all the links should change accordingly.
+ $('base1').remove();
+ link123HrefIs("http://example.org/", 6);
+ is($('link4').href, "http://example.org/bar/#", "link 4 test 6");
+
+ // If we add a new base after base2, nothing should change.
+ var base3 = document.createElement("base");
+ base3.href = "http://base3.example.org/";
+ $('base2').parentNode.insertBefore(base3, $('base2').nextSibling);
+ link123HrefIs("http://example.org/", 7);
+ is($('link4').href, "http://example.org/bar/#", "link 4 test 7");
+
+ // But now if we add a new base before base 2, it should become the primary
+ // base.
+ var base4 = document.createElement("base");
+ base4.href = "http://base4.example.org/";
+ $('base2').parentNode.insertBefore(base4, $('base2'));
+ link123HrefIs("http://base4.example.org/", 8);
+ is($('link4').href, "http://base4.example.org/#", "link 4 test 8");
+
+ // Now if we remove all the base tags, the base should become the page's URI
+ // again.
+ $('base2').remove();
+ base3.remove();
+ base4.remove();
+
+ link123HrefIs(`${location.origin}/`, 9);
+ is($('link4').href, loc + "#", "link 4 test 9");
+
+ // Setting the href of base0 shouldn't do anything because it's not in the
+ // XHTML namespace.
+ $('base0').href = "http://bar.com";
+ link123HrefIs(`${location.origin}/`, 10);
+ is($('link4').href, loc + "#", "link 4 test 10");
+
+ // We load into an iframe a document with a <base href="...">, then remove
+ // the document element. Then we add an <html>, <body>, and <a>, and make
+ // sure that the <a> is resolved relative to the page's location, not its
+ // original base. We do this twice, rebuilding the document in a different
+ // way each time.
+
+ iframeCw.location = "file_bug209275_1.html";
+ yield undefined; // wait for our child to call us back.
+ is(iframeCw.document.getElementById("link").href,
+ path + "file_bug209275_1.html#",
+ "Wrong href after nuking document.");
+
+ iframeCw.location = "file_bug209275_2.html";
+ yield undefined; // wait for callback from child
+ is(iframeCw.document.getElementById("link").href,
+ `${location.origin}/`,
+ "Wrong href after nuking document second time.");
+
+ // Make sure that document.open() makes the document forget about any <base>
+ // tags it has.
+ iframeCw.location = "file_bug209275_3.html";
+ yield undefined; // wait for callback from child
+ is(iframeCw.document.getElementById("link").href,
+ "http://mochi.test:8888/",
+ "Wrong href after document.open().");
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", function() {
+ gGen = run();
+ gGen.next();
+});
+
+]]>
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug237071.html b/dom/html/test/test_bug237071.html
new file mode 100644
index 0000000000..8360c1eb86
--- /dev/null
+++ b/dom/html/test/test_bug237071.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=237071
+-->
+<head>
+ <title>Test for Bug 237071</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=237071">Mozilla Bug 237071</a>
+<p id="display"></p>
+<div id="content" >
+ <ol id="theOL" start="22">
+ <li id="foo" >should be 22</li>
+ <li id="foo23">should be 23</li>
+ </ol>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Bug 237071 **/
+is($('theOL').start, 22, "OL start attribute mapped to .start, not just text attribute");
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug242709.html b/dom/html/test/test_bug242709.html
new file mode 100644
index 0000000000..7dde04713d
--- /dev/null
+++ b/dom/html/test/test_bug242709.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=242709
+-->
+<head>
+ <title>Test for Bug 242709</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=242709">Mozilla Bug 242709</a>
+<p id="display"></p>
+<div id="content">
+<iframe src="bug242709_iframe.html" id="a"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 242709 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var submitted = function() {
+ ok(true, "Disabling button after form submission doesn't prevent submitting");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug24958.html b/dom/html/test/test_bug24958.html
new file mode 100644
index 0000000000..a6a077aefe
--- /dev/null
+++ b/dom/html/test/test_bug24958.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=24958
+-->
+<head>
+ <title>Test for Bug 24958</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <SCRIPT id="foo" TYPE="text/javascript">/*This space intentionally left blank*/</SCRIPT>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=24958">Mozilla Bug 24958</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 24958 **/
+is($("foo").text, "\/*This space intentionally left blank*\/", "HTMLScriptElement.text should return text")
+
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug255820.html b/dom/html/test/test_bug255820.html
new file mode 100644
index 0000000000..5de2fca7c0
--- /dev/null
+++ b/dom/html/test/test_bug255820.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=255820
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Test for Bug 255820</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=255820">Mozilla Bug 255820</a>
+<p id="display">
+ <iframe id="f1"></iframe>
+ <iframe id="f2"></iframe>
+ <iframe id="f3"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 255820 **/
+SimpleTest.waitForExplicitFinish();
+
+is(document.characterSet, "UTF-8",
+ "Unexpected character set for our document");
+
+var testsLeft = 3;
+
+function testFinished() {
+ --testsLeft;
+ if (testsLeft == 0) {
+ SimpleTest.finish();
+ }
+}
+
+function charsetTestFinished(id, doc, charsetTarget) {
+ is(doc.characterSet, charsetTarget, "Unexpected charset for subframe " + id);
+ testFinished();
+}
+
+function f3Continue() {
+ var doc = $("f3").contentDocument;
+ is(doc.defaultView.getComputedStyle(doc.body).color, "rgb(0, 180, 0)",
+ "Wrong color");
+ charsetTestFinished('f3', doc, "UTF-8");
+}
+
+function runTest() {
+ var doc = $("f1").contentDocument;
+ is(doc.characterSet, "UTF-8",
+ "Unexpected initial character set for first frame");
+ doc.open();
+ doc.write('<html></html>');
+ doc.close();
+ charsetTestFinished("f1", doc, "UTF-8");
+
+ doc = $("f2").contentDocument;
+ is(doc.characterSet, "UTF-8",
+ "Unexpected initial character set for second frame");
+ doc.open();
+ var str = '<html><head>';
+ str += '<script src="data:application/javascript,"><'+'/script>';
+ str += '<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">';
+ str += '</head><body>';
+ str += '</body></html>';
+ doc.write(str);
+ doc.close();
+ is(doc.characterSet, "UTF-8",
+ "Unexpected character set for second frame after write");
+ $("f2").
+ setAttribute("onload",
+ "charsetTestFinished('f2', this.contentDocument, 'UTF-8');");
+
+ doc = $("f3").contentDocument;
+ is(doc.characterSet, "UTF-8",
+ "Unexpected initial character set for third frame");
+ doc.open();
+ var str = '<html><head>';
+ str += '<style>body { color: rgb(255, 0, 0) }</style>';
+ str += '<link type="text/css" rel="stylesheet" href="data:text/css, body { color: rgb(0, 180, 0) }">';
+ str += '</head><body>';
+ str += '</body></html>';
+ doc.write(str);
+ doc.close();
+ is(doc.characterSet, "UTF-8",
+ "Unexpected character set for third frame after write");
+ $("f3").setAttribute("onload", "f3Continue()");
+}
+
+addLoadEvent(runTest);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug259332.html b/dom/html/test/test_bug259332.html
new file mode 100644
index 0000000000..f41f88930c
--- /dev/null
+++ b/dom/html/test/test_bug259332.html
@@ -0,0 +1,64 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=259332
+-->
+<head>
+ <title>Test for Bug 259332</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=259332">Mozilla Bug 259332</a>
+<p id="display"></p>
+<div id="content">
+ <div id="a">a
+ <div id="a">a</div>
+ <input name="a" value="a">
+ <div id="b">b</div>
+ <input name="b" value="b">
+ <div id="c">c</div>
+ </div>
+ <input name="write">
+ <input name="write">
+ <input id="write">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 259332 **/
+
+list = document.all.a;
+ok(list.length == 3, "initial a length");
+
+blist = document.all.b;
+ok(document.all.b.length == 2, "initial b length");
+document.getElementById('b').id = 'a';
+ok(document.all.b.nodeName == "INPUT", "just one b");
+
+ok(blist.length == 1, "just one b");
+ok(list.length == 4, "one more a");
+
+newDiv = document.createElement('div');
+newDiv.id = 'a';
+newDiv.innerHTML = 'a';
+list[0].appendChild(newDiv);
+ok(list.length == 5, "two more a");
+
+ok(document.all.c.textContent == 'c', "one c");
+document.all.c.id = 'a';
+ok(!document.all.c, "no c");
+ok(list.length == 6, "three more a");
+
+ok(document.all.write.length == 3, "name is write");
+
+newDiv = document.createElement('div');
+newDiv.id = 'd';
+newDiv.innerHTML = 'd';
+list[0].appendChild(newDiv);
+ok(document.all.d.textContent == 'd', "new d");
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug274626.html b/dom/html/test/test_bug274626.html
new file mode 100644
index 0000000000..6003722bef
--- /dev/null
+++ b/dom/html/test/test_bug274626.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=274626
+-->
+<head>
+ <title>Test for Bug 274626</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=274626">Mozilla Bug 274626</a>
+<br>
+
+<input id='textbox_enabled' title='hello' value='hello' />
+<input id='textbox_disabled' title='hello' value='hello' disabled/>
+
+<br>
+<input id='input_button_enabled' title='hello' value='hello' type='button' />
+<input id='input_button_disabled' title='hello' value='hello' type='button' disabled />
+
+<br>
+<input id='checkbox_enabled' title='hello' type='checkbox'>hello</input>
+<input id='checkbox_disabled' title='hello' type='checkbox' disabled >hello</input>
+
+<br>
+<button id='button_enabled' title='hello' value='hello' type='button'>test</button>
+<button id='button_disabled' title='hello' value='hello' type='button' disabled>test</button>
+
+<br>
+<textarea id='textarea_enabled' title='hello' value='hello' onclick="alert('click event');"> </textarea>
+<textarea id='textarea_disabled' title='hello' value='hello' onclick="alert('click event');" disabled></textarea>
+
+
+<br>
+<select id='select_enabled' title='hello' onclick="alert('click event');">
+ <option value='item1'>item1</option>
+ <option value='item2'>item2</option>
+</select>
+<select id='select_disabled' title='hello' onclick="alert('click event');" disabled>
+ <option value='item1'>item1</option>
+ <option value='item2'>item2</option>
+</select>
+
+<br>
+<form>
+ <fieldset id='fieldset_enabled' title='hello' onclick="alert('click event');">
+ <legend>Enabled fieldset:</legend>
+ Name: <input type='text' size='30' /><br />
+ Email: <input type='text' size='30' /><br />
+ Date of birth: <input type='text' size='10' />
+ </fieldset>
+</form>
+<form>
+ <fieldset id='fieldset_disabled' title='hello' onclick="alert('click event');" disabled>
+ <legend>Disabled fieldset:</legend>
+ Name: <input type='text' size='30' /><br />
+ Email: <input type='text' size='30' /><br />
+ Date of birth: <input type='text' size='10' />
+ </fieldset>
+</form>
+
+<script class="testbody" type="application/javascript">
+
+/** Test for Bug 274626 **/
+
+ function HandlesMouseMove(evt) {
+ evt.target.handlesMouseMove = true;
+ }
+
+ var controls=["textbox_enabled","textbox_disabled",
+ "input_button_enabled", "input_button_disabled", "checkbox_enabled",
+ "checkbox_disabled", "button_enabled", "button_disabled",
+ "textarea_enabled", "textarea_disabled", "select_enabled",
+ "select_disabled", "fieldset_enabled", "fieldset_disabled"];
+
+ for (id of controls) {
+ var ctrl = document.getElementById(id);
+ ctrl.addEventListener('mousemove', HandlesMouseMove);
+ ctrl.handlesMouseMove = false;
+ var evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent("mousemove", true, true, window,
+ 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ ctrl.dispatchEvent(evt);
+
+ // Mouse move events are what causes tooltips to show up.
+ // Before this fix we would not allow mouse move events to go through
+ // which in turn did not allow tooltips to be displayed.
+ // This test will ensure that all HTML elements handle mouse move events
+ // so that tooltips can be displayed
+ ok(ctrl.handlesMouseMove, "Disabled element need mouse move for tooltips");
+ }
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug277724.html b/dom/html/test/test_bug277724.html
new file mode 100644
index 0000000000..0732a4cf9a
--- /dev/null
+++ b/dom/html/test/test_bug277724.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=277724
+-->
+<head>
+ <title>Test for Bug 277724</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=277724">Mozilla Bug 277724</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 277724 **/
+
+var childUnloaded = false;
+
+var nodes = [
+ [ "select", HTMLSelectElement ],
+ [ "textarea", HTMLTextAreaElement ],
+ [ "text", HTMLInputElement ],
+ [ "password", HTMLInputElement ],
+ [ "checkbox", HTMLInputElement ],
+ [ "radio", HTMLInputElement ],
+ [ "image", HTMLInputElement ],
+ [ "submit", HTMLInputElement ],
+ [ "reset", HTMLInputElement ],
+ [ "button input", HTMLInputElement ],
+ [ "hidden", HTMLInputElement ],
+ [ "file", HTMLInputElement ],
+ [ "submit button", HTMLButtonElement ],
+ [ "reset button", HTMLButtonElement ],
+ [ "button", HTMLButtonElement ]
+];
+
+function soon(f) {
+ return function() { setTimeout(f, 0); }
+}
+
+function startTest(frameid) {
+ is(childUnloaded, false, "Child not unloaded yet");
+
+ var doc = $(frameid).contentDocument;
+ var win = $(frameid).contentWindow;
+ ok(doc instanceof win.Document, "doc should be a document");
+
+ for (var i = 0; i < nodes.length; ++i) {
+ var id = nodes[i][0];
+ var node = doc.getElementById(id);
+ ok(node instanceof win[nodes[i][1].name], id + " should be a " + nodes[i][1]);
+ is(node.disabled, false, "check for " + id + " state");
+ node.disabled = true;
+ is(node.disabled, true, "check for " + id + " state change");
+ }
+
+ $(frameid).onload = soon(function() { continueTest(frameid) });
+
+ // Do this off a timeout so it's not treated like a replace load.
+ function loadBlank() {
+ $(frameid).contentWindow.location = "about:blank";
+ }
+ setTimeout(loadBlank, 0);
+}
+
+function continueTest(frameid) {
+ is(childUnloaded, true, "Unload handler should have fired");
+ var doc = $(frameid).contentDocument;
+ var win = $(frameid).contentWindow;
+ ok(doc instanceof win.Document, "doc should be a document");
+
+ for (var i = 0; i < nodes.length; ++i) {
+ var id = nodes[i][0];
+ var node = doc.getElementById(id);
+ ok(node === null, id + " should be null");
+ }
+
+ $(frameid).onload = soon(function() { finishTest(frameid); });
+
+ // Do this off a timeout too. Why, I'm not sure. Something in session
+ // history creates another history state if we don't. :(
+ function goBack() {
+ $(frameid).contentWindow.history.back();
+ }
+ setTimeout(goBack, 0);
+}
+
+// XXXbz this is a nasty hack to work around the XML content sink not being
+// incremental, so that the _first_ control we test is ok but others are not.
+var testIs = is;
+var once = false;
+function flipper(a, b, c) {
+ if (once) {
+ todo(a == b, c);
+ } else {
+ once = true;
+ is(a, b, c);
+ }
+}
+
+function finishTest(frameid) {
+ var doc = $(frameid).contentDocument;
+ var win = $(frameid).contentWindow;
+ ok(doc instanceof win.Document, "doc should be a document");
+
+ for (var i = 0; i < nodes.length; ++i) {
+ var id = nodes[i][0];
+ var node = doc.getElementById(id);
+ ok(node instanceof win[nodes[i][1].name], id + " should be a " + nodes[i][1]);
+ //testIs(node.disabled, true, "check for " + id + " state restore");
+ }
+
+ if (frameid == "frame2") {
+ SimpleTest.finish();
+ } else {
+ childUnloaded = false;
+
+ // XXXbz this is a nasty hack to deal with the content sink. See above.
+ testIs = flipper;
+
+ $("frame2").onload = soon(function() { startTest("frame2"); });
+ $("frame2").src = "bug277724_iframe2.xhtml";
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+
+<!-- Don't use display:none, since we don't support framestate restoration
+ without a frame tree -->
+<div id="content" style="visibility: hidden">
+ <iframe src="bug277724_iframe1.html" id="frame1"
+ onload="setTimeout(function() { startTest('frame1') }, 0)"></iframe>
+ <iframe src="" id="frame2"></iframe>
+</div>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug277890.html b/dom/html/test/test_bug277890.html
new file mode 100644
index 0000000000..69bc820880
--- /dev/null
+++ b/dom/html/test/test_bug277890.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=277890
+-->
+<head>
+ <title>Test for Bug 277890</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=277890">Mozilla Bug 277890</a>
+<p id="display"></p>
+<div id="content">
+<iframe src="bug277890_iframe.html" id="a"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 277890 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var submitted = function() {
+ ok(true, "Disabling button after form submission doesn't prevent submitting");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug287465.html b/dom/html/test/test_bug287465.html
new file mode 100644
index 0000000000..bc6307423e
--- /dev/null
+++ b/dom/html/test/test_bug287465.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=287465
+-->
+<head>
+ <title>Test for Bug 287465</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=287465">Mozilla Bug 287465</a>
+<p id="display"></p>
+<div id="content" style="display:none">
+
+<iframe id="i1" srcdoc="<svg xmlns='http://www.w3.org/2000/svg'></svg>"></iframe>
+<object id="o1" data="object_bug287465_o1.html"></object>
+<iframe id="i2" srcdoc="<html></html>"></iframe>
+<object id="o2" data="object_bug287465_o2.html"></object>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(doTest);
+
+function doTest() {
+ function checkSVGDocument(id) {
+ var e = document.getElementById(id);
+ ok(e.contentDocument != null, "check nonnull contentDocument '" + id + "'");
+ is(e.contentDocument, e.getSVGDocument(), "check documents match '" + id + "'");
+ }
+
+ checkSVGDocument("o1");
+ checkSVGDocument("i1");
+ checkSVGDocument("o2");
+ checkSVGDocument("i2");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug295561.html b/dom/html/test/test_bug295561.html
new file mode 100644
index 0000000000..456985f731
--- /dev/null
+++ b/dom/html/test/test_bug295561.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=295561
+-->
+<head>
+ <title>Test for Bug 295561</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=295561">Mozilla Bug 295561</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+<table id="testTable">
+<thead>
+<tr id="headRow"><td></td></tr>
+</thead>
+<tfoot>
+<tr id="footRow"><td></td></tr>
+</tfoot>
+<tbody id="tBody" name="namedTBody">
+<tr id="trow" name="namedTRow">
+<td id="tcell" name="namedTCell"></td>
+<th id="tcellh" name="namedTH"></th>
+</tr>
+<tr><td></td></tr>
+</tbody>
+<tbody id="tBody2" name="namedTBody2">
+<tr id="trow2" name="namedTRow2">
+<td id="tcell2" name="namedTCell2"></td>
+<th id="tcellh2" name="namedTH2"></th>
+</tr>
+</table>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function testItById(id, collection, collectionName) {
+ is(collection[id], $(id),
+ "Should be able to get by id '" + id + "' from " + collectionName +
+ " collection using square brackets.")
+ is(collection.namedItem(id), $(id),
+ "Should be able to get by id '" + id + "' from " + collectionName +
+ " collection using namedItem.")
+}
+
+function testItByName(name, id, collection, collectionName) {
+ is(collection[name], $(id),
+ "Should be able to get by name '" + name + "' from " + collectionName +
+ " collection using square brackets.")
+ is(collection.namedItem(name), $(id),
+ "Should be able to get by name '" + name + "' from " + collectionName +
+ " collection using namedItem.")
+}
+
+function testIt(name, id, collection, collectionName) {
+ testItByName(name, id, collection, collectionName);
+ testItById(id, collection, collectionName);
+}
+
+var table = $("testTable")
+testIt("namedTBody", "tBody", table.tBodies, "tBodies")
+testIt("namedTRow", "trow", table.rows, "table rows")
+testIt("namedTRow", "trow", $("tBody").rows, "tbody rows")
+testIt("namedTCell", "tcell", $("trow").cells, "cells")
+testIt("namedTH", "tcellh", $("trow").cells, "cells")
+testIt("namedTBody2", "tBody2", table.tBodies, "tBodies")
+testIt("namedTRow2", "trow2", table.rows, "table rows")
+testIt("namedTRow2", "trow2", $("tBody2").rows, "tbody rows")
+testIt("namedTCell2", "tcell2", $("trow2").cells, "cells")
+testIt("namedTH2", "tcellh2", $("trow2").cells, "cells")
+is(table.tBodies.length, 2, "Incorrect tBodies length");
+is(table.rows.length, 5, "Incorrect rows length");
+is(table.rows[0], $("headRow"), "THead row in wrong spot");
+is(table.rows[1], $("trow"), "First tbody row in wrong spot");
+is(table.rows[3], $("trow2"), "Second tbody row in wrong spot");
+is(table.rows[4], $("footRow"), "TFoot row in wrong spot");
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug297761.html b/dom/html/test/test_bug297761.html
new file mode 100644
index 0000000000..0d4532827d
--- /dev/null
+++ b/dom/html/test/test_bug297761.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=297761
+-->
+<head>
+ <title>Test for Bug 297761</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=297761">Mozilla Bug 297761</a>
+<p id="display"></p>
+<div id="content">
+ <iframe src="file_bug297761.html"></iframe>
+ <iframe src="file_bug297761.html"></iframe>
+ <iframe src="file_bug297761.html"></iframe>
+ <iframe src="file_bug297761.html"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 297761 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var nbTests = 4;
+var curTest = 0;
+
+function nextTest()
+{
+ if (curTest == 3) {
+ frames[curTest].document.forms[0].submit();
+ } else {
+ var el = null;
+ if (curTest == 2) {
+ el = frames[curTest].document.getElementById('i');
+ } else {
+ el = frames[curTest].document.forms[0].elements[curTest];
+ }
+
+ el.focus();
+ el.click();
+ }
+}
+
+function frameLoaded(aFrame)
+{
+ var documentLocation = location.href.replace(/\.html.*/, "\.html");
+ is(aFrame.contentWindow.location.href.replace(/\?x=0&y=0/, "?"),
+ documentLocation.replace(/test_bug/, "file_bug") + "?",
+ "form should have been submitted to the document location");
+
+ if (++curTest == nbTests) {
+ SimpleTest.finish();
+ } else {
+ nextTest();
+ }
+}
+
+function runTest()
+{
+ // Initialize event handlers.
+ var frames = document.getElementsByTagName('iframe');
+ for (var i=0; i<nbTests; ++i) {
+ frames[i].setAttribute('onload', "frameLoaded(this);");
+ }
+
+ nextTest();
+}
+
+addLoadEvent(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug300691-1.html b/dom/html/test/test_bug300691-1.html
new file mode 100644
index 0000000000..44418e8f3a
--- /dev/null
+++ b/dom/html/test/test_bug300691-1.html
@@ -0,0 +1,126 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=300691
+-->
+<head>
+ <title>Test for Bug 300691</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=300691">Mozilla Bug 300691</a>
+<p id="display">
+ <textarea id="target"></textarea>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var t = $("target");
+
+// FIXME(bug 1838346): This shouldn't need to be async probably?
+SimpleTest.waitForExplicitFinish();
+onload = function() {
+
+/** Test for Bug 300691 **/
+function valueIs(arg, reason) {
+ is(t.value, arg, reason);
+}
+
+function defValueIs(arg, reason) {
+ is(t.defaultValue, arg, reason);
+}
+
+valueIs("", "Nothing in the textarea");
+defValueIs("", "Nothing in the textarea 2");
+
+t.appendChild(document.createTextNode("ab"));
+valueIs("ab", "Appended textnode");
+defValueIs("ab", "Appended textnode 2");
+
+t.firstChild.data = "abcd";
+valueIs("abcd", "Modified textnode text");
+defValueIs("abcd", "Modified textnode text 2");
+
+t.appendChild(document.createTextNode("efgh"));
+valueIs("abcdefgh", "Appended another textnode");
+defValueIs("abcdefgh", "Appended another textnode 2");
+
+t.removeChild(t.lastChild);
+valueIs("abcd", "Removed textnode");
+defValueIs("abcd", "Removed textnode 2");
+
+t.appendChild(document.createTextNode("efgh"));
+valueIs("abcdefgh", "Appended yet another textnode");
+defValueIs("abcdefgh", "Appended yet another textnode 2");
+
+t.normalize();
+valueIs("abcdefgh", "Normalization changes nothing for the value");
+defValueIs("abcdefgh", "Normalization changes nothing for the value 2");
+
+t.defaultValue = "abc";
+valueIs("abc", "Just set the default value on non-edited textarea");
+defValueIs("abc", "Just set the default value on non-edited textarea 2");
+
+t.appendChild(document.createTextNode("defgh"));
+valueIs("abcdefgh", "Appended another textnode again");
+defValueIs("abcdefgh", "Appended another textnode again 2");
+
+t.focus(); // This puts the caret at the end of the textarea, and doing
+ // something like "home" in a cross-platform way is kinda hard.
+sendKey("left");
+sendKey("left");
+sendKey("left");
+sendString("Test");
+
+valueIs("abcdeTestfgh", "Typed 'Test' after three left-arrows starting from end");
+defValueIs("abcdefgh", "Typing 'Test' shouldn't affect default value");
+
+sendKey("right");
+sendKey("right");
+sendKey("back_space");
+sendKey("back_space");
+
+valueIs("abcdeTesth",
+ "Backspaced twice after two right-arrows starting from end of typing");
+defValueIs("abcdefgh", "Deleting shouldn't affect default value");
+
+t.appendChild(document.createTextNode("ijk"));
+valueIs("abcdeTesth",
+ "Appending textnode shouldn't affect value in edited textarea");
+defValueIs("abcdefghijk", "Appended textnode 3");
+
+t.lastChild.data = "lmno";
+valueIs("abcdeTesth",
+ "Modifying textnode text shouldn't affect value in edited textarea");
+defValueIs("abcdefghlmno", "Modified textnode text 3");
+
+t.firstChild.remove();
+valueIs("abcdeTesth",
+ "Removing child textnode shouldn't affect value in edited textarea");
+defValueIs("defghlmno", "Removed textnode 3");
+
+t.insertBefore(document.createTextNode("abc"), t.firstChild);
+valueIs("abcdeTesth",
+ "Inserting child textnode shouldn't affect value in edited textarea");
+defValueIs("abcdefghlmno", "Inserted a text node");
+
+t.normalize();
+valueIs("abcdeTesth", "Normalization changes nothing for the value 3");
+defValueIs("abcdefghlmno", "Normalization changes nothing for the value 4");
+
+t.defaultValue = "abc";
+valueIs("abcdeTesth", "Setting default value shouldn't affect edited textarea");
+defValueIs("abc", "Just set the default value textarea");
+SimpleTest.finish();
+
+};
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug300691-2.html b/dom/html/test/test_bug300691-2.html
new file mode 100644
index 0000000000..0dbc5be79a
--- /dev/null
+++ b/dom/html/test/test_bug300691-2.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=300691
+-->
+<head>
+ <title>Test for Bug 300691</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=300691">Mozilla Bug 300691</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ // First, setup. We'll be toggling these variables as we go.
+ var test1Ran = false;
+ var test2Ran = false;
+ var test3Ran = false;
+ var test4Ran = false;
+ var test5Ran = false;
+ var test6Ran = false;
+ var test7Ran = false;
+ var test8Ran = false;
+ var test9Ran = false;
+ var test10Ran = false;
+ var test11Ran = false;
+ var test12Ran = false;
+ var test13Ran = false;
+ var test14aRan = false;
+ var test14bRan = false;
+ var test15aRan = false;
+ var test15bRan = false;
+</script>
+<script id="test1" type="text/javascript">test1Ran = true;</script>
+<script id="test2" type="text/javascript"></script>
+<script id="test3" type="text/javascript">;</script>
+<script id="test4" type="text/javascript"> </script>
+<script id="test5" type="text/javascript"></script>
+<script id="test6" type="text/javascript"></script>
+<script id="test7" type="text/javascript"></script>
+<script id="test8" type="text/javascript"></script>
+<script id="test9" type="text/javascript"></script>
+<script id="test10" type="text/javascript" src="data:text/javascript,">
+ test10Ran = true;
+</script>
+<script id="test11" type="text/javascript"
+ src="data:text/javascript,test11Ran = true">
+ test11Ran = false;
+</script>
+<script id="test12" type="text/javascript"></script>
+<script id="test13" type="text/javascript"></script>
+<script id="test14" type="text/javascript"></script>
+<script id="test15" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+ /** Test for Bug 300691 **/
+ $("test2").appendChild(document.createTextNode("test2Ran = true"));
+ is(test2Ran, true, "Should be 2!");
+
+ $("test3").appendChild(document.createTextNode("test3Ran = true"));
+ is(test3Ran, false, "Should have run already 3!");
+
+ $("test4").appendChild(document.createTextNode("test4Ran = true"));
+ is(test4Ran, false, "Should have run already 4!");
+
+ $("test5").appendChild(document.createTextNode(" "));
+ $("test5").appendChild(document.createTextNode("test5Ran = true"));
+ is(test5Ran, false, "Should have run already 5!");
+
+ $("test6").appendChild(document.createTextNode(" "));
+
+ $("test7").appendChild(document.createTextNode(""));
+
+ $("test8").appendChild(document.createTextNode(""));
+
+ $("test9").appendChild(document.createTextNode(""));
+
+ $("test12").src = "data:text/javascript,test12Ran = true;";
+ is(test12Ran, false, "Not yet 12!");
+
+ $("test13").setAttribute("src", "data:text/javascript,test13Ran = true;");
+ is(test13Ran, false, "Not yet 13!");
+
+ $("test14").src = "data:text/javascript,test14aRan = true;";
+ $("test14").appendChild(document.createTextNode("test14bRan = true"));
+ is(test14aRan, false, "Not yet 14a!");
+ is(test14bRan, false, "Not yet 14b!");
+
+ $("test15").src = "data:text/javascript,test15aRan = true;";
+ $("test15").appendChild(document.createTextNode("test15bRan = true"));
+ $("test15").removeAttribute("src");
+ is(test15aRan, false, "Not yet 15a!");
+ is(test15bRan, false, "Not yet 15b!");
+</script>
+<script type="text/javascript">
+ // Follow up on some of those
+ $("test6").appendChild(document.createTextNode("test6Ran = true"));
+ is(test6Ran, false, "Should have run already 6!");
+
+ $("test7").appendChild(document.createTextNode("test7Ran = true"));
+ is(test7Ran, true, "Should be 7!");
+
+ $("test8").insertBefore(document.createTextNode("test8Ran = true"),
+ $("test8").firstChild);
+ is(test8Ran, true, "Should be 8!");
+
+ $("test9").firstChild.data = "test9Ran = true";
+ is(test9Ran, true, "Should be 9!");
+</script>
+<script type="text/javascript">
+function done() {
+ is(test1Ran, true, "Should have run!");
+ is(test3Ran, false, "Already executed test3 script once");
+ is(test4Ran, false,
+ "Should have executed whitespace-only script already");
+ is(test5Ran, false,
+ "Should have executed once the whitespace node was added");
+ is(test6Ran, false,
+ "Should have executed once the whitespace node was added 2");
+ is(test10Ran, false, "Has an src; inline part shouldn't run");
+ is(test11Ran, true, "Script with src should have run");
+ is(test12Ran, true, "Setting src should execute script");
+ is(test13Ran, true, "Setting src attribute should execute script");
+ is(test14aRan, true, "src attribute takes precedence over inline content");
+ is(test14bRan, false, "src attribute takes precedence over inline content 2");
+ is(test15aRan, true,
+ "src attribute load should have started before the attribute got removed");
+ is(test15bRan, false,
+ "src attribute still got executed, so this shouldn't have been");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(done);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug300691-3.xhtml b/dom/html/test/test_bug300691-3.xhtml
new file mode 100644
index 0000000000..788ca2160d
--- /dev/null
+++ b/dom/html/test/test_bug300691-3.xhtml
@@ -0,0 +1,48 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=300691
+-->
+<head>
+ <title>Test for Bug 300691</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=300691">Mozilla Bug 300691</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ // First, setup. We'll be toggling these variables as we go.
+ // Note that scripts don't execute immediately when you put text in them --
+ // they wait for the currently-running script to finish.
+ var test1Ran = false;
+ var test2Ran = false;
+ var test3Ran = false;
+</script>
+<script id="test1" type="text/javascript">test1Ran = true;</script>
+<script id="test2" type="text/javascript"></script>
+<script id="test3" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+<![CDATA[
+ /** Test for Bug 300691 **/
+ $("test2").appendChild(document.createCDATASection("test2Ran = true"));
+ is(test2Ran, true, "Should be 2!");
+
+ $("test3").appendChild(document.createCDATASection(""));
+]]>
+</script>
+<script type="text/javascript">
+ // Follow up on some of those
+ $("test3").firstChild.data = "test3Ran = true";
+ is(test3Ran, true, "Should be 3!");
+</script>
+<script type="text/javascript">
+ is(test1Ran, true, "Should have run!");
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug311681.html b/dom/html/test/test_bug311681.html
new file mode 100644
index 0000000000..7c74f7664b
--- /dev/null
+++ b/dom/html/test/test_bug311681.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=311681
+-->
+<head>
+ <title>Test for Bug 311681</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=311681">Mozilla Bug 311681</a>
+<script class="testbody" type="text/javascript">
+ // Setup script
+ SimpleTest.waitForExplicitFinish();
+
+ // Make sure to trigger the hashtable case by asking for enough elements
+ // by ID.
+ for (var i = 0; i < 256; ++i) {
+ var x = document.getElementById(i);
+ }
+
+ // save off the document.getElementById function, since getting it as a
+ // property off the document it causes a content flush.
+ var fun = document.getElementById;
+
+ // Slot for our initial element with id "content"
+ var testNode;
+
+ function getCont() {
+ return fun.call(document, "content");
+ }
+
+ function testClone() {
+ // Test to make sure that if we have multiple nodes with the same ID in
+ // a document we don't forget about one of them when the other is
+ // removed.
+ var newParent = $("display");
+ var node = testNode.cloneNode(true);
+ isnot(node, testNode, "Clone should be a different node");
+
+ newParent.appendChild(node);
+
+ // Check what getElementById returns, no flushing
+ is(getCont(), node, "Should be getting new node pre-flush 1");
+
+ // Trigger a layout flush, just in case.
+ var itemHeight = newParent.offsetHeight/10;
+
+ // Check what getElementById returns now.
+ is(getCont(), node, "Should be getting new node post-flush 1");
+
+ clear(newParent);
+
+ // Check what getElementById returns, no flushing
+ is(getCont(), testNode, "Should be getting orig node pre-flush 2");
+
+ // Trigger a layout flush, just in case.
+ var itemHeight = newParent.offsetHeight/10;
+
+ // Check what getElementById returns now.
+ is(getCont(), testNode, "Should be getting orig node post-flush 2");
+
+ node = testNode.cloneNode(true);
+ newParent.appendChild(node);
+ testNode.remove();
+
+ // Check what getElementById returns, no flushing
+ is(getCont(), node, "Should be getting clone pre-flush");
+
+ // Trigger a layout flush, just in case.
+ var itemHeight = newParent.offsetHeight/10;
+
+ // Check what getElementById returns now.
+ is(getCont(), node, "Should be getting clone post-flush");
+
+ }
+
+ function clear(node) {
+ while (node.hasChildNodes()) {
+ node.firstChild.remove();
+ }
+ }
+
+ addLoadEvent(testClone);
+ addLoadEvent(SimpleTest.finish);
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <script class="testbody" type="text/javascript">
+ testNode = fun.call(document, "content");
+ isnot(testNode, null, "Should have node here");
+ </script>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug311681.xhtml b/dom/html/test/test_bug311681.xhtml
new file mode 100644
index 0000000000..15019fa644
--- /dev/null
+++ b/dom/html/test/test_bug311681.xhtml
@@ -0,0 +1,102 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=311681
+-->
+<head>
+ <title>Test for Bug 311681</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=311681">Mozilla Bug 311681</a>
+<script class="testbody" type="text/javascript">
+<![CDATA[
+ // Setup script
+ SimpleTest.waitForExplicitFinish();
+
+ // Make sure to trigger the hashtable case by asking for enough elements
+ // by ID.
+ for (var i = 0; i < 256; ++i) {
+ var x = document.getElementById(i);
+ }
+
+ // save off the document.getElementById function, since getting it as a
+ // property off the document it causes a content flush.
+ var fun = document.getElementById;
+
+ // Slot for our initial element with id "content"
+ var testNode;
+
+ function getCont() {
+ return fun.call(document, "content");
+ }
+
+ function testClone() {
+ // Test to make sure that if we have multiple nodes with the same ID in
+ // a document we don't forget about one of them when the other is
+ // removed.
+ var newParent = $("display");
+ var node = testNode.cloneNode(true);
+ isnot(node, testNode, "Clone should be a different node");
+
+ newParent.appendChild(node);
+
+ // Check what getElementById returns, no flushing
+ is(getCont(), node, "Should be getting new node pre-flush 1");
+
+ // Trigger a layout flush, just in case.
+ var itemHeight = newParent.offsetHeight/10;
+
+ // Check what getElementById returns now.
+ is(getCont(), node, "Should be getting new node post-flush 1");
+
+ clear(newParent);
+
+ // Check what getElementById returns, no flushing
+ is(getCont(), testNode, "Should be getting orig node pre-flush 2");
+
+ // Trigger a layout flush, just in case.
+ var itemHeight = newParent.offsetHeight/10;
+
+ // Check what getElementById returns now.
+ is(getCont(), testNode, "Should be getting orig node post-flush 2");
+
+ node = testNode.cloneNode(true);
+ newParent.appendChild(node);
+ testNode.remove();
+
+ // Check what getElementById returns, no flushing
+ is(getCont(), node, "Should be getting clone pre-flush");
+
+ // Trigger a layout flush, just in case.
+ var itemHeight = newParent.offsetHeight/10;
+
+ // Check what getElementById returns now.
+ is(getCont(), node, "Should be getting clone post-flush");
+
+ }
+
+ function clear(node) {
+ while (node.hasChildNodes()) {
+ node.firstChild.remove();
+ }
+ }
+
+ addLoadEvent(testClone);
+ addLoadEvent(SimpleTest.finish);
+]]>
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <script class="testbody" type="text/javascript">
+ <![CDATA[
+ testNode = fun.call(document, "content");
+ ok(testNode != null, "Should have node here");
+ ]]>
+ </script>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug324378.html b/dom/html/test/test_bug324378.html
new file mode 100644
index 0000000000..8bab3feaf4
--- /dev/null
+++ b/dom/html/test/test_bug324378.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html id="a" id="b">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=324378
+-->
+<head id="c" id="d">
+ <head id="j" foo="k" foo="l">
+ <title>Test for Bug 324378</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body id="e" id="f">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=324378">Mozilla Bug 324378</a>
+<script>
+ var html = document.documentElement;
+ is(document.getElementsByTagName("html").length, 1,
+ "Unexpected number of htmls");
+ is(document.getElementsByTagName("html")[0], html,
+ "Unexpected <html> element");
+ is(document.getElementsByTagName("head").length, 1,
+ "Unexpected number of heads");
+ is(html.getElementsByTagName("head").length, 1,
+ "Unexpected number of heads in <html>");
+ is(document.getElementsByTagName("body").length, 1,
+ "Unexpected number of bodies");
+ is(html.getElementsByTagName("body").length, 1,
+ "Unexpected number of bodies in <html>");
+ var head = document.getElementsByTagName("head")[0];
+ var body = document.getElementsByTagName("body")[0];
+</script>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <html id="g" foo="h" foo="i">
+ <body id="m" foo="n" foo="o">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 324378 **/
+ is(document.getElementsByTagName("html").length, 1,
+ "Unexpected number of htmls after additions");
+ is(document.getElementsByTagName("html")[0], html,
+ "Unexpected <html> element");
+ is(document.documentElement, html,
+ "Unexpected root node");
+ is(document.getElementsByTagName("head").length, 1,
+ "Unexpected number of heads after additions");
+ is(document.getElementsByTagName("head")[0], head,
+ "Unexpected <head> element");
+ is(document.getElementsByTagName("body").length, 1,
+ "Unexpected number of bodies after additions");
+ is(document.getElementsByTagName("body")[0], body,
+ "Unexpected <body> element");
+
+ is(html.id, "a", "Unexpected <html> id");
+ is(head.id, "c", "Unexpected <head> id");
+ is(body.id, "e", "Unexpected <body> id");
+ is($("a"), html, "Unexpected node with id=a");
+ is($("b"), null, "Unexpected node with id=b");
+ is($("c"), head, "Unexpected node with id=c");
+ is($("d"), null, "Unexpected node with id=d");
+ is($("e"), body, "Unexpected node with id=e");
+ is($("f"), null, "Unexpected node with id=f");
+ is($("g"), null, "Unexpected node with id=g");
+ is($("j"), null, "Unexpected node with id=j");
+ is($("m"), null, "Unexpected node with id=m");
+
+ is(html.getAttribute("foo"), "h", "Unexpected 'foo' value on <html>");
+ is(head.getAttribute("foo"), null, "Unexpected 'foo' value on <head>");
+ is(body.getAttribute("foo"), "n", "Unexpected 'foo' value on <body>");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug330705-1.html b/dom/html/test/test_bug330705-1.html
new file mode 100644
index 0000000000..64b6e89b29
--- /dev/null
+++ b/dom/html/test/test_bug330705-1.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=330705
+-->
+<head>
+ <title>Test for Bug 330705</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script>
+ /* variable is true if the element is focused, false otherwise */
+ var inputFocused = false;
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=330705">Mozilla Bug 330705</a>
+<p id="display">
+ <input onfocus="inputFocused = true" onblur="inputFocused = false" type="text">
+ <button></button>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Bug 330705 **/
+ SimpleTest.waitForExplicitFinish();
+ var isFocused = false;
+
+ function onLoad() {
+ document.getElementsByTagName('input')[0].focus();
+ document.getElementsByTagName('button')[0].blur();
+ ok(inputFocused == true, "the input element is still focused after blur() has been called on the unfocused element");
+ SimpleTest.finish();
+ }
+
+ addLoadEvent(onLoad);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug332246.html b/dom/html/test/test_bug332246.html
new file mode 100644
index 0000000000..e4fbd20bec
--- /dev/null
+++ b/dom/html/test/test_bug332246.html
@@ -0,0 +1,75 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=332246
+-->
+<head>
+ <title>Test for Bug 332246 - scrollIntoView(false) doesn't work correctly for inline elements that wrap at multiple lines</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=332246">Mozilla Bug 332246</a>
+<p id="display"></p>
+<div id="content">
+
+<div id="a1" style="height: 100px; width: 100px; overflow: hidden; outline:1px dotted black;">
+<div style="height: 100px"></div>
+<a id="a2" href="#" style="display:block; background:yellow; height:200px;">Top</a>
+<div style="height: 100px"></div>
+</div>
+
+<div id="b1" style="height: 100px; width: 100px; overflow: hidden; outline:1px dotted black;">
+<div style="height: 100px"></div>
+<div id="b2" href="#" style="border:10px solid black; background:yellow; height:200px;"></div>
+<div style="height: 100px"></div>
+</div>
+
+<br>
+
+<div id="c1" style="height: 100px; width: 100px; overflow: hidden; position: relative; outline:1px dotted black;">
+<div id="c2" style="border: 10px solid black; height: 200px; width: 50px; position: absolute; top: 100px;"></div>
+<div style="height: 100px"></div>
+</div>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 332246 **/
+
+function isWithFuzz(itIs, itShouldBe, fuzz, description) {
+ ok(Math.abs(itIs - itShouldBe) <= fuzz, `${description} - expected a value between ${itShouldBe - fuzz} and ${itShouldBe + fuzz}, got ${itIs}`);
+}
+
+var a1 = document.getElementById('a1');
+var a2 = document.getElementById('a2');
+isWithFuzz(a1.scrollHeight, 400, 1, "Wrong a1.scrollHeight");
+is(a1.offsetHeight, 100, "Wrong a1.offsetHeight");
+a2.scrollIntoView(true);
+is(a1.scrollTop, 100, "Wrong scrollTop value after a2.scrollIntoView(true)");
+a2.scrollIntoView(false);
+is(a1.scrollTop, 200, "Wrong scrollTop value after a2.scrollIntoView(false)");
+
+var b1 = document.getElementById('b1');
+var b2 = document.getElementById('b2');
+isWithFuzz(b1.scrollHeight, 420, 1, "Wrong b1.scrollHeight");
+is(b1.offsetHeight, 100, "Wrong b1.offsetHeight");
+b2.scrollIntoView(true);
+is(b1.scrollTop, 100, "Wrong scrollTop value after b2.scrollIntoView(true)");
+b2.scrollIntoView(false);
+is(b1.scrollTop, 220, "Wrong scrollTop value after b2.scrollIntoView(false)");
+
+var c1 = document.getElementById('c1');
+var c2 = document.getElementById('c2');
+isWithFuzz(c1.scrollHeight, 320, 1, "Wrong c1.scrollHeight");
+is(c1.offsetHeight, 100, "Wrong c1.offsetHeight");
+c2.scrollIntoView(true);
+is(c1.scrollTop, 100, "Wrong scrollTop value after c2.scrollIntoView(true)");
+c2.scrollIntoView(false);
+isWithFuzz(c1.scrollTop, 220, 1, "Wrong scrollTop value after c2.scrollIntoView(false)");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug332848.xhtml b/dom/html/test/test_bug332848.xhtml
new file mode 100644
index 0000000000..a7a1950125
--- /dev/null
+++ b/dom/html/test/test_bug332848.xhtml
@@ -0,0 +1,86 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=332848
+-->
+<head>
+ <title>Test for Bug 332848</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=332848">Mozilla Bug 332848</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+/** Test for Bug 332848 **/
+
+// parseChecker will become true if we keep parsing after calling close().
+var parseChecker = false;
+
+function test() {
+ try {
+ document.open();
+ is(0, 1, "document.open succeeded");
+ } catch (e) {
+ is (e.name, "InvalidStateError",
+ "Wrong exception from document.open");
+ is (e.code, DOMException.INVALID_STATE_ERR,
+ "Wrong exception from document.open");
+ }
+
+ try {
+ document.write("aaa");
+ is(0, 1, "document.write succeeded");
+ } catch (e) {
+ is (e.name, "InvalidStateError",
+ "Wrong exception from document.write");
+ is (e.code, DOMException.INVALID_STATE_ERR,
+ "Wrong exception from document.write");
+ }
+
+ try {
+ document.writeln("aaa");
+ is(0, 1, "document.write succeeded");
+ } catch (e) {
+ is (e.name, "InvalidStateError",
+ "Wrong exception from document.write");
+ is (e.code, DOMException.INVALID_STATE_ERR,
+ "Wrong exception from document.write");
+ }
+
+ try {
+ document.close();
+ is(0, 1, "document.close succeeded");
+ } catch (e) {
+ is (e.name, "InvalidStateError",
+ "Wrong exception from document.close");
+ is (e.code, DOMException.INVALID_STATE_ERR,
+ "Wrong exception from document.close");
+ }
+}
+
+function loadTest() {
+ is(parseChecker, true, "Parsing stopped");
+ test();
+ SimpleTest.finish();
+}
+
+window.onload = loadTest;
+
+SimpleTest.waitForExplicitFinish();
+
+test();
+]]>
+</script>
+<script>
+ parseChecker = true;
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug332893-1.html b/dom/html/test/test_bug332893-1.html
new file mode 100644
index 0000000000..552da95c28
--- /dev/null
+++ b/dom/html/test/test_bug332893-1.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+
+<form id="form1">
+ <input id="F1I1" type="input" value="11"/>
+ <input id="F1I2" type="input" value="12"/>
+</form>
+<form id="form2">
+ <input id="F2I1" type="input" value="21"/>
+ <input id="F2I2" type="input" value="22"/>
+</form>
+<script>
+<!-- Create a new input, add it to the first form, move it to the 2nd form, then move it back to the first -->
+ var form1 = document.getElementById("form1");
+ var form2 = document.getElementById("form2");
+ var newInput = document.createElement("input");
+ newInput.value = "13";
+ form1.insertBefore(newInput, form1.firstChild);
+ var F2I2 = document.getElementById("F2I2");
+ form2.insertBefore(newInput, F2I2);
+ form1.insertBefore(newInput, form1.firstChild);
+
+ is(form1.elements.length, 3, "Form 1 has the correct length");
+ is(form1.elements[0].value, "13", "Form 1 element 1 is correct");
+ is(form1.elements[1].value, "11", "Form 1 element 2 is correct");
+ is(form1.elements[2].value, "12", "Form 1 element 3 is correct");
+
+ is(form2.elements.length, 2, "Form 2 has the correct length");
+ is(form2.elements[0].value, "21", "Form 2 element 1 is correct");
+ is(form2.elements[1].value, "22", "Form 2 element 2 is correct");
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug332893-2.html b/dom/html/test/test_bug332893-2.html
new file mode 100644
index 0000000000..d24b746566
--- /dev/null
+++ b/dom/html/test/test_bug332893-2.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+
+
+<form id="form1">
+ <table>
+ <tbody id="table1">
+ <tr id="F1I0"><td><input form='form1' type="input" value="10"/></td></tr>
+ <tr id="F1I1"><td><input type="input" value="11"/></td></tr>
+ <tr id="F1I2"><td><input type="input" value="12"/></td></tr>
+ </tbody>
+ </table>
+</form>
+<form id="form2">
+ <table>
+ <tbody id="table2">
+ <tr id="F2I1"><td><input type="input" value="21"/></td></tr>
+ <tr id="F2I2"><td><input type="input" value="22"/></td></tr>
+ </tbody>
+ </table>
+</form>
+
+<script>
+ var table1 = document.getElementById("table1");
+ var F1I0 = table1.getElementsByTagName("tr")[0];
+ var F1I1 = table1.getElementsByTagName("tr")[1];
+ table1.removeChild(F1I0);
+ table1.removeChild(F1I1);
+
+ var table2 = document.getElementById("table2");
+ table2.insertBefore(F1I0, table2.firstChild);
+ table2.insertBefore(F1I1, table2.firstChild);
+
+ var form1 = document.getElementById("form1");
+ var form2 = document.getElementById("form2");
+
+ is(form1.elements.length, 2, "Form 1 length is correct");
+ is(form1.elements[0].value, "12", "Form 1 first element is correct");
+ is(form1.elements[1].value, "10", "Form 2 second element is correct");
+ is(form2.elements.length, 3, "Form 2 length is correct");
+ is(form2.elements[0].value, "11", "Form 2 element 1 is correct");
+ is(form2.elements[1].value, "21", "Form 2 element 2 is correct");
+ is(form2.elements[2].value, "22", "Form 2 element 3 is correct");
+
+</script>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug332893-3.html b/dom/html/test/test_bug332893-3.html
new file mode 100644
index 0000000000..247607dcb2
--- /dev/null
+++ b/dom/html/test/test_bug332893-3.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<form id="form1">
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ <table>
+ <tbody id="table1">
+ <tr id="F1I0"><td><input form='form1' type="input" value="10"/></td></tr>
+ <tr id="F1I1"><td><input type="input" value="11"/></td></tr>
+ <tr id="F1I2"><td><input type="input" value="12"/></td></tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</form>
+<form id="form2">
+ <table>
+ <tbody id="table2">
+ <tr id="F2I1"><td><input type="input" value="21"/></td></tr>
+ <tr id="F2I2"><td><input type="input" value="22"/></td></tr>
+ </tbody>
+ </table>
+</form>
+
+<script>
+ var table1 = document.getElementById("table1");
+ var F1I0 = table1.getElementsByTagName("tr")[0];
+ var F1I1 = table1.getElementsByTagName("tr")[1];
+ table1.removeChild(F1I0);
+ table1.removeChild(F1I1);
+
+ var table2 = document.getElementById("table2");
+ table2.insertBefore(F1I0, table2.firstChild);
+ table2.insertBefore(F1I1, table2.firstChild);
+
+ var form1 = document.getElementById("form1");
+ var form2 = document.getElementById("form2");
+
+ is(form1.elements.length, 2, "Form 1 has the correct length");
+ is(form1.elements[0].value, "12", "Form 1 element 1 is correct");
+ is(form1.elements[1].value, "10", "Form 1 element 2 is correct");
+
+ is(form2.elements.length, 3, "Form 2 has the correct length");
+ is(form2.elements[0].value, "11", "Form 2 element 1 is correct");
+ is(form2.elements[1].value, "21", "Form 2 element 2 is correct");
+ is(form2.elements[2].value, "22", "Form 2 element 2 is correct");
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug332893-4.html b/dom/html/test/test_bug332893-4.html
new file mode 100644
index 0000000000..72a0239a5d
--- /dev/null
+++ b/dom/html/test/test_bug332893-4.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<form id="form1">
+ <input id="input1" type="input" name="input" value="1"/>
+ <input id="input2" type="input" name="input" value="2"/>
+ <input id="input3" type="input" name="input" value="3"/>
+</form>
+<script>
+ var input1 = document.getElementById("input1");
+ var input2 = document.getElementById("input2");
+ var form1 = document.getElementById("form1");
+ form1.insertBefore(input2, input1);
+
+ is(form1.elements.input.length, 3, "Form 1 'input' has the correct length");
+ is(form1.elements.input[0].value, "2", "Form 1 element 1 is correct");
+ is(form1.elements.input[1].value, "1", "Form 1 element 2 is correct");
+ is(form1.elements.input[2].value, "3", "Form 1 element 3 is correct");
+
+ is(form1.elements.input[0].id, "input2", "Form 1 element 1 id is correct");
+ is(form1.elements.input[1].id, "input1", "Form 1 element 2 id is correct");
+ is(form1.elements.input[2].id, "input3", "Form 1 element 3 id is correct");
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug332893-5.html b/dom/html/test/test_bug332893-5.html
new file mode 100644
index 0000000000..e5fb9b94d6
--- /dev/null
+++ b/dom/html/test/test_bug332893-5.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<form id="form1">
+ <input id="input1" type="input" name="input" value="1"/>
+ <input id="input" type="input" name="input_other" value="2"/>
+ <input id="input3" type="input" name="input" value="3"/>
+</form>
+<script>
+ var input1 = document.getElementById("input1");
+ var input2 = document.getElementById("input");
+ var form1 = document.getElementById("form1");
+ form1.insertBefore(input2, input1);
+
+ is(form1.elements.input.length, 3, "Form 1 'input' has the correct length");
+ is(form1.elements.input[0].value, "2", "Form 1 element 1 is correct");
+ is(form1.elements.input[1].value, "1", "Form 1 element 2 is correct");
+ is(form1.elements.input[2].value, "3", "Form 1 element 3 is correct");
+
+ is(form1.elements.input[0].id, "input", "Form 1 element 1 id is correct");
+ is(form1.elements.input[1].id, "input1", "Form 1 element 2 id is correct");
+ is(form1.elements.input[2].id, "input3", "Form 1 element 3 id is correct");
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug332893-6.html b/dom/html/test/test_bug332893-6.html
new file mode 100644
index 0000000000..b12e7a0c3a
--- /dev/null
+++ b/dom/html/test/test_bug332893-6.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<form id="form1">
+ <input id="input1" type="input" name="input" value="1"/>
+ <input id="input" type="input" name="input_other" value="2"/>
+ <input id="input3" type="input" name="input" value="3"/>
+</form>
+<script>
+ var input1 = document.getElementById("input1");
+ var input2 = document.getElementById("input");
+ var form1 = document.getElementById("form1");
+ form1.insertBefore(input2, input1);
+
+ is(form1.elements.input.length, 3, "Form 1 'input' has the correct length");
+ is(form1.elements.input[0].value, "2", "Form 1 element 1 is correct");
+ is(form1.elements.input[1].value, "1", "Form 1 element 2 is correct");
+
+ is(form1.elements.input[0].id, "input", "Form 1 element 1 id is correct");
+ is(form1.elements.input[1].id, "input1", "Form 1 element 2 id is correct");
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug332893-7.html b/dom/html/test/test_bug332893-7.html
new file mode 100644
index 0000000000..15672d2d20
--- /dev/null
+++ b/dom/html/test/test_bug332893-7.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<form id="form1">
+ <input id="input1" type="input" name="input" value="1"/>
+ <input id="input2" type="input" name="input" value="2"/>
+ <input id="input3" type="input" name="input" value="3"/>
+ <input id="input4" type="input" name="input" value="4"/>
+ <input id="input5" type="input" name="input" value="5"/>
+ <input id="input6" type="input" name="input" value="6"/>
+ <input id="input7" type="input" name="input" value="7"/>
+ <input id="input8" type="input" name="input" value="8"/>
+ <input id="input9" type="input" name="input" value="9"/>
+ <input id="input10" type="input" name="input" value="10"/>
+
+
+
+
+</form>
+<script>
+ var input1 = document.getElementById("input1");
+ var input2 = document.getElementById("input2");
+ var input3 = document.getElementById("input3");
+ var input4 = document.getElementById("input4");
+ var input5 = document.getElementById("input5");
+ var input6 = document.getElementById("input6");
+ var input7 = document.getElementById("input7");
+ var input8 = document.getElementById("input8");
+ var input9 = document.getElementById("input9");
+ var input10 = document.getElementById("input10");
+
+
+ var form1 = document.getElementById("form1");
+
+ form1.insertBefore(input2, input1);
+ form1.insertBefore(input10, input6);
+ form1.insertBefore(input8, input4);
+ form1.insertBefore(input9, input2);
+
+ is(form1.elements.input.length, 10, "Form 1 'input' has the correct length");
+ is(form1.elements.input[0].value, "9", "Form 1 element 1 is correct");
+ is(form1.elements.input[1].value, "2", "Form 1 element 2 is correct");
+ is(form1.elements.input[2].value, "1", "Form 1 element 3 is correct");
+ is(form1.elements.input[3].value, "3", "Form 1 element 4 is correct");
+ is(form1.elements.input[4].value, "8", "Form 1 element 5 is correct");
+ is(form1.elements.input[5].value, "4", "Form 1 element 6 is correct");
+ is(form1.elements.input[6].value, "5", "Form 1 element 7 is correct");
+ is(form1.elements.input[7].value, "10", "Form 1 element 8 is correct");
+ is(form1.elements.input[8].value, "6", "Form 1 element 9 is correct");
+ is(form1.elements.input[9].value, "7", "Form 1 element 10 is correct");
+
+ is(form1.elements.input[0].id, "input9", "Form 1 element 1 id is correct");
+ is(form1.elements.input[1].id, "input2", "Form 1 element 2 id is correct");
+ is(form1.elements.input[2].id, "input1", "Form 1 element 3 id is correct");
+ is(form1.elements.input[3].id, "input3", "Form 1 element 4 id is correct");
+ is(form1.elements.input[4].id, "input8", "Form 1 element 5 id is correct");
+ is(form1.elements.input[5].id, "input4", "Form 1 element 6 id is correct");
+ is(form1.elements.input[6].id, "input5", "Form 1 element 7 id is correct");
+ is(form1.elements.input[7].id, "input10", "Form 1 element 8 id is correct");
+ is(form1.elements.input[8].id, "input6", "Form 1 element 9 id is correct");
+ is(form1.elements.input[9].id, "input7", "Form 1 element 10 id is correct");
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug3348.html b/dom/html/test/test_bug3348.html
new file mode 100644
index 0000000000..a0f99d9c43
--- /dev/null
+++ b/dom/html/test/test_bug3348.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=3348
+-->
+<head>
+ <title>Test for Bug 3348</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=3348">Mozilla Bug 3348</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+<form id="form1">
+<input type="button" value="click here" onclick="buttonClick();">
+</form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 3348 **/
+
+var oForm = document.getElementById("form1");
+is(oForm.tagName, "FORM", "tagName of HTML element gives tag in upper case");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug340017.xhtml b/dom/html/test/test_bug340017.xhtml
new file mode 100644
index 0000000000..69de021e41
--- /dev/null
+++ b/dom/html/test/test_bug340017.xhtml
@@ -0,0 +1,27 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=340017
+-->
+<head>
+ <title>Test for Bug 340017</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=340017">Mozilla Bug 340017</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form id="frmfoo" name="foo" action="" />
+</div>
+<pre id="test">
+<script type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 340017 **/
+is(document.foo, document.getElementById("frmfoo"),
+ "The form with name 'foo' should be a document accessible property");
+]]>
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug340800.html b/dom/html/test/test_bug340800.html
new file mode 100644
index 0000000000..bcb5b2de08
--- /dev/null
+++ b/dom/html/test/test_bug340800.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=340800
+-->
+<head>
+ <title>Test for Bug 340800</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=340800">Mozilla Bug 340800</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <h1>iframe text/plain as DOM test</h1>
+
+ <div>
+
+ <iframe name="iframe1" width="100%" height="200"
+ src="bug340800_iframe.txt"></iframe>
+ </div>
+
+ <div>
+ <h2>textarea with iframe content</h2>
+ <textarea rows="10" cols="80" id="textarea1"></textarea>
+ </div>
+
+ <div>
+ <h2>div with white-space: pre and iframe content</h2>
+ <div id="div1"></div>
+ </div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 340800 **/
+function populateIframes () {
+ var iframe, iframeBody;
+ if ((iframe = window.frames.iframe1) && (iframeBody = iframe.document.body)) {
+ $('div1').innerHTML = iframeBody.innerHTML;
+ $('textarea1').value = iframeBody.innerHTML;
+ }
+ is($('div1').firstChild.tagName, "PRE", "innerHTML from txt iframe works with div");
+ ok($('textarea1').value.indexOf("<pre>") > -1, "innerHTML from txt iframe works with textarea.value");
+ SimpleTest.finish();
+}
+
+addLoadEvent(populateIframes);
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug347174.html b/dom/html/test/test_bug347174.html
new file mode 100644
index 0000000000..a453627f06
--- /dev/null
+++ b/dom/html/test/test_bug347174.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=347174
+-->
+<head>
+ <title>Test for Bug 347174</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=347174">Mozilla Bug 347174</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 347174 **/
+// simple test of readyState during loading, DOMContentLoaded, and complete
+// this test passes in IE7
+window.readyStateText = [];
+window.readyStateText.push("script tag: " + document.readyState);
+is(document.readyState, "loading", "document.readyState should be 'loading' when scripts runs initially");
+
+function attachCustomEventListener(element, eventName, command) {
+ if (window.addEventListener && !window.opera)
+ element.addEventListener(eventName, command, true);
+ else if (window.attachEvent)
+ element.attachEvent("on" + eventName, command);
+}
+
+function showMessage(msg) {
+ window.readyStateText.push(msg);
+ document.getElementById("display").innerHTML = readyStateText.join("<br>");
+}
+
+function load() {
+ is(document.readyState, "complete", "document.readyState should be 'complete' on load");
+ showMessage("load: " + document.readyState);
+ SimpleTest.finish();
+}
+
+function readyStateChange() {
+ showMessage("readyStateChange: " + document.readyState);
+}
+
+function DOMContentLoaded() {
+ is(document.readyState, "interactive", "document.readyState should be 'interactive' on DOMContentLoaded");
+ showMessage("DOMContentLoaded: " + document.readyState);
+}
+
+window.onload=load;
+
+attachCustomEventListener(document, "readystatechange", readyStateChange);
+attachCustomEventListener(document, "DOMContentLoaded", DOMContentLoaded);
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug347174_write.html b/dom/html/test/test_bug347174_write.html
new file mode 100644
index 0000000000..75912b59a9
--- /dev/null
+++ b/dom/html/test/test_bug347174_write.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=347174
+-->
+<head>
+ <title>Test for Bug 347174</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=347174">Mozilla Bug 347174</a>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 347174 **/
+// simple test of readyState during loading, DOMContentLoaded, and complete
+// this test passes in IE7
+window.readyStateText = [];
+window.loaded = false;
+function attachCustomEventListener(element, eventName, command) {
+ if (window.addEventListener && !window.opera)
+ element.addEventListener(eventName, command, true);
+ else if (window.attachEvent)
+ element.attachEvent("on" + eventName, command);
+}
+
+function showMessage(msg) {
+ window.readyStateText.push(msg);
+ document.getElementById("display").innerHTML = readyStateText.join("<br>");
+}
+
+function frameLoad() {
+ var doc = $('iframe').contentWindow.document;
+ is(doc.readyState, "complete", "frame document.readyState should be 'complete' on load");
+ showMessage("frame load: " + doc.readyState);
+ if (window.loaded) SimpleTest.finish();
+}
+
+function load() {
+ window.loaded = true;
+
+ var imgsrc = "<img onload ='window.parent.imgLoad()' src='image.png?noCache="
+ + (new Date().getTime()) + "'>\n";
+ var doc = $('iframe').contentWindow.document;
+ doc.writeln(imgsrc);
+ doc.close();
+ showMessage("frame after document.write: " + doc.readyState);
+ isnot(doc.readyState, "complete", "frame document.readyState should not be 'complete' after document.write");
+}
+
+function imgLoad() {
+ var doc = $('iframe').contentWindow.document;
+ showMessage("frame after imgLoad: " + doc.readyState);
+ is(doc.readyState, "interactive", "frame document.readyState should still be 'interactive' after img loads");
+}
+
+window.onload=load;
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+<iframe src="404doesnotexist" id="iframe" onload="frameLoad();"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/test_bug347174_xsl.html b/dom/html/test/test_bug347174_xsl.html
new file mode 100644
index 0000000000..e396e8b721
--- /dev/null
+++ b/dom/html/test/test_bug347174_xsl.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=347174
+-->
+<head>
+ <title>Test for Bug 347174</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=347174">Mozilla Bug 347174</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <iframe src="347174transformable.xml" id="iframe"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 347174 **/
+// Test of readyState of XML document transformed via XSLT to HTML
+// this test passes in IE7
+window.readyStateText = [];
+
+function showMessage(msg) {
+ window.readyStateText.push(msg);
+ document.getElementById("display").innerHTML = readyStateText.join("<br>");
+}
+
+function frameScriptTag(readyState) {
+ isnot(readyState, "complete", "document.readyState should not be 'complete' when scripts run initially");
+ showMessage("script tag: " + readyState);
+}
+
+function frameLoad(readyState) {
+ is(readyState, "complete", "document.readyState should be 'complete' on load");
+ showMessage("load: " + readyState);
+ SimpleTest.finish();
+}
+
+function frameReadyStateChange(readyState) {
+ showMessage("readyStateChange: " + readyState);
+}
+
+function frameDOMContentLoaded(readyState) {
+ is(readyState, "interactive", "document.readyState should be 'interactive' on DOMContentLoaded");
+ showMessage("DOMContentLoaded: " + readyState);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug347174_xslp.html b/dom/html/test/test_bug347174_xslp.html
new file mode 100644
index 0000000000..313e96d1d0
--- /dev/null
+++ b/dom/html/test/test_bug347174_xslp.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=347174
+-->
+<head>
+ <title>Test for Bug 347174</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=347174">Mozilla Bug 347174</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 347174 **/
+// verifies that documents created with createDocument are born in "complete" state
+// (so we don't accidentally leave them in "interactive" state)
+window.readyStateText = [];
+
+function runTest() {
+ var xhr = new XMLHttpRequest();
+ xhr.responseType = "document";
+ xhr.open("GET", "347174transform.xsl");
+ xhr.send();
+ xhr.onload = function() {
+ var xslDoc = xhr.responseXML.documentElement;
+
+ var processor = new XSLTProcessor();
+ processor.importStylesheet(xslDoc);
+
+ window.transformedDoc = processor.transformToDocument(xmlDoc);
+
+ showMessage("loaded: " + xmlDoc.readyState);
+ is(xmlDoc.readyState, "complete", "XML document.readyState should be 'complete' after transform");
+ SimpleTest.finish();
+ };
+}
+
+var xmlDoc = document.implementation.createDocument("", "test", null);
+showMessage("createDocument: " + xmlDoc.readyState);
+is(xmlDoc.readyState, "complete", "created document readyState should be 'complete' before being associated with a parser");
+
+runTest();
+
+function showMessage(msg) {
+ window.readyStateText.push(msg);
+ $("display").innerHTML = readyStateText.join("<br>");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug353415-1.html b/dom/html/test/test_bug353415-1.html
new file mode 100644
index 0000000000..5cb93e0577
--- /dev/null
+++ b/dom/html/test/test_bug353415-1.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<iframe name="submit_frame"></iframe>
+<form method="get" id="form1" target="submit_frame" action="../../../../../blah">
+<input type="text" name="field1" value="teststring"><br>
+<input type="radio" name="field2" value="0" checked> 0
+<input type="radio" name="field3" value="1"> 1<br>
+<input type="checkbox" name="field4" value="1" checked> 1
+<input type="checkbox" name="field5" value="2"> 2
+<input type="checkbox" name="field6" value="3" checked> 3
+<select name="field7">
+<option value="1">1</option>
+<option value="2" selected>2</option>
+<option value="3">3</option>
+<option value="4">4</option>
+</select>
+<input name="field8" value="8">
+<input name="field9" value="9">
+<input type="image" name="field10">
+<label name="field11">
+<input name="field12">
+<input type="button" name="field13" value="button">
+</form>
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ document.getElementsByName('submit_frame')[0].onload = function() {
+ is(frames.submit_frame.location.href, `${location.origin}/blah?field1=teststring&field2=0&field4=1&field6=3&field7=2&field8=8&field9=9&field12=`, "Submit string was correct.");
+ SimpleTest.finish();
+ };
+
+ document.forms[0].submit();
+ });
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug353415-2.html b/dom/html/test/test_bug353415-2.html
new file mode 100644
index 0000000000..d27480dff4
--- /dev/null
+++ b/dom/html/test/test_bug353415-2.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<iframe name="submit_frame"></iframe>
+<form method="get" id="form1" target="submit_frame" action="../../../../../blah">
+<table>
+<tr><td>
+<input type="text" name="field1" value="teststring"><br>
+<input type="radio" name="field2" value="0" checked> 0
+<input type="radio" name="field3" value="1"> 1<br>
+<input type="checkbox" name="field4" value="1" checked> 1
+<input type="checkbox" name="field5" value="2"> 2
+<input type="checkbox" name="field6" value="3" checked> 3
+<select name="field7">
+<option value="1">1</option>
+<option value="2" selected>2</option>
+<option value="3">3</option>
+<option value="4">4</option>
+</select>
+<input name="field8" value="8">
+<input name="field9" value="9">
+<input type="image" name="field10">
+<label name="field11"></label>
+<input name="field12">
+<input type="button" name="field13" value="button">
+<input type="hidden" name="field14" value="14">
+</td>
+<input type="text" name="field1-2" value="teststring"><br>
+<input type="radio" name="field2-2" value="0" checked> 0
+<input type="radio" name="field3-2" value="1"> 1<br>
+<input type="checkbox" name="field4-2" value="1" checked> 1
+<input type="checkbox" name="field5-2" value="2"> 2
+<input type="checkbox" name="field6-2" value="3" checked> 3
+<select name="field7-2">
+<option value="1">1</option>
+<option value="2" selected>2</option>
+<option value="3">3</option>
+<option value="4">4</option>
+</select>
+<input name="field8-2" value="8">
+<input name="field9-2" value="9">
+<input type="image" name="field10-2">
+<label name="field11-2"></label>
+<input name="field12-2">
+<input type="button" name="field13-2" value="button">
+<input type="hidden" name="field14-2" value="14">
+</tr>
+</table>
+</form>
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ document.getElementsByName('submit_frame')[0].onload = function() {
+ is(frames.submit_frame.location.href, `${location.origin}/blah?field1-2=teststring&field2-2=0&field4-2=1&field6-2=3&field7-2=2&field8-2=8&field9-2=9&field12-2=&field1=teststring&field2=0&field4=1&field6=3&field7=2&field8=8&field9=9&field12=&field14=14&field14-2=14`, "Submit string was correct.");
+ SimpleTest.finish();
+ };
+
+ document.forms[0].submit();
+ });
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug359657.html b/dom/html/test/test_bug359657.html
new file mode 100644
index 0000000000..fe24eace2e
--- /dev/null
+++ b/dom/html/test/test_bug359657.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=359657
+-->
+<head>
+ <title>Test for Bug 359657</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=359657">Mozilla Bug 359657</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+/** Test for Bug 359657 **/
+function runTest() {
+ var span = document.createElement("span");
+ $("test").insertBefore(span, $("test").firstChild);
+ ok(true, "Reachability, we should get here without crashing");
+ is($("test").firstChild, span, "First child is correct");
+ SimpleTest.finish();
+}
+</script>
+<div>
+ <iframe src=""></iframe>
+ <!-- Important: This test needs to run async at this point. The actual test
+ is not crashing while running this test! -->
+ <script type="text/javascript" src="data:text/javascript,runTest()"></script>
+</div>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug369370.html b/dom/html/test/test_bug369370.html
new file mode 100644
index 0000000000..c39e6e3243
--- /dev/null
+++ b/dom/html/test/test_bug369370.html
@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=369370
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Test for Bug 369370</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <script type="text/javascript">
+ /*
+ * Test strategy:
+ */
+ function makeClickFor(x, y) {
+ var event = kidDoc.createEvent("mouseevent");
+ event.initMouseEvent("click",
+ true, true, kidWin, 1, // bubbles, cancelable, view, single-click
+ x, y, x, y, // screen X/Y, client X/Y
+ false, false, false, false, // no key modifiers
+ 0, null); // left click, not relatedTarget
+ return event;
+ }
+
+ function childLoaded() {
+ kidDoc = kidWin.document;
+ ok(true, "Child window loaded");
+
+ var elements = kidDoc.getElementsByTagName("img");
+ is(elements.length, 1, "looking for imagedoc img");
+ var img = elements[0];
+
+ // Need to use innerWidth/innerHeight of the window
+ // since the containing image is absolutely positioned,
+ // causing clientHeight to be zero.
+ is(kidWin.innerWidth, 400, "Checking doc width");
+ is(kidWin.innerHeight, 300, "Checking doc height");
+
+ // Image just loaded and is scaled to window size.
+ is(img.width, 400, "image width");
+ is(img.height, 300, "image height");
+ is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft");
+ is(kidDoc.body.scrollTop, 0, "Checking scrollTop");
+
+ // ========== test 1 ==========
+ // Click in the upper left to zoom in
+ var event = makeClickFor(25,25);
+ img.dispatchEvent(event);
+ ok(true, "----- click 1 -----");
+
+ is(img.width, 800, "image width");
+ is(img.height, 600, "image height");
+ is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft");
+ is(kidDoc.body.scrollTop, 0, "Checking scrollTop");
+
+ // ========== test 2 ==========
+ // Click there again to zoom out
+ event = makeClickFor(25,25);
+ img.dispatchEvent(event);
+ ok(true, "----- click 2 -----");
+
+ is(img.width, 400, "image width");
+ is(img.height, 300, "image height");
+ is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft");
+ is(kidDoc.body.scrollTop, 0, "Checking scrollTop");
+
+ // ========== test 3 ==========
+ // Click in the lower right to zoom in
+ event = makeClickFor(350, 250);
+ img.dispatchEvent(event);
+ ok(true, "----- click 3 -----");
+
+ is(img.width, 800, "image width");
+ is(img.height, 600, "image height");
+ is(kidDoc.body.scrollLeft,
+ kidDoc.body.scrollLeftMax, "Checking scrollLeft");
+ is(kidDoc.body.scrollTop,
+ kidDoc.body.scrollTopMax, "Checking scrollTop");
+
+ // ========== test 4 ==========
+ // Click there again to zoom out
+ event = makeClickFor(350, 250);
+ img.dispatchEvent(event);
+ ok(true, "----- click 4 -----");
+
+ is(img.width, 400, "image width");
+ is(img.height, 300, "image height");
+ is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft");
+ is(kidDoc.body.scrollTop, 0, "Checking scrollTop");
+
+ // ========== test 5 ==========
+ // Click in the upper left to zoom in again
+ event = makeClickFor(25, 25);
+ img.dispatchEvent(event);
+ ok(true, "----- click 5 -----");
+ is(img.width, 800, "image width");
+ is(img.height, 600, "image height");
+ is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft");
+ is(kidDoc.body.scrollTop, 0, "Checking scrollTop");
+ is(img.getBoundingClientRect().top, 0, "Image is in view vertically");
+
+ // ========== test 6 ==========
+ // Now try resizing the window so the image fits vertically.
+ function test6() {
+ kidWin.addEventListener("resize", function() {
+ // Give the image document time to respond
+ SimpleTest.executeSoon(function() {
+ is(img.height, 600, "image height");
+ var bodyHeight = kidDoc.documentElement.scrollHeight;
+ var imgRect = img.getBoundingClientRect();
+ is(imgRect.top, bodyHeight - imgRect.bottom, "Image is vertically centered");
+ test7();
+ });
+ }, {once: true});
+
+ var decorationSize = kidWin.outerHeight - kidWin.innerHeight;
+ kidWin.resizeTo(400, 600 + 50 + decorationSize);
+ }
+
+ // ========== test 7 ==========
+ // Now try resizing the window so the image no longer fits vertically.
+ function test7() {
+ kidWin.addEventListener("resize", function() {
+ // Give the image document time to respond
+ SimpleTest.executeSoon(function() {
+ is(img.height, 600, "image height");
+ is(img.getBoundingClientRect().top, 0, "Image is at top again");
+ kidWin.close();
+ SimpleTest.finish();
+ });
+ }, {once: true});
+
+ var decorationSize = kidWin.outerHeight - kidWin.innerHeight;
+ kidWin.resizeTo(400, 300 + decorationSize);
+ }
+
+ test6();
+ }
+ var kidWin;
+ var kidDoc;
+
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({"set":[["browser.enable_automatic_image_resizing", true]]}, function() {
+ kidWin = window.open("bug369370-popup.png", "bug369370", "width=400,height=300");
+ // will init onload
+ ok(kidWin, "opened child window");
+ kidWin.onload = childLoaded;
+ });
+ </script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug371375.html b/dom/html/test/test_bug371375.html
new file mode 100644
index 0000000000..1cd0865293
--- /dev/null
+++ b/dom/html/test/test_bug371375.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=371375
+-->
+<head>
+ <title>Test for Bug 371375</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=371375">Mozilla Bug 371375</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ /** Test for Bug 371375 **/
+ var load1Called = false;
+ var error1Called = false;
+ var s = document.createElement('script');
+ s.type = 'text/javascript';
+ s.onload = function() { load1Called = true; };
+ s.onerror = function(event) { error1Called = true; event.stopPropagation(); };
+ s.src = 'about:cache-entry?client=image&sb=0&key=http://www.google.com';
+ document.body.appendChild(s);
+
+ var load2Called = false;
+ var error2Called = false;
+ var s2 = document.createElement('script');
+ s2.type = 'text/javascript';
+ s2.onload = function() { load2Called = true; };
+ s2.onerror = function(event) { error2Called = true; event.stopPropagation(); };
+ s2.src = 'data:text/plain, var x = 1;'
+ document.body.appendChild(s2);
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(function() {
+ is(load1Called, false, "Load handler should not be called");
+ is(error1Called, true, "Error handler should be called");
+ is(load2Called, true, "Load handler for valid script should be called");
+ is(error2Called, false,
+ "Error handler for valid script should not be called");
+ SimpleTest.finish();
+ });
+</script>
+</body>
+</html>
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug372098.html b/dom/html/test/test_bug372098.html
new file mode 100644
index 0000000000..900b20100d
--- /dev/null
+++ b/dom/html/test/test_bug372098.html
@@ -0,0 +1,68 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=372098
+-->
+<head>
+ <title>Test for Bug 372098</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <base target="bug372098"></base>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=372098">Mozilla Bug 372098</a>
+ <p id="display"></p>
+ <div id="content" style="display:none;">
+ <iframe name="bug372098"></iframe>
+ <a id="a" href="bug372098-link-target.html?a" target="">link</a>
+ <map>
+ <area id="area" shape="default" href="bug372098-link-target.html?area" target=""/>
+ </map>
+ </div>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+
+var a_passed = false;
+var area_passed = false;
+
+/* Start the test */
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(handle_load);
+
+function handle_load()
+{
+ sendMouseEvent({type:'click'}, 'a');
+}
+
+/* Finish the test */
+
+function finish_test()
+{
+ ok(a_passed, "The 'a' element used the correct target.");
+ ok(area_passed, "The 'area' element used the correct target.");
+ SimpleTest.finish();
+}
+
+/* Callback function used by the linked document */
+
+function callback(tag)
+{
+ switch (tag) {
+ case 'a':
+ a_passed = true;
+ sendMouseEvent({type:'click'}, 'area');
+ return;
+ case 'area':
+ area_passed = true;
+ finish_test();
+ return;
+ }
+ throw new Error("Eh??? We only test the 'a', 'link' and 'area' elements.");
+}
+
+ </script>
+ </pre>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug373589.html b/dom/html/test/test_bug373589.html
new file mode 100644
index 0000000000..f494370f3d
--- /dev/null
+++ b/dom/html/test/test_bug373589.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=373589
+-->
+<head>
+ <title>Test for Bug 373589</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=373589">Mozilla Bug 373589</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 373589 **/
+ var docElem = document.documentElement;
+ var body = document.body;
+ var numChildren = docElem.childNodes.length;
+ docElem.removeChild(body);
+ ok(numChildren > docElem.childNodes.length, "body was removed");
+ body.link;
+ ok(true, "didn't crash");
+ docElem.appendChild(body);
+ is(numChildren, docElem.childNodes.length, "body re-added");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug375003-1.html b/dom/html/test/test_bug375003-1.html
new file mode 100644
index 0000000000..2a5e6b911e
--- /dev/null
+++ b/dom/html/test/test_bug375003-1.html
@@ -0,0 +1,157 @@
+<!DOCTYPE HTML>
+<html id="html">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=375003
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Test 1 for bug 375003</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <style type="text/css">
+
+ html,body {
+ color:black; background-color:white; font-size:16px; padding:0; margin:0;
+ }
+
+ .s { display:block; width:20px; height:20px; background-color:lime; }
+ table { background:pink; }
+ #td5,#td6 { border:7px solid blue;}
+ </style>
+
+<script>
+var x = [ 'Left','Top','Width','Height' ];
+function test(id,s,expected) {
+ var el = document.getElementById(id);
+ for(var i = 0; i < x.length; ++i) {
+ // eslint-disable-next-line no-eval
+ var actual = eval('el.'+s+x[i]);
+ if (expected[i] != -1 && s+x[i]!='scrollHeight')
+ is(actual, expected[i], id+"."+s+x[i]);
+ }
+}
+function t3(id,c,o,s,pid) {
+ test(id,'client',c);
+ test(id,'offset',o);
+ test(id,'scroll',s);
+ var p = document.getElementById(id).offsetParent;
+ is(p.id, pid, id+".offsetParent");
+}
+
+function run_test() {
+ t3('span1',[0,0,20,20],[12,12,20,20],[0,0,20,20],'td1');
+ t3('td1' ,[1,1,69,44],[16,16,71,46],[0,0,69,46],'table1');
+ t3('tr1' ,[0,0,71,46],[16,16,71,46],[0,0,71,44],'table1');
+ t3('span2',[10,0,20,20],[27,12,30,20],[0,0,20,20],'td2');
+ t3('table1',[0,0,103,131],[10,10,103,131],[0,0,103,131],'body');
+ t3('div1',[10,10,-1,131],[0,0,-1,151],[0,0,-1,85],'body');
+
+ t3('span2b',[10,0,20,20],[25,-1,30,20],[0,0,20,20],'body');
+ // XXX not sure how to make reliable cross-platform tests for replaced-inline, inline
+ // t3('span2c',[10,2,18,2],[25,-1,30,6],[0,0,30,20],'body');
+ // t3('span2d',[0,0,0,0],[25,-1,10,19],[0,0,10,20],'body');
+
+ t3('span3' ,[0,0,20,20],[15,0,20,20],[0,0,20,20],'td3');
+ t3('td3' ,[0,0,35,20],[0,0,35,20],[0,0,35,20],'table3');
+ t3('tr3' ,[0,0,35,20],[0,0,35,20],[0,0,35,22],'table3');
+ t3('span4' ,[0,0,20,20],[0,0,20,20],[0,0,20,20],'td4');
+ t3('table3',[0,0,35,40],[0,0,35,40],[0,0,35,40],'div3');
+ t3('div3',[10,10,-1,40],[0,151,-1,60],[0,0,-1,70],'body');
+
+ t3('span5' ,[0,0,20,20],[1,1,20,20],[0,0,20,20],'td5');
+ t3('td5' ,[7,7,22,22],[2,2,36,36],[0,0,22,36],'table5');
+ t3('tr5' ,[0,0,36,36],[2,2,36,36],[0,0,36,22],'table5');
+ t3('span6' ,[0,0,20,20],[20,58,20,20],[0,0,20,20],'div5');
+ t3('table5',[0,0,40,78],[0,0,40,78],[0,0,40,78],'div5');
+ t3('div5',[10,10,-1,78],[0,211,-1,98],[0,0,-1,70],'body');
+
+ t3('span7' ,[0,0,20,20],[1,1,20,20],[0,0,20,20],'td7');
+ t3('td7' ,[1,1,37,22],[2,2,39,24],[0,0,37,22],'table7');
+ t3('tr7' ,[0,0,39,24],[2,2,39,24],[0,0,39,22],'table7');
+ t3('span8' ,[0,0,20,20],[19,30,20,20],[0,0,20,20],'table7');
+ t3('table7',[0,0,57,68],[10,319,57,68],[0,0,57,68],'body');
+ t3('div7',[10,10,-1,68],[0,309,-1,88],[0,0,-1,70],'body');
+
+ t3('span9' ,[0,0,20,20],[1,1,20,20],[0,0,20,20],'td9');
+ t3('td9' ,[1,1,22,22],[2,2,24,24],[0,0,22,24],'table9');
+ t3('tr9' ,[0,0,24,24],[2,2,24,24],[0,0,24,22],'table9');
+ t3('span10' ,[0,0,20,20],[17,43,20,20],[0,0,20,20],'table9');
+ t3('table9',[0,0,54,60],[10,407,54,60],[0,0,54,60],'body');
+ t3('div9',[10,10,-1,0],[0,397,-1,20],[0,0,-1,70],'body');
+
+ t3('span11' ,[0,0,20,20],[1,1,20,20],[0,0,20,20],'td11');
+ t3('td11' ,[0,0,22,22],[2,2,22,22],[0,0,22,22],'table11');
+ t3('tr11' ,[0,0,22,22],[2,2,22,22],[0,0,22,22],'table11');
+ t3('span12' ,[0,0,20,20],[28,454,20,20],[0,0,20,20],'body');
+ t3('table11',[0,0,26,30],[10,427,26,30],[0,0,26,30],'body');
+ t3('div11',[10,10,-1,30],[0,417,-1,50],[0,0,-1,70],'body');
+}
+</script>
+</head>
+<body id="body">
+
+<div id="content">
+<div id="div1" style="border:10px solid black">
+<table id="table1" cellspacing="7" cellpadding="12" border="9">
+ <tbody id="tbody1"><tr id="tr1"><td id="td1"><div class="s" id="span1"></div></td></tr></tbody>
+ <tbody id="tbody2"><tr id="tr2"><td id="td2"><div class="s" id="span2" style="margin-left:15px; border-left:10px solid blue;"></div></td></tr></tbody>
+</table>
+</div>
+
+<div id="div3" style="border:10px solid black; position:relative">
+<table id="table3" cellpadding="0" cellspacing="0" border="0">
+ <tbody id="tbody3"><tr id="tr3"><td id="td3"><div class="s" id="span3" style="margin-left:15px"></div></td></tr></tbody>
+ <tbody id="tbody4"><tr id="tr4"><td id="td4"><div class="s" id="span4"></div></td></tr></tbody>
+</table>
+</div>
+
+<div id="div5" style="border:10px solid black; position:relative">
+<table id="table5">
+ <tbody id="tbody5"><tr id="tr5"><td id="td5"><div class="s" id="span5"></div></td></tr></tbody>
+ <tbody id="tbody6"><tr id="tr6"><td id="td6"><div class="s" id="span6" style="left:10px; top:10px; position:relative"></div></td></tr></tbody>
+</table>
+</div>
+
+<div id="div7" style="border:10px solid black;">
+<table id="table7" style="position:relative" border=7>
+ <tbody id="tbody7"><tr id="tr7"><td id="td7"><div class="s" id="span7"></div></td></tr></tbody>
+ <tbody id="tbody8"><tr id="tr8"><td id="td8"><div class="s" id="span8" style="position:relative; margin-left:15px"></div></td></tr></tbody>
+</table>
+</div>
+
+<div id="div9" style="border:10px solid black;">
+<table id="table9" style="position:absolute" border="13">
+ <tbody id="tbody9"><tr id="tr9"><td id="td9"><div class="s" id="span9"></div></td></tr></tbody>
+ <tbody id="tbody10"><tr id="tr10"><td id="td10"><div class="s" id="span10" style="position:absolute"></div></td></tr></tbody>
+</table>
+</div>
+
+<div id="div11" style="border:10px solid black; ">
+<table id="table11">
+ <tbody id="tbody11"><tr id="tr11"><td id="td11"><div class="s" id="span11"></div></td></tr></tbody>
+ <tbody id="tbody12"><tr id="tr12"><td id="td12"><div class="s" id="span12" style="position:absolute;margin-left:15px"></div></td></tr></tbody>
+</table>
+</div>
+
+<div style="border:10px solid black">
+<div class="s" id="span2b" style="margin-left:15px; border-left:10px solid blue;"></div></div>
+
+<div style="border:10px solid black">
+<button id="span2c" style="margin-left:15px; border-left:10px solid blue;"></button></div>
+
+<div style="border:10px solid black">
+<span id="span2d" style="margin-left:15px; border-left:10px solid blue;"></span></div>
+</div>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=375003">Mozilla Bug 375003</a>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+run_test();
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug375003-2.html b/dom/html/test/test_bug375003-2.html
new file mode 100644
index 0000000000..b8b985846c
--- /dev/null
+++ b/dom/html/test/test_bug375003-2.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=375003
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Test 2 for bug 375003</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <style type="text/css">
+
+ html {
+ padding:0; margin:0;
+ }
+ body {
+ color:black; background-color:white; font-size:12px; padding:10px; margin:0;
+ }
+
+ #div1,#abs1,#table1 {
+ border: 20px solid lime;
+ padding: 30px;
+ width: 100px;
+ height: 60px;
+ overflow:scroll;
+ }
+ #abs1,#table2parent {
+ position:absolute;
+ left:500px;
+ }
+ #table3parent {
+ position:fixed;
+ left:300px;
+ top:100px;
+ }
+ .content {
+ display:block;
+ width:200px;
+ height:200px;
+ background:yellow;
+ border: 0px dotted black;
+ }
+</style>
+
+
+<script type="text/javascript">
+var x = [ 'Left','Top','Width','Height' ];
+function test(id,s,expected) {
+ var el = document.getElementById(id);
+ for(var i = 0; i < x.length; ++i) {
+ // eslint-disable-next-line no-eval
+ var actual = eval('el.'+s+x[i]);
+ if (expected[i] != -1 && s+x[i]!='scrollHeight')
+ is(actual, expected[i], id+"."+s+x[i]);
+ }
+}
+function t3(id,c,o,s,pid) {
+ test(id,'client',c);
+ test(id,'offset',o);
+ test(id,'scroll',s);
+ var p = document.getElementById(id).offsetParent;
+ is(p.id, pid, id+".offsetParent");
+}
+
+function run_test() {
+ // XXX how test clientWidth/clientHeight (the -1 below) in cross-platform manner
+ // without hard-coding the scrollbar width?
+ t3('div1',[20,20,-1,-1],[10,10,200,160],[0,0,230,20],'body');
+ t3('abs1',[20,20,-1,-1],[500,170,200,160],[0,0,230,20],'body');
+ t3('table1',[0,0,306,306],[10,170,306,306],[0,0,306,306],'body');
+ t3('table2',[0,0,206,206],[0,0,206,206],[0,0,206,20],'table2parent');
+ t3('table3',[0,0,228,228],[0,0,228,228],[0,0,228,228],'table3parent');
+ t3('table3parent',[0,0,228,228],[300,100,228,228],[0,0,228,228],'body');
+}
+</script>
+
+</head>
+<body id="body">
+<div id="content">
+<div id="div1parent">
+ <div id="div1"><span class="content">DIV</span></div>
+</div>
+
+<div id="abs1parent">
+ <div id="abs1"><span class="content">abs.pos.DIV</span></div>
+</div>
+
+<div id="table1parent">
+ <table id="table1"><tbody><tr><td id="td1"><span class="content">TABLE</span></td></tr></tbody></table>
+</div>
+
+<div id="table2parent">
+ <table id="table2"><tbody><tr><td id="td2"><span class="content">TABLE in abs</span></td></tr></tbody></table>
+</div>
+
+<div id="table3parent">
+ <table id="table3" border="10"><tbody><tr><td id="td3"><span class="content">TABLE in fixed</span></td></tr></tbody></table>
+</div>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+run_test();
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug377624.html b/dom/html/test/test_bug377624.html
new file mode 100644
index 0000000000..d385708c5e
--- /dev/null
+++ b/dom/html/test/test_bug377624.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=377624
+-->
+<head>
+ <title>Test for Bug 377624</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377624">Mozilla Bug 377624</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 377624 **/
+
+var input = document.createElement('input');
+ok("accept" in input, "'accept' is a valid input property");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug380383.html b/dom/html/test/test_bug380383.html
new file mode 100644
index 0000000000..4ec632c0d6
--- /dev/null
+++ b/dom/html/test/test_bug380383.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=380383
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+ <title>Test for Bug 380383</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=380383">Mozilla Bug 380383</a>
+<p id="display">
+ <iframe id="f1" name="f1"></iframe>
+ <iframe id="f2" name="f2"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ /** Test for Bug 380383 **/
+ is($("f1").contentDocument.characterSet, "UTF-8",
+ "Unexpected charset for f1");
+
+ function runTest() {
+ is($("f2").contentDocument.characterSet, "UTF-8",
+ "Unexpected charset for f2");
+ }
+
+ addLoadEvent(runTest);
+ addLoadEvent(SimpleTest.finish);
+ SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug383383.html b/dom/html/test/test_bug383383.html
new file mode 100644
index 0000000000..a518f426cf
--- /dev/null
+++ b/dom/html/test/test_bug383383.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=383383
+-->
+<head>
+ <title>Test for Bug 383383</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=383383">Mozilla Bug 383383</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript" for=" window " event=" onload() ">
+
+var foo = "bar";
+
+</script>
+
+<script class="testbody" type="text/javascript" for="object" event="handler">
+
+// This script should fail to run
+foo = "baz";
+
+isnot(foo, "baz", "test failed");
+
+</script>
+
+<script class="testbody" type="text/javascript">
+
+ok(foo == "bar", "test passed");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug383383_2.xhtml b/dom/html/test/test_bug383383_2.xhtml
new file mode 100644
index 0000000000..4dccd381c4
--- /dev/null
+++ b/dom/html/test/test_bug383383_2.xhtml
@@ -0,0 +1,20 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for bug 383383</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script>
+ SimpleTest.waitForExplicitFinish()
+ </script>
+ <script for="window" event="bar">
+ // This script should not run, but should not cause a parse error either.
+ ok(false, "Script was unexpectedly run")
+ </script>
+ <script>
+ ok(true, "Script was run as it should")
+ SimpleTest.finish()
+ </script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug384419.html b/dom/html/test/test_bug384419.html
new file mode 100644
index 0000000000..72ed15ef87
--- /dev/null
+++ b/dom/html/test/test_bug384419.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=384419
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+ <title>Test for bug 384419</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+ <style type="text/css">
+ html,body {
+ color:black; background-color:white; font-size:16px; padding:0; margin:0;
+ }
+ body { margin: 10px; }
+ table { border:15px solid black; margin-left:100px; }
+</style>
+
+
+<script type="text/javascript">
+function t3(id,expected,pid) {
+ var el = document.getElementById(id);
+ var actual = el.offsetLeft;
+ is(actual, expected, id+".offsetLeft");
+
+ var p = document.getElementById(id).offsetParent;
+ is(p.id, pid, id+".offsetParent");
+}
+
+function run_test() {
+ t3('rel384419',135,'body');
+ t3('abs384419',135,'body');
+ t3('fix384419',135,'body');
+}
+</script>
+
+</head>
+<body id="body">
+<!-- It's important for the test that the tables below are directly inside body -->
+<table cellpadding="7" cellspacing="3"><tr><td width="100"><div id="rel384419" style="position:relative;border:1px solid blue">X</div> relative</table>
+<table cellpadding="7" cellspacing="3"><tr><td width="100"><div id="abs384419" style="position:absolute;border:1px solid blue">X</div> absolute</table>
+<table cellpadding="7" cellspacing="3"><tr><td width="100"><div id="fix384419" style="position:fixed;border:1px solid blue">X</div> fixed</table>
+
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+run_test();
+</script>
+</pre>
+
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=384419">bug 384419</a>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug386496.html b/dom/html/test/test_bug386496.html
new file mode 100644
index 0000000000..47c48592e8
--- /dev/null
+++ b/dom/html/test/test_bug386496.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=386496
+-->
+<head>
+ <title>Test for Bug 386496</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <SCRIPT Type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=386496">Mozilla Bug 386496</a>
+<p id="display"></p>
+<div id="content">
+ <iframe style='display: block;' id="testIframe"
+ srcdoc="<div><a id='a' href='http://a.invalid/'>Link</a></div>">
+ </iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 386496 **/
+
+var frame = document.getElementById("testIframe");
+
+function testDesignMode() {
+ var unloadRequested = false;
+
+ frame.contentDocument.designMode = "on";
+
+ frame.contentWindow.addEventListener("beforeunload", function() {
+ unloadRequested = true;
+ });
+
+ synthesizeMouseAtCenter(frame.contentDocument.getElementById("a"), {},
+ frame.contentWindow);
+
+ // The click has been sent. If 'beforeunload' event has been caught when we go
+ // back from the event loop that means the link has been activated.
+ setTimeout(function() {
+ ok(!unloadRequested, "The link should not be activated in designMode");
+ SimpleTest.finish();
+ }, 0);
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(testDesignMode);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug386728.html b/dom/html/test/test_bug386728.html
new file mode 100644
index 0000000000..5562ce6712
--- /dev/null
+++ b/dom/html/test/test_bug386728.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=386728
+-->
+<head>
+ <title>Test for Bug 386728</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=386728">Mozilla Bug 386728</a>
+<p id="display"></p>
+<div id="content">
+ <div id="frameContent">
+ <div id="edit">This text is editable</div>
+ <button id="button_on" onclick="document.getElementById('edit').setAttribute('contenteditable', 'true')"></button>
+ </div>
+ <iframe id="testIframe"></iframe>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 386728 **/
+
+var frame = document.getElementById("testIframe");
+
+function testContentEditable() {
+ frame.style.display = 'block';
+ var frameContent = frame.contentDocument.adoptNode(document.getElementById("frameContent"));
+ frame.contentDocument.body.appendChild(frameContent);
+ frame.contentDocument.getElementById("edit").contentEditable = "true";
+ frame.contentDocument.getElementById("edit").contentEditable = "false";
+ frame.contentDocument.getElementById("button_on").click();
+ is(frame.contentDocument.getElementById("edit").contentEditable, "true");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(testContentEditable);
+addLoadEvent(SimpleTest.finish);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug386996.html b/dom/html/test/test_bug386996.html
new file mode 100644
index 0000000000..a3068c2c2e
--- /dev/null
+++ b/dom/html/test/test_bug386996.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=386996
+-->
+<head>
+ <title>Test for Bug 386996</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=386996">Mozilla Bug 386996</a>
+<p id="display"></p>
+<div id="content">
+ <input id="input1"><input disabled><input id="input2">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 386996 **/
+
+var frame = document.getElementById("testIframe");
+
+function testContentEditable() {
+ var focusedElement;
+ document.getElementById("input1").onfocus = function() { focusedElement = this };
+ document.getElementById("input2").onfocus = function() { focusedElement = this };
+
+ document.getElementById("input1").focus();
+ synthesizeKey("KEY_Tab");
+
+ is(focusedElement.id, "input2");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(testContentEditable);
+addLoadEvent(SimpleTest.finish);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug388558.html b/dom/html/test/test_bug388558.html
new file mode 100644
index 0000000000..a86bab8d1a
--- /dev/null
+++ b/dom/html/test/test_bug388558.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=388558
+-->
+<head>
+ <title>Test for Bug 388558</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=388558">Mozilla Bug 388558</a>
+<p id="display"></p>
+<div id="content">
+ <input type="text" id="input" onchange="++inputChange;">
+ <textarea id="textarea" onchange="++textareaChange;"></textarea>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 388558 **/
+var inputChange = 0;
+var textareaChange = 0;
+
+function testUserInput() {
+ var input = document.getElementById("input");
+ var textarea = SpecialPowers.wrap(document.getElementById("textarea"));
+
+ input.focus();
+ SpecialPowers.wrap(input).setUserInput("foo");
+ input.blur();
+ is(inputChange, 1, "Input element should have got one change event.");
+
+ input.focus();
+ input.value = "bar";
+ input.blur();
+ is(inputChange, 1,
+ "Change event dispatched when setting the value of the input element");
+
+ input.value = "";
+ is(inputChange, 1,
+ "Change event dispatched when setting the value of the input element (2).");
+
+ SpecialPowers.wrap(input).setUserInput("foo");
+ is(inputChange, 2,
+ "Change event dispatched when input element doesn't have focus.");
+
+ textarea.focus();
+ textarea.setUserInput("foo");
+ textarea.blur();
+ is(textareaChange, 1, "Textarea element should have got one change event.");
+
+ textarea.focus();
+ textarea.value = "bar";
+ textarea.blur();
+ is(textareaChange, 1,
+ "Change event dispatched when setting the value of the textarea element.");
+
+ textarea.value = "";
+ is(textareaChange, 1,
+ "Change event dispatched when setting the value of the textarea element (2).");
+
+ textarea.setUserInput("foo");
+ is(textareaChange, 1,
+ "Change event dispatched when textarea element doesn't have focus.");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(testUserInput);
+addLoadEvent(SimpleTest.finish);
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug388746.html b/dom/html/test/test_bug388746.html
new file mode 100644
index 0000000000..38c73ee0b5
--- /dev/null
+++ b/dom/html/test/test_bug388746.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=388746
+-->
+<head>
+ <title>Test for Bug 388746</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=388746">Mozilla Bug 388746</a>
+<p id="display"></p>
+<div id="content">
+ <input>
+ <textarea></textarea>
+ <select>
+ <option>option1</option>
+ <optgroup label="optgroup">
+ <option>option2</option>
+ </optgroup>
+ </select>
+ <button>Button</button>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 388746 **/
+
+var previousEventTarget = "";
+
+function handler(evt) {
+ if (evt.eventPhase == 2) {
+ previousEventTarget = evt.target.localName.toLowerCase();
+ }
+}
+
+function testElementType(type) {
+ var el = document.getElementsByTagName(type)[0];
+ el.addEventListener("DOMAttrModified", handler, true);
+ el.setAttribute("foo", "bar");
+ ok(previousEventTarget == type,
+ type + " element should have got DOMAttrModified event.");
+}
+
+function test() {
+ testElementType("input");
+ testElementType("textarea");
+ testElementType("select");
+ testElementType("option");
+ testElementType("optgroup");
+ testElementType("button");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(test);
+addLoadEvent(SimpleTest.finish);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug388794.html b/dom/html/test/test_bug388794.html
new file mode 100644
index 0000000000..06388547d7
--- /dev/null
+++ b/dom/html/test/test_bug388794.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=388794
+-->
+<head>
+ <title>Test for Bug 388794</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input { padding: 0; margin: 0; border: none; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=388794">Mozilla Bug 388794</a>
+<p id="display">
+ <form action="dummy_page.html" target="test1" method="GET">
+ <input id="test1image" type="image" name="testImage">
+ </form>
+ <form action="dummy_page.html" target="test2" method="GET">
+ <input id="test2image" type="image">
+ </form>
+ <form action="dummy_page.html" target="test3" method="GET">
+ <input id="test3image" type="image" src="nnc_lockup.gif" name="testImage">
+ </form>
+ <form action="dummy_page.html" target="test4" method="GET">
+ <input id="test4image" type="image" src="nnc_lockup.gif">
+ </form>
+ <form action="dummy_page.html" target="test5" method="GET">
+ <input id="test5image" type="image" src="nnc_lockup.gif" name="testImage">
+ </form>
+ <form action="dummy_page.html" target="test6" method="GET">
+ <input id="test6image" type="image" src="nnc_lockup.gif">
+ </form>
+ <iframe name="test1" id="test1"></iframe>
+ <iframe name="test2" id="test2"></iframe>
+ <iframe name="test3" id="test3"></iframe>
+ <iframe name="test4" id="test4"></iframe>
+ <iframe name="test5" id="test5"></iframe>
+ <iframe name="test6" id="test6"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 388794 **/
+SimpleTest.waitForExplicitFinish();
+
+var pendingLoads = 0;
+/* Use regex due to rounding error in Fennec with C++APZ enabled */
+var hrefs = {
+ test1: /\?testImage\.x=0&testImage\.y=0/,
+ test2: /\?x=0&y=0/,
+ test3: /\?testImage\.x=0&testImage\.y=0/,
+ test4: /\?x=0&y=0/,
+ test5: /\?testImage\.x=[4-6]&testImage\.y=[4-6]/,
+ test6: /\?x=[4-6]&y=[4-6]/,
+};
+
+function submitForm(idNum) {
+ $("test"+idNum).setAttribute("onload", "frameLoaded(this)");
+ $("test" + idNum + "image").focus();
+ sendKey("return");
+}
+
+function submitFormMouse(idNum) {
+ $("test"+idNum).setAttribute("onload", "frameLoaded(this)");
+ // Use 4.99 instead of 5 to guard against the possibility that the
+ // image's 'top' is exactly N + 0.5 pixels from the root. In that case
+ // we'd round up the widget mouse coordinate to N + 6, which relative
+ // to the image would be 5.5, which would get rounded up to 6 when
+ // submitting the form. Instead we round the widget mouse coordinate to
+ // N + 5, which relative to the image would be 4.5 which gets rounded up
+ // to 5.
+ synthesizeMouse($("test" + idNum + "image"), 4.99, 4.99, {});
+}
+
+addLoadEvent(function() {
+ // Need the timeout so painting has a chance to be unsuppressed.
+ setTimeout(function() {
+ submitForm(++pendingLoads);
+ submitForm(++pendingLoads);
+ submitForm(++pendingLoads);
+ submitForm(++pendingLoads);
+ submitFormMouse(++pendingLoads);
+ submitFormMouse(++pendingLoads);
+ }, 0);
+});
+
+function frameLoaded(frame) {
+ ok(
+ hrefs[frame.name].test(frame.contentWindow.location.href),
+ "Unexpected href for frame " + frame.name + " - " +
+ "expected to match: " + hrefs[frame.name].toString() + " got: " + frame.contentWindow.location.href
+ );
+ if (--pendingLoads == 0) {
+ SimpleTest.finish();
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug389797.html b/dom/html/test/test_bug389797.html
new file mode 100644
index 0000000000..701d6e65c9
--- /dev/null
+++ b/dom/html/test/test_bug389797.html
@@ -0,0 +1,243 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=389797
+-->
+<head>
+ <title>Test for Bug 389797</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=389797">Mozilla Bug 389797</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 389797 **/
+var allTags = [];
+var classInfos = {};
+var interfaces = {};
+var interfacesNonClassinfo = {};
+
+function getClassName(tag) {
+ return "HTML" + classInfos[tag] + "Element";
+}
+
+function HTML_TAG(aTagName, aImplClass) {
+ allTags.push(aTagName);
+ classInfos[aTagName] = aImplClass;
+ interfaces[aTagName] = [];
+
+ // Some interfaces don't appear in classinfo because other interfaces that
+ // inherit from them do.
+ interfacesNonClassinfo[aTagName] = [ ];
+
+ if (arguments.length > 2) {
+ for (var i = 0; i < arguments[2].length; ++i) {
+ interfaces[aTagName].push(arguments[2][i]);
+ }
+ }
+
+ if (arguments.length > 3) {
+ for (i = 0; i < arguments[3].length; ++i) {
+ interfacesNonClassinfo[aTagName].push(arguments[3][i]);
+ }
+ }
+}
+
+const objectIfaces = [
+ "nsIRequestObserver",
+ "nsIStreamListener",
+ "nsIObjectLoadingContent",
+ "nsIChannelEventSink",
+];
+
+/* List copy/pasted from nsHTMLTagList.h, with the second field modified to the
+ correct classinfo (instead of the impl class) in the following cases:
+
+ base
+ blockquote
+ dir
+ dl
+ embed
+ menu
+ ol
+ param
+ q
+ ul
+ wbr
+ head
+ html
+ */
+
+HTML_TAG("a", "Anchor");
+HTML_TAG("abbr", "");
+HTML_TAG("acronym", "");
+HTML_TAG("address", "");
+HTML_TAG("area", "Area");
+HTML_TAG("article", "");
+HTML_TAG("aside", "");
+HTML_TAG("b", "");
+HTML_TAG("base", "Base");
+HTML_TAG("bdi", "")
+HTML_TAG("bdo", "");
+HTML_TAG("bgsound", "Unknown");
+HTML_TAG("big", "");
+HTML_TAG("blockquote", "Quote");
+HTML_TAG("body", "Body");
+HTML_TAG("br", "BR");
+HTML_TAG("button", "Button");
+HTML_TAG("canvas", "Canvas");
+HTML_TAG("caption", "TableCaption");
+HTML_TAG("center", "");
+HTML_TAG("cite", "");
+HTML_TAG("code", "");
+HTML_TAG("col", "TableCol");
+HTML_TAG("colgroup", "TableCol");
+HTML_TAG("data", "Data");
+HTML_TAG("datalist", "DataList");
+HTML_TAG("dd", "");
+HTML_TAG("del", "Mod");
+HTML_TAG("dfn", "");
+HTML_TAG("dir", "Directory");
+HTML_TAG("div", "Div");
+HTML_TAG("dl", "DList");
+HTML_TAG("dt", "");
+HTML_TAG("em", "");
+HTML_TAG("embed", "Embed", [], objectIfaces);
+HTML_TAG("fieldset", "FieldSet");
+HTML_TAG("figcaption", "")
+HTML_TAG("figure", "")
+HTML_TAG("font", "Font");
+HTML_TAG("footer", "")
+HTML_TAG("form", "Form");
+HTML_TAG("frame", "Frame", [ "nsIDOMMozBrowserFrame" ]);
+HTML_TAG("frameset", "FrameSet");
+HTML_TAG("h1", "Heading");
+HTML_TAG("h2", "Heading");
+HTML_TAG("h3", "Heading");
+HTML_TAG("h4", "Heading");
+HTML_TAG("h5", "Heading");
+HTML_TAG("h6", "Heading");
+HTML_TAG("head", "Head");
+HTML_TAG("header", "")
+HTML_TAG("hgroup", "")
+HTML_TAG("hr", "HR");
+HTML_TAG("html", "Html");
+HTML_TAG("i", "");
+HTML_TAG("iframe", "IFrame", [ "nsIDOMMozBrowserFrame" ]);
+HTML_TAG("image", "");
+HTML_TAG("img", "Image", [ "nsIImageLoadingContent" ], []);
+HTML_TAG("input", "Input", [], [ "imgINotificationObserver",
+ "nsIImageLoadingContent" ]);
+HTML_TAG("ins", "Mod");
+HTML_TAG("kbd", "");
+HTML_TAG("keygen", "Unknown");
+HTML_TAG("label", "Label");
+HTML_TAG("legend", "Legend");
+HTML_TAG("li", "LI");
+HTML_TAG("link", "Link");
+HTML_TAG("listing", "Pre");
+HTML_TAG("main", "");
+HTML_TAG("map", "Map");
+HTML_TAG("mark", "");
+HTML_TAG("marquee", "Marquee");
+HTML_TAG("menu", "Menu");
+HTML_TAG("meta", "Meta");
+HTML_TAG("meter", "Meter");
+HTML_TAG("multicol", "Unknown");
+HTML_TAG("nav", "")
+HTML_TAG("nobr", "");
+HTML_TAG("noembed", "");
+HTML_TAG("noframes", "");
+HTML_TAG("noscript", "");
+HTML_TAG("object", "Object", [], objectIfaces);
+HTML_TAG("ol", "OList");
+HTML_TAG("optgroup", "OptGroup");
+HTML_TAG("option", "Option");
+HTML_TAG("p", "Paragraph");
+HTML_TAG("param", "Param");
+HTML_TAG("plaintext", "");
+HTML_TAG("pre", "Pre");
+HTML_TAG("q", "Quote");
+HTML_TAG("rb", "");
+HTML_TAG("rp", "");
+HTML_TAG("rt", "");
+HTML_TAG("rtc", "");
+HTML_TAG("ruby", "");
+HTML_TAG("s", "");
+HTML_TAG("samp", "");
+HTML_TAG("script", "Script", [ "nsIScriptLoaderObserver" ], []);
+HTML_TAG("section", "")
+HTML_TAG("select", "Select");
+HTML_TAG("small", "");
+HTML_TAG("span", "Span");
+HTML_TAG("strike", "");
+HTML_TAG("strong", "");
+HTML_TAG("style", "Style");
+HTML_TAG("sub", "");
+HTML_TAG("sup", "");
+HTML_TAG("table", "Table");
+HTML_TAG("tbody", "TableSection");
+HTML_TAG("td", "TableCell");
+HTML_TAG("textarea", "TextArea");
+HTML_TAG("tfoot", "TableSection");
+HTML_TAG("th", "TableCell");
+HTML_TAG("thead", "TableSection");
+HTML_TAG("template", "Template");
+HTML_TAG("time", "Time");
+HTML_TAG("title", "Title");
+HTML_TAG("tr", "TableRow");
+HTML_TAG("tt", "");
+HTML_TAG("u", "");
+HTML_TAG("ul", "UList");
+HTML_TAG("var", "");
+HTML_TAG("wbr", "");
+HTML_TAG("xmp", "Pre");
+
+function tagName(aTag) {
+ return "<" + aTag + ">";
+}
+
+for (var tag of allTags) {
+ var node = document.createElement(tag);
+
+ // Have to use the proto's toString(), since HTMLAnchorElement and company
+ // override toString().
+ var nodeString = HTMLElement.prototype.toString.apply(node);
+
+ // Debug builds have extra info, so chop off after "Element" if it's followed
+ // by ' ' or ']'
+ nodeString = nodeString.replace(/Element[\] ].*/, "Element");
+
+ var classInfoString = getClassName(tag);
+ is(nodeString, "[object " + classInfoString,
+ "Unexpected classname for " + tagName(tag));
+ is(node instanceof window[classInfoString], true,
+ tagName(tag) + " not an instance of " + classInfos[tag]);
+
+ if (classInfoString != 'HTMLUnknownElement') {
+ is(node instanceof HTMLUnknownElement, false,
+ tagName(tag) + " is an instance of HTMLUnknownElement");
+ } else {
+ is(node instanceof HTMLUnknownElement, true,
+ tagName(tag) + " is an instance of HTMLUnknownElement");
+ }
+
+ // Check that each node QIs to all the things we expect it to QI to
+ for (var iface of interfaces[tag].concat(interfacesNonClassinfo[tag])) {
+ is(iface in SpecialPowers.Ci, true,
+ iface + " not in Components.interfaces");
+ is(node instanceof SpecialPowers.Ci[iface], true,
+ tagName(tag) + " does not QI to " + iface);
+ }
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug390975.html b/dom/html/test/test_bug390975.html
new file mode 100644
index 0000000000..8a7e09b807
--- /dev/null
+++ b/dom/html/test/test_bug390975.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=390975
+-->
+<head>
+ <title>Test for Bug 390975</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=390975">Mozilla Bug 390975</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <table id="table1">
+ <form id="form1">
+ <input>
+ <input>
+ <tr><td>
+ <input>
+ <input>
+ <input>
+ </td></tr>
+ </form>
+ </table>
+
+ <table id="table2">
+ <form id="form2">
+ <input>
+ <input>
+ <tr id="row2"><td>
+ <input>
+ <input>
+ <input>
+ </td></tr>
+ </form>
+ </table>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 390975 **/
+var form = $("form1");
+is(form.elements.length, 5, "Unexpected elements length");
+
+$("table1").remove();
+is(form.elements.length, 3, "Should have lost control outside table");
+
+form.remove();
+is(form.elements.length, 0, "Should have lost control outside form");
+
+form = $("form2");
+is(form.elements.length, 5, "Unexpected elements length");
+
+$("row2").remove();
+is(form.elements.length, 2, "Should have lost controls inside table row");
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug391994.html b/dom/html/test/test_bug391994.html
new file mode 100644
index 0000000000..8dfa6cc772
--- /dev/null
+++ b/dom/html/test/test_bug391994.html
@@ -0,0 +1,184 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=391994
+-->
+<head>
+ <title>Test for Bug 391994</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=391994">Mozilla Bug 391994</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 391994 **/
+var testNumber = 0;
+
+function assertSelected(aOption, aExpectDefaultSelected, aExpectSelected) {
+ ++testNumber;
+ is(aOption.defaultSelected, aExpectDefaultSelected,
+ "Asserting default-selected state for option " + testNumber);
+ is(aOption.selected, aExpectSelected,
+ "Asserting selected state for option " + testNumber);
+}
+
+function assertSame(aSel1, aSel2Str, aTestNumber) {
+ var div = document.createElement("div");
+ div.innerHTML = aSel2Str;
+ sel2 = div.firstChild;
+ is(aSel1.options.length, sel2.options.length,
+ "Length should be same in select test " + aTestNumber);
+ is(aSel1.selectedIndex, sel2.selectedIndex,
+ "Selected index should be same in select test " + aTestNumber);
+ for (var i = 0; i < aSel1.options.length; ++i) {
+ is(aSel1.options[i].selected, sel2.options[i].selected,
+ "Options[" + i + "].selected should be the same in select test " +
+ aTestNumber);
+ is(aSel1.options[i].defaultSelected, sel2.options[i].defaultSelected,
+ "Options[" + i +
+ "].defaultSelected should be the same in select test " +
+ aTestNumber);
+ }
+}
+
+// Creation methods
+var opt = document.createElement("option");
+assertSelected(opt, false, false);
+
+opt = new Option();
+assertSelected(opt, false, false);
+
+// Setting of defaultSelected
+opt = new Option();
+opt.setAttribute("selected", "selected");
+assertSelected(opt, true, true);
+
+opt = new Option();
+opt.defaultSelected = true;
+assertSelected(opt, true, true);
+is(opt.hasAttribute("selected"), true, "Attribute should be set");
+is(opt.getAttribute("selected"), "",
+ "Attribute should be set to empty string");
+
+// Setting of selected
+opt = new Option();
+opt.selected = false;
+assertSelected(opt, false, false);
+
+opt = new Option();
+opt.selected = true;
+assertSelected(opt, false, true);
+
+// Interaction of selected and defaultSelected
+opt = new Option();
+opt.selected;
+opt.setAttribute("selected", "selected");
+assertSelected(opt, true, true);
+
+opt = new Option();
+opt.selected = false;
+opt.setAttribute("selected", "selected");
+assertSelected(opt, true, false);
+
+opt = new Option();
+opt.setAttribute("selected", "selected");
+opt.selected = true;
+opt.removeAttribute("selected");
+assertSelected(opt, false, true);
+
+// First test of putting things in a <select>: Adding default-selected option
+// should select it.
+var sel = document.createElement("select");
+sel.appendChild(new Option());
+is(sel.selectedIndex, 0, "First option should be selected");
+assertSelected(sel.firstChild, false, true);
+
+sel.appendChild(new Option());
+is(sel.selectedIndex, 0, "First option should still be selected");
+assertSelected(sel.firstChild, false, true);
+assertSelected(sel.firstChild.nextSibling, false, false);
+
+opt = new Option();
+opt.defaultSelected = true;
+sel.appendChild(opt);
+assertSelected(sel.firstChild, false, false);
+assertSelected(sel.firstChild.nextSibling, false, false);
+assertSelected(opt, true, true);
+is(opt, sel.firstChild.nextSibling.nextSibling, "What happened here?");
+is(sel.options[0], sel.firstChild, "Unexpected option 0");
+is(sel.options[1], sel.firstChild.nextSibling, "Unexpected option 1");
+is(sel.options[2], opt, "Unexpected option 2");
+is(sel.selectedIndex, 2, "Unexpected selectedIndex in select test 1");
+
+assertSame(sel, "<select><option><option><option selected></select>", 1);
+
+// Second test of putting things in a <select>: Adding two default-selected
+// options should select the second one.
+sel = document.createElement("select");
+sel.appendChild(new Option());
+sel.appendChild(new Option());
+opt = new Option();
+opt.defaultSelected = true;
+sel.appendChild(opt);
+opt = new Option();
+opt.defaultSelected = true;
+sel.appendChild(opt);
+assertSelected(sel.options[0], false, false);
+assertSelected(sel.options[1], false, false);
+assertSelected(sel.options[2], true, false);
+assertSelected(sel.options[3], true, true);
+is(sel.selectedIndex, 3, "Unexpected selectedIndex in select test 2");
+
+assertSame(sel,
+ "<select><option><option><option selected><option selected></select>", 2);
+
+// Third test of putting things in a <select>: adding a selected option earlier
+// than another selected option should make the new option selected.
+sel = document.createElement("select");
+sel.appendChild(new Option());
+sel.appendChild(new Option());
+opt = new Option();
+opt.defaultSelected = true;
+sel.appendChild(opt);
+opt = new Option();
+opt.defaultSelected = true;
+sel.options[0] = opt;
+assertSelected(sel.options[0], true, true);
+assertSelected(sel.options[1], false, false);
+assertSelected(sel.options[2], true, false);
+is(sel.selectedIndex, 0, "Unexpected selectedIndex in select test 3");
+
+// Fourth test of putting things in a <select>: Just like second test, but with
+// a <select multiple>
+sel = document.createElement("select");
+sel.multiple = true;
+sel.appendChild(new Option());
+sel.appendChild(new Option());
+opt = new Option();
+opt.defaultSelected = true;
+sel.appendChild(opt);
+opt = new Option();
+opt.defaultSelected = true;
+sel.appendChild(opt);
+assertSelected(sel.options[0], false, false);
+assertSelected(sel.options[1], false, false);
+assertSelected(sel.options[2], true, true);
+assertSelected(sel.options[3], true, true);
+is(sel.selectedIndex, 2, "Unexpected selectedIndex in select test 4");
+
+assertSame(sel,
+ "<select multiple><option><option>" +
+ "<option selected><option selected></select>",
+ 4);
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug394700.html b/dom/html/test/test_bug394700.html
new file mode 100644
index 0000000000..fb6a54421b
--- /dev/null
+++ b/dom/html/test/test_bug394700.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=394700
+-->
+<head>
+ <title>Test for Bug 394700</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=394700">Mozilla Bug 394700</a>
+<p id="display"></p>
+<div id="content">
+ <select><option id="A">A</option><option id="B">B</option></select>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 394700 **/
+
+function remove(q1) { q1.remove(); }
+
+function testSelectedIndex()
+{
+ document.addEventListener("DOMNodeRemoved", foo);
+ remove(document.getElementById("B"));
+ document.removeEventListener("DOMNodeRemoved", foo);
+
+ function foo()
+ {
+ document.removeEventListener("DOMNodeRemoved", foo);
+ remove(document.getElementById("A"));
+ }
+ var selectElement = document.getElementsByTagName("select")[0];
+ is(selectElement.selectedIndex, -1, "Wrong selected index!");
+ is(selectElement.length, 0, "Select shouldn't have any options!");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(testSelectedIndex);
+addLoadEvent(SimpleTest.finish);
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug395107.html b/dom/html/test/test_bug395107.html
new file mode 100644
index 0000000000..8273cd6c6e
--- /dev/null
+++ b/dom/html/test/test_bug395107.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=395107
+-->
+<head>
+ <title>Test for Bug 395107</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=395107">Mozilla Bug 395107</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 395107 **/
+var testNumber = 0;
+
+function assertSelected(aOption, aExpectDefaultSelected, aExpectSelected) {
+ ++testNumber;
+ is(aOption.defaultSelected, aExpectDefaultSelected,
+ "Asserting default-selected state for option " + testNumber);
+ is(aOption.selected, aExpectSelected,
+ "Asserting selected state for option " + testNumber);
+}
+
+function assertSame(aSel1, aSel2Str, aTestNumber) {
+ var div = document.createElement("div");
+ div.innerHTML = aSel2Str;
+ sel2 = div.firstChild;
+ is(aSel1.options.length, sel2.options.length,
+ "Length should be same in select test " + aTestNumber);
+ is(aSel1.selectedIndex, sel2.selectedIndex,
+ "Selected index should be same in select test " + aTestNumber);
+ for (var i = 0; i < aSel1.options.length; ++i) {
+ is(aSel1.options[i].selected, sel2.options[i].selected,
+ "Options[" + i + "].selected should be the same in select test " +
+ aTestNumber);
+ is(aSel1.options[i].defaultSelected, sel2.options[i].defaultSelected,
+ "Options[" + i +
+ "].defaultSelected should be the same in select test " +
+ aTestNumber);
+ }
+}
+
+// In a single-select, setting an option selected should deselect an
+// existing selected option.
+var sel = document.createElement("select");
+sel.appendChild(new Option());
+is(sel.selectedIndex, 0, "First option should be selected");
+assertSelected(sel.firstChild, false, true);
+sel.appendChild(new Option());
+is(sel.selectedIndex, 0, "First option should still be selected");
+assertSelected(sel.firstChild, false, true);
+assertSelected(sel.firstChild.nextSibling, false, false);
+
+opt = new Option();
+sel.appendChild(opt);
+opt.defaultSelected = true;
+assertSelected(sel.firstChild, false, false);
+assertSelected(sel.firstChild.nextSibling, false, false);
+assertSelected(opt, true, true);
+is(opt, sel.firstChild.nextSibling.nextSibling, "What happened here?");
+is(sel.options[0], sel.firstChild, "Unexpected option 0");
+is(sel.options[1], sel.firstChild.nextSibling, "Unexpected option 1");
+is(sel.options[2], opt, "Unexpected option 2");
+is(sel.selectedIndex, 2, "Unexpected selectedIndex in select test 1");
+
+assertSame(sel, "<select><option><option><option selected></select>", 1);
+
+// Same, but with the option that gets set selected earlier in the select
+sel = document.createElement("select");
+sel.appendChild(new Option());
+sel.appendChild(new Option());
+opt = new Option();
+opt.defaultSelected = true;
+sel.appendChild(opt);
+opt = new Option();
+sel.options[0] = opt;
+opt.defaultSelected = true;
+assertSelected(sel.options[0], true, true);
+assertSelected(sel.options[1], false, false);
+assertSelected(sel.options[2], true, false);
+is(sel.selectedIndex, 0, "Unexpected selectedIndex in select test 2");
+
+// And now try unselecting options
+sel = document.createElement("select");
+sel.appendChild(new Option());
+opt = new Option();
+opt.defaultSelected = true;
+sel.appendChild(opt);
+sel.appendChild(new Option());
+opt.defaultSelected = false;
+
+assertSelected(sel.options[0], false, true);
+assertSelected(sel.options[1], false, false);
+assertSelected(sel.options[2], false, false);
+is(sel.selectedIndex, 0, "Unexpected selectedIndex in select test 2");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug401160.xhtml b/dom/html/test/test_bug401160.xhtml
new file mode 100644
index 0000000000..bb9fe47111
--- /dev/null
+++ b/dom/html/test/test_bug401160.xhtml
@@ -0,0 +1,27 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=401160
+-->
+<head>
+ <title>Test for Bug 401160</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=401160">Mozilla Bug 401160</a>
+<label id="label" contenteditable="true"><legend></legend><div></div></label>
+
+<pre id="test">
+<script type="text/javascript">
+
+function do_test() {
+ document.getElementById('label').focus();
+ ok(true, "This is crash test - the test succeeded if we reach this line")
+}
+
+do_test();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug402680.html b/dom/html/test/test_bug402680.html
new file mode 100644
index 0000000000..517942772e
--- /dev/null
+++ b/dom/html/test/test_bug402680.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=402680
+-->
+<head>
+ <title>Test for Bug 402680</title>
+ <script>
+ var activeElementIsNull = (document.activeElement == null);
+ </script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=402680">Mozilla Bug 402680</a>
+<p id="display"></p>
+<div id="content">
+ <input type="text">
+ <textarea></textarea>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 402680 **/
+
+ok(activeElementIsNull,
+ "Before document has body, active element should be null");
+
+function testActiveElement() {
+ ok(document.body == document.activeElement,
+ "After page load body element should be the active element!");
+ var input = document.getElementsByTagName("input")[0];
+ input.focus();
+ ok(document.activeElement == input,
+ "Input element isn't the active element!");
+ var textarea = document.getElementsByTagName("textarea")[0];
+ textarea.focus();
+ ok(document.activeElement == textarea,
+ "Textarea element isn't the active element!");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(testActiveElement);
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug403868.html b/dom/html/test/test_bug403868.html
new file mode 100644
index 0000000000..43118a0683
--- /dev/null
+++ b/dom/html/test/test_bug403868.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=403868
+-->
+<head>
+ <title>Test for Bug 403868</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=403868">Mozilla Bug 403868</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 403868 **/
+function createSpan(id, insertionPoint) {
+ var s = document.createElement("span");
+ s.id = id;
+ $("content").insertBefore(s, insertionPoint);
+ return s;
+}
+
+var s1a = createSpan("test1", null);
+is(document.getElementById("test1"), s1a,
+ "Only one span with id=test1 in the tree; should work!");
+
+var s2a = createSpan("test1", null);
+is(document.getElementById("test1"), s1a,
+ "Appending span with id=test1 doesn't change which one comes first");
+
+var s3a = createSpan("test1", s2a);
+is(document.getElementById("test1"), s1a,
+ "Inserting span with id=test1 not at the beginning; doesn't matter");
+
+var s4a = createSpan("test1", s1a);
+is(document.getElementById("test1"), s4a,
+ "Inserting span with id=test1 at the beginning changes which one is first");
+
+s4a.remove();
+is(document.getElementById("test1"), s1a,
+ "First-created span with id=test1 is first again");
+
+s1a.remove();
+is(document.getElementById("test1"), s3a,
+ "Third-created span with id=test1 is first now");
+
+// Start the id hashtable
+for (var i = 0; i < 256; ++i) {
+ document.getElementById("no-such-id-in-the-document" + i);
+}
+
+var s1b = createSpan("test2", null);
+is(document.getElementById("test2"), s1b,
+ "Only one span with id=test2 in the tree; should work!");
+
+var s2b = createSpan("test2", null);
+is(document.getElementById("test2"), s1b,
+ "Appending span with id=test2 doesn't change which one comes first");
+
+var s3b = createSpan("test2", s2b);
+is(document.getElementById("test2"), s1b,
+ "Inserting span with id=test2 not at the beginning; doesn't matter");
+
+var s4b = createSpan("test2", s1b);
+is(document.getElementById("test2"), s4b,
+ "Inserting span with id=test2 at the beginning changes which one is first");
+
+s4b.remove();
+is(document.getElementById("test2"), s1b,
+ "First-created span with id=test2 is first again");
+
+s1b.remove();
+is(document.getElementById("test2"), s3b,
+ "Third-created span with id=test2 is first now");
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug403868.xhtml b/dom/html/test/test_bug403868.xhtml
new file mode 100644
index 0000000000..53c2a24d57
--- /dev/null
+++ b/dom/html/test/test_bug403868.xhtml
@@ -0,0 +1,86 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=403868
+-->
+<head>
+ <title>Test for Bug 403868</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=403868">Mozilla Bug 403868</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+/** Test for Bug 403868 **/
+function createSpan(id, insertionPoint) {
+ var s = document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ s.id = id;
+ $("content").insertBefore(s, insertionPoint);
+ return s;
+}
+
+var s1a = createSpan("test1", null);
+is(document.getElementById("test1"), s1a,
+ "Only one span with id=test1 in the tree; should work!");
+
+var s2a = createSpan("test1", null);
+is(document.getElementById("test1"), s1a,
+ "Appending span with id=test1 doesn't change which one comes first");
+
+var s3a = createSpan("test1", s2a);
+is(document.getElementById("test1"), s1a,
+ "Inserting span with id=test1 not at the beginning; doesn't matter");
+
+var s4a = createSpan("test1", s1a);
+is(document.getElementById("test1"), s4a,
+ "Inserting span with id=test1 at the beginning changes which one is first");
+
+s4a.remove();
+is(document.getElementById("test1"), s1a,
+ "First-created span with id=test1 is first again");
+
+s1a.remove();
+is(document.getElementById("test1"), s3a,
+ "Third-created span with id=test1 is first now");
+
+// Start the id hashtable
+for (var i = 0; i < 256; ++i) {
+ document.getElementById("no-such-id-in-the-document" + i);
+}
+
+var s1b = createSpan("test2", null);
+is(document.getElementById("test2"), s1b,
+ "Only one span with id=test2 in the tree; should work!");
+
+var s2b = createSpan("test2", null);
+is(document.getElementById("test2"), s1b,
+ "Appending span with id=test2 doesn't change which one comes first");
+
+var s3b = createSpan("test2", s2b);
+is(document.getElementById("test2"), s1b,
+ "Inserting span with id=test2 not at the beginning; doesn't matter");
+
+var s4b = createSpan("test2", s1b);
+is(document.getElementById("test2"), s4b,
+ "Inserting span with id=test2 at the beginning changes which one is first");
+
+s4b.remove();
+is(document.getElementById("test2"), s1b,
+ "First-created span with id=test2 is first again");
+
+s1b.remove();
+is(document.getElementById("test2"), s3b,
+ "Third-created span with id=test2 is first now");
+
+]]>
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug405242.html b/dom/html/test/test_bug405242.html
new file mode 100644
index 0000000000..b8999dc9f6
--- /dev/null
+++ b/dom/html/test/test_bug405242.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=405242
+-->
+<head>
+ <title>Test for Bug 405252</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=405242">Mozilla Bug 405242</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Bug 405242 **/
+var sel = document.createElement("select");
+sel.appendChild(new Option());
+sel.appendChild(new Option());
+sel.appendChild(new Option());
+opt = new Option();
+opt.value = 10;
+sel.appendChild(opt);
+sel.options.remove(0);
+sel.options.remove(1000);
+sel.options.remove(-1);
+is(sel.length, 3, "Unexpected option collection length");
+is(sel[2].value, "10", "Unexpected remained option");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug406596.html b/dom/html/test/test_bug406596.html
new file mode 100644
index 0000000000..6886d078be
--- /dev/null
+++ b/dom/html/test/test_bug406596.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=406596
+-->
+<head>
+ <title>Test for Bug 406596</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=406596">Mozilla Bug 406596</a>
+<div id="content">
+ <div id="edit" contenteditable="true">This text is editable, you can change its content <a href="#" id="a" tabindex="0">ABCDEFGHIJKLMNOPQRSTUV</a> <input type="submit" value="abcd" id="b"></input> <img src="foo.png" id="c"></div>
+ <div tabindex="0">This text is not editable but is focusable</div>
+ <div tabindex="0">This text is not editable but is focusable</div>
+ <a href="#" id="d" contenteditable="true">ABCDEFGHIJKLMNOPQRSTUV</a>
+ <div tabindex="0">This text is not editable but is focusable</div>
+ <div tabindex="0">This text is not editable but is focusable</div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 406596 **/
+
+function testTabbing(click, focus, selectionOffset) {
+ var wu = SpecialPowers.getDOMWindowUtils(window);
+
+ var elem = document.getElementById(click);
+ var rect = elem.getBoundingClientRect();
+ var selection = window.getSelection();
+
+ var x = (rect.left + rect.right) / 4;
+ var y = (rect.top + rect.bottom) / 2;
+ wu.sendMouseEvent("mousedown", x, y, 0, 1, 0);
+ wu.sendMouseEvent("mousemove", x + selectionOffset, y, 0, 1, 0);
+ wu.sendMouseEvent("mouseup", x + selectionOffset, y, 0, 1, 0);
+ if (selectionOffset) {
+ is(selection.rangeCount, 1, "there should be one range in the selection");
+ var range = selection.getRangeAt(0);
+ }
+ var focusedElement = document.activeElement;
+ is(focusedElement, document.getElementById(focus),
+ "clicking should move focus to the contentEditable node");
+ synthesizeKey("KEY_Tab");
+ synthesizeKey("KEY_Tab");
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ synthesizeKey("KEY_Tab", {shiftKey: true});
+ is(document.activeElement, focusedElement,
+ "tab/shift-tab should move focus back to the contentEditable node");
+ if (selectionOffset) {
+ is(selection.rangeCount, 1,
+ "there should still be one range in the selection");
+ var newRange = selection.getRangeAt(0);
+ is(newRange.compareBoundaryPoints(Range.START_TO_START, range), 0,
+ "the selection should be the same as before the tabbing");
+ is(newRange.compareBoundaryPoints(Range.END_TO_END, range), 0,
+ "the selection should be the same as before the tabbing");
+ }
+}
+
+function test() {
+ window.getSelection().removeAllRanges();
+ testTabbing("edit", "edit", 0);
+ testTabbing("a", "edit", 0);
+ testTabbing("d", "d", 0);
+ testTabbing("edit", "edit", 10);
+ testTabbing("a", "edit", 10);
+ testTabbing("d", "d", 10);
+
+ SimpleTest.finish();
+}
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ setTimeout(test, 0);
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug417760.html b/dom/html/test/test_bug417760.html
new file mode 100644
index 0000000000..52a3c1b425
--- /dev/null
+++ b/dom/html/test/test_bug417760.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=417760
+-->
+<head>
+ <title>cannot focus() img with tabindex="-1"</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <style type="text/css">
+ img {
+ border: 5px solid white;
+ }
+ img:focus {
+ border: 5px solid black;
+ }
+ </style>
+
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="text/javascript">
+ function checkFocus(aExpected, aTabIndex)
+ {
+ elemCurr = document.activeElement.getAttribute("id");
+ is(elemCurr, aExpected, "Element with tabIndex " + aTabIndex
+ + " did not receive focus!");
+ }
+
+ function doTest()
+ {
+ // First, test img with tabindex = 0
+ document.getElementById("img-tabindex-0").focus();
+ checkFocus("img-tabindex-0", 0);
+
+ // now test the img with tabindex = -1
+ document.getElementById("img-tabindex-minus-1").focus();
+ checkFocus("img-tabindex-minus-1", -1);
+
+ // now test the img without tabindex, should NOT receive focus!
+ document.getElementById("img-no-tabindex").focus();
+ checkFocus("img-tabindex-minus-1", null);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=417760">Mozilla Bug 417760</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <br>img tabindex="0":
+ <img id="img-tabindex-0"
+ src="file_bug417760.png"
+ alt="MoCo logo" tabindex="0"/>
+ <br>img tabindex="-1":
+ <img id="img-tabindex-minus-1"
+ src="file_bug417760.png"
+ alt="MoCo logo" tabindex="-1"/>
+ <br>img without tabindex:
+ <img id="img-no-tabindex"
+ src="file_bug417760.png"
+ alt="MoCo logo"/>
+</body>
+</html>
diff --git a/dom/html/test/test_bug421640.html b/dom/html/test/test_bug421640.html
new file mode 100644
index 0000000000..c63d026d1f
--- /dev/null
+++ b/dom/html/test/test_bug421640.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=421640
+-->
+<head>
+ <title>Test for Bug 421640</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=421640">Mozilla Bug 421640</a>
+<div id="content">
+ <div id="edit" contenteditable="true">This text is editable</div>
+ <div><button id="button">Test</button></div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 421640 **/
+
+function test(click, focus, nextFocus) {
+ var wu = SpecialPowers.getDOMWindowUtils(window);
+
+ var selection = window.getSelection();
+ var edit = document.getElementById("edit");
+ var text = edit.firstChild;
+
+ selection.removeAllRanges();
+
+ var rect = edit.getBoundingClientRect();
+ wu.sendMouseEvent("mousedown", rect.left + 1, rect.top + 1, 0, 1, 0);
+ wu.sendMouseEvent("mousemove", rect.right - 1, rect.top + 1, 0, 1, 0);
+ wu.sendMouseEvent("mouseup", rect.right - 1, rect.top + 1, 0, 1, 0);
+
+ is(selection.anchorNode, text, "");
+
+ rect = document.getElementById("button").getBoundingClientRect();
+ wu.sendMouseEvent("mousedown", rect.left + 10, rect.top + 1, 0, 1, 0);
+ wu.sendMouseEvent("mouseup", rect.left + 10, rect.top + 1, 0, 1, 0);
+
+ is(selection.anchorNode, text, "");
+
+ SimpleTest.finish();
+}
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ setTimeout(test, 0);
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug424698.html b/dom/html/test/test_bug424698.html
new file mode 100644
index 0000000000..b59190e53d
--- /dev/null
+++ b/dom/html/test/test_bug424698.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=424698
+-->
+<head>
+ <title>Test for Bug 424698</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=424698">Mozilla Bug 424698</a>
+<p id="display">
+<input id="i1">
+<input id="target">
+<textarea id="i2"></textarea>
+<textarea id="target2"></textarea>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 424698 **/
+var i = $("i1");
+is(i.value, "", "Value should be empty string");
+i.defaultValue = "test";
+is(i.value, "test", "Setting defaultValue should work");
+i.defaultValue = "test2";
+is(i.value, "test2", "Setting defaultValue multiple times should work");
+
+// Now let's hide and reshow things
+i.style.display = "none";
+is(i.offsetWidth, 0, "Input didn't hide?");
+i.style.display = "";
+isnot(i.offsetWidth, 0, "Input didn't show?");
+is(i.value, "test2", "Hiding/showing should not affect value");
+i.defaultValue = "test3";
+is(i.value, "test3", "Setting defaultValue after hide/show should work");
+
+// Make sure typing works ok
+i = $("target");
+i.focus(); // Otherwise editor gets confused when we send the key events
+is(i.value, "", "Value should be empty string in second control");
+sendString("2test2");
+is(i.value, "2test2", 'We just typed the string "2test2"');
+i.defaultValue = "2test3";
+is(i.value, "2test2", "Setting defaultValue after typing should not work");
+i.style.display = "none";
+is(i.offsetWidth, 0, "Second input didn't hide?");
+i.style.display = "";
+isnot(i.offsetWidth, 0, "Second input didn't show?");
+is(i.value, "2test2", "Hiding/showing second input should not affect value");
+i.defaultValue = "2test4";
+is(i.value, "2test2", "Setting defaultValue after hide/show should not work if we typed");
+
+i = $("i2");
+is(i.value, "", "Textarea value should be empty string");
+i.defaultValue = "test";
+is(i.value, "test", "Setting textarea defaultValue should work");
+i.defaultValue = "test2";
+is(i.value, "test2", "Setting textarea defaultValue multiple times should work");
+
+// Now let's hide and reshow things
+i.style.display = "none";
+is(i.offsetWidth, 0, "Textarea didn't hide?");
+i.style.display = "";
+isnot(i.offsetWidth, 0, "Textarea didn't show?");
+is(i.value, "test2", "Hiding/showing textarea should not affect value");
+i.defaultValue = "test3";
+is(i.value, "test3", "Setting textarea defaultValue after hide/show should work");
+
+// Make sure typing works ok
+i = $("target2");
+i.focus(); // Otherwise editor gets confused when we send the key events
+is(i.value, "", "Textarea value should be empty string in second control");
+sendString("2test2");
+is(i.value, "2test2", 'We just typed the string "2test2"');
+i.defaultValue = "2test3";
+is(i.value, "2test2", "Setting textarea defaultValue after typing should not work");
+i.style.display = "none";
+is(i.offsetWidth, 0, "Second textarea didn't hide?");
+i.style.display = "";
+isnot(i.offsetWidth, 0, "Second textarea didn't show?");
+is(i.value, "2test2", "Hiding/showing second textarea should not affect value");
+i.defaultValue = "2test4";
+is(i.value, "2test2", "Setting textarea defaultValue after hide/show should not work if we typed");
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug428135.xhtml b/dom/html/test/test_bug428135.xhtml
new file mode 100644
index 0000000000..ce269e2f8c
--- /dev/null
+++ b/dom/html/test/test_bug428135.xhtml
@@ -0,0 +1,156 @@
+<?xml version="1.0"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=428135
+-->
+<head>
+ <title>Test for Bug 428135</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=428135">Mozilla Bug 428135</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+/** Test for Bug 428135 **/
+
+var expectedCurrentTargets = new Array();
+
+function d(el, ename) {
+ var e = document.createEvent("Events");
+ e.initEvent(ename, true, true);
+ el.dispatchEvent(e);
+}
+
+function testListener(e) {
+ e.preventDefault();
+ var expected = expectedCurrentTargets.shift();
+ ok(expected == e.currentTarget,
+ "Unexpected current target [" + e.currentTarget + "], event=" + e.type +
+ ", phase=" + e.eventPhase + ", target should have been " + expected);
+}
+
+function getAndAddListeners(elname) {
+ var el = document;
+ if (elname) {
+ el = document.getElementById(elname);
+ }
+ el.addEventListener("submit", testListener, true);
+ el.addEventListener("submit", testListener);
+ el.addEventListener("reset", testListener, true);
+ el.addEventListener("reset", testListener);
+ el.addEventListener("fooEvent", testListener, true);
+ el.addEventListener("fooEvent", testListener);
+ return el;
+}
+
+function testSubmitResetEvents() {
+ getAndAddListeners(null);
+ var outerForm = getAndAddListeners("outerForm");
+ var outerSubmit = getAndAddListeners("outerSubmit");
+ var outerReset = getAndAddListeners("outerReset");
+ var outerSubmitDispatcher = getAndAddListeners("outerSubmitDispatcher");
+ var outerResetDispatcher = getAndAddListeners("outerResetDispatcher");
+ var outerChild = getAndAddListeners("outerChild");
+ var innerForm = getAndAddListeners("innerForm");
+ var innerSubmit = getAndAddListeners("innerSubmit");
+ var innerReset = getAndAddListeners("innerReset");
+ var innerSubmitDispatcher = getAndAddListeners("innerSubmitDispatcher");
+ var innerResetDispatcher = getAndAddListeners("innerResetDispatcher");
+
+ expectedCurrentTargets = new Array(document, outerForm, outerForm, document);
+ outerSubmit.click();
+ ok(!expectedCurrentTargets.length,
+ "(1) expectedCurrentTargets isn't empty!");
+
+ expectedCurrentTargets = new Array(document, outerForm, outerForm, document);
+ outerReset.click();
+ ok(!expectedCurrentTargets.length,
+ "(2) expectedCurrentTargets isn't empty!");
+
+ // Because of bug 428135, submit shouldn't propagate
+ // back to outerForm and document!
+ expectedCurrentTargets =
+ new Array(document, outerForm, outerSubmitDispatcher, outerSubmitDispatcher);
+ outerSubmitDispatcher.click();
+ ok(!expectedCurrentTargets.length,
+ "(3) expectedCurrentTargets isn't empty!");
+
+ // Because of bug 428135, reset shouldn't propagate
+ // back to outerForm and document!
+ expectedCurrentTargets =
+ new Array(document, outerForm, outerResetDispatcher, outerResetDispatcher);
+ outerResetDispatcher.click();
+ ok(!expectedCurrentTargets.length,
+ "(4) expectedCurrentTargets isn't empty!");
+
+ // Because of bug 428135, submit shouldn't propagate
+ // back to outerForm and document!
+ expectedCurrentTargets =
+ new Array(document, outerForm, outerChild, innerForm, innerForm, outerChild);
+ innerSubmit.click();
+ ok(!expectedCurrentTargets.length,
+ "(5) expectedCurrentTargets isn't empty!");
+
+ // Because of bug 428135, reset shouldn't propagate
+ // back to outerForm and document!
+ expectedCurrentTargets =
+ new Array(document, outerForm, outerChild, innerForm, innerForm, outerChild);
+ innerReset.click();
+ ok(!expectedCurrentTargets.length,
+ "(6) expectedCurrentTargets isn't empty!");
+
+ // Because of bug 428135, submit shouldn't propagate
+ // back to inner/outerForm or document!
+ expectedCurrentTargets =
+ new Array(document, outerForm, outerChild, innerForm, innerSubmitDispatcher,
+ innerSubmitDispatcher);
+ innerSubmitDispatcher.click();
+ ok(!expectedCurrentTargets.length,
+ "(7) expectedCurrentTargets isn't empty!");
+
+ // Because of bug 428135, reset shouldn't propagate
+ // back to inner/outerForm or document!
+ expectedCurrentTargets =
+ new Array(document, outerForm, outerChild, innerForm, innerResetDispatcher,
+ innerResetDispatcher);
+ innerResetDispatcher.click();
+ ok(!expectedCurrentTargets.length,
+ "(8) expectedCurrentTargets isn't empty!");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(testSubmitResetEvents);
+addLoadEvent(SimpleTest.finish);
+
+
+]]>
+</script>
+</pre>
+<form id="outerForm">
+ <input type="submit" value="outer" id="outerSubmit"/>
+ <input type="reset" value="reset outer" id="outerReset"/>
+ <input type="button" value="dispatch submit" onclick="d(this, 'submit')"
+ id="outerSubmitDispatcher"/>
+ <input type="button" value="dispatch reset" onclick="d(this, 'reset')"
+ id="outerResetDispatcher"/>
+ <div id="outerChild">
+ <form id="innerForm">
+ <input type="submit" value="inner" id="innerSubmit"/>
+ <input type="reset" value="reset inner" id="innerReset"/>
+ <input type="button" value="dispatch submit" onclick="d(this, 'submit')"
+ id="innerSubmitDispatcher"/>
+ <input type="button" value="dispatch reset" onclick="d(this, 'reset')"
+ id="innerResetDispatcher"/>
+ </form>
+ </div>
+</form>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug430351.html b/dom/html/test/test_bug430351.html
new file mode 100644
index 0000000000..8cee4fe24f
--- /dev/null
+++ b/dom/html/test/test_bug430351.html
@@ -0,0 +1,523 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=430351
+-->
+<head>
+ <title>Test for Bug 430351</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=430351">Mozilla Bug 430351</a>
+<p id="display"></p>
+<div id="content">
+ <div id="parent"></div>
+ <div id="editableParent" contenteditable="true"></div>
+ <iframe id="frame"></iframe>
+ <map name="map"><area></map>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 430351 **/
+
+var focusableElements = [
+ "<a tabindex=\"-1\"></a>",
+ "<a tabindex=\"0\"></a>",
+ "<a tabindex=\"0\" disabled></a>",
+ "<a tabindex=\"1\"></a>",
+ "<a contenteditable=\"true\"></a>",
+
+ "<a href=\"#\"></a>",
+ "<a href=\"#\" tabindex=\"-1\"></a>",
+ "<a href=\"#\" tabindex=\"0\"></a>",
+ "<a href=\"#\" tabindex=\"0\" disabled></a>",
+ "<a href=\"#\" tabindex=\"1\"></a>",
+ "<a href=\"#\" contenteditable=\"true\"></a>",
+ "<a href=\"#\" disabled></a>",
+
+ "<button></button>",
+ "<button tabindex=\"-1\"></button>",
+ "<button tabindex=\"0\"></button>",
+ "<button tabindex=\"1\"></button>",
+ "<button contenteditable=\"true\"></button>",
+
+ "<button type=\"reset\"></button>",
+ "<button type=\"reset\" tabindex=\"-1\"></button>",
+ "<button type=\"reset\" tabindex=\"0\"></button>",
+ "<button type=\"reset\" tabindex=\"1\"></button>",
+ "<button type=\"reset\" contenteditable=\"true\"></button>",
+
+ "<button type=\"submit\"></button>",
+ "<button type=\"submit\" tabindex=\"-1\"></button>",
+ "<button type=\"submit\" tabindex=\"0\"></button>",
+ "<button type=\"submit\" tabindex=\"1\"></button>",
+ "<button type=\"submit\" contenteditable=\"true\"></button>",
+
+ "<div tabindex=\"-1\"></div>",
+ "<div tabindex=\"0\"></div>",
+ "<div tabindex=\"1\"></div>",
+ "<div contenteditable=\"true\"></div>",
+ "<div tabindex=\"0\" disabled></div>",
+
+ "<embed>",
+ "<embed tabindex=\"-1\">",
+ "<embed tabindex=\"0\">",
+ "<embed tabindex=\"0\" disabled>",
+ "<embed tabindex=\"1\">",
+ "<embed disabled>",
+ "<embed contenteditable=\"true\">",
+
+ "<iframe contenteditable=\"true\"></iframe>",
+
+ "<iframe src=\"about:blank\"></iframe>",
+ "<iframe src=\"about:blank\" disabled></iframe>",
+ "<iframe src=\"about:blank\" tabindex=\"-1\"></iframe>",
+ "<iframe src=\"about:blank\" tabindex=\"0\"></iframe>",
+ "<iframe src=\"about:blank\" tabindex=\"0\" disabled></iframe>",
+ "<iframe src=\"about:blank\" tabindex=\"1\"></iframe>",
+ "<iframe src=\"about:blank\" contenteditable=\"true\"></iframe>",
+
+ "<iframe></iframe>",
+ "<iframe tabindex=\"-1\"></iframe>",
+ "<iframe tabindex=\"0\"></iframe>",
+ "<iframe tabindex=\"0\" disabled></iframe>",
+ "<iframe tabindex=\"1\"></iframe>",
+ "<iframe disabled></iframe>",
+
+ "<img tabindex=\"-1\">",
+ "<img tabindex=\"0\">",
+ "<img tabindex=\"0\" disabled>",
+ "<img tabindex=\"1\">",
+
+ "<input>",
+ "<input tabindex=\"-1\">",
+ "<input tabindex=\"0\">",
+ "<input tabindex=\"1\">",
+ "<input contenteditable=\"true\">",
+
+ "<input type=\"button\">",
+ "<input type=\"button\" tabindex=\"-1\">",
+ "<input type=\"button\" tabindex=\"0\">",
+ "<input type=\"button\" tabindex=\"1\">",
+ "<input type=\"button\" contenteditable=\"true\">",
+
+ "<input type=\"checkbox\">",
+ "<input type=\"checkbox\" tabindex=\"-1\">",
+ "<input type=\"checkbox\" tabindex=\"0\">",
+ "<input type=\"checkbox\" tabindex=\"1\">",
+ "<input type=\"checkbox\" contenteditable=\"true\">",
+
+ "<input type=\"image\">",
+ "<input type=\"image\" tabindex=\"-1\">",
+ "<input type=\"image\" tabindex=\"0\">",
+ "<input type=\"image\" tabindex=\"1\">",
+ "<input type=\"image\" contenteditable=\"true\">",
+
+ "<input type=\"password\">",
+ "<input type=\"password\" tabindex=\"-1\">",
+ "<input type=\"password\" tabindex=\"0\">",
+ "<input type=\"password\" tabindex=\"1\">",
+ "<input type=\"password\" contenteditable=\"true\">",
+
+ "<input type=\"radio\">",
+ "<input type=\"radio\" tabindex=\"-1\">",
+ "<input type=\"radio\" tabindex=\"0\">",
+ "<input type=\"radio\" tabindex=\"1\">",
+ "<input type=\"radio\" contenteditable=\"true\">",
+ "<input type=\"radio\" checked>",
+ "<form><input type=\"radio\" name=\"foo\"></form>",
+
+ "<input type=\"reset\">",
+ "<input type=\"reset\" tabindex=\"-1\">",
+ "<input type=\"reset\" tabindex=\"0\">",
+ "<input type=\"reset\" tabindex=\"1\">",
+ "<input type=\"reset\" contenteditable=\"true\">",
+
+ "<input type=\"submit\">",
+ "<input type=\"submit\" tabindex=\"-1\">",
+ "<input type=\"submit\" tabindex=\"0\">",
+ "<input type=\"submit\" tabindex=\"1\">",
+ "<input type=\"submit\" contenteditable=\"true\">",
+
+ "<input type=\"text\">",
+ "<input type=\"text\" tabindex=\"-1\">",
+ "<input type=\"text\" tabindex=\"0\">",
+ "<input type=\"text\" tabindex=\"1\">",
+ "<input type=\"text\" contenteditable=\"true\">",
+
+ "<input type=\"number\">",
+ "<input type=\"number\" tabindex=\"-1\">",
+ "<input type=\"number\" tabindex=\"0\">",
+ "<input type=\"number\" tabindex=\"1\">",
+ "<input type=\"number\" contenteditable=\"true\">",
+
+ "<object tabindex=\"-1\"></object>",
+ "<object tabindex=\"0\"></object>",
+ "<object tabindex=\"1\"></object>",
+ "<object contenteditable=\"true\"></object>",
+
+ "<object classid=\"java:a\"></object>",
+ "<object classid=\"java:a\" tabindex=\"-1\"></object>",
+ "<object classid=\"java:a\" tabindex=\"0\"></object>",
+ "<object classid=\"java:a\" tabindex=\"0\" disabled></object>",
+ "<object classid=\"java:a\" tabindex=\"1\"></object>",
+ "<object classid=\"java:a\" disabled></object>",
+ "<object classid=\"java:a\" contenteditable=\"true\"></object>",
+
+ "<select></select>",
+ "<select tabindex=\"-1\"></select>",
+ "<select tabindex=\"0\"></select>",
+ "<select tabindex=\"1\"></select>",
+ "<select contenteditable=\"true\"></select>",
+
+ "<option tabindex='-1'></option>",
+ "<option tabindex='0'></option>",
+ "<option tabindex='1'></option>",
+ "<option contenteditable></option>",
+
+ "<optgroup tabindex='-1'></optgroup>",
+ "<optgroup tabindex='0'></optgroup>",
+ "<optgroup tabindex='1'></optgroup>",
+ "<optgroup contenteditable></optgroup>"
+];
+
+var nonFocusableElements = [
+ "<a></a>",
+ "<a disabled></a>",
+
+ "<button tabindex=\"0\" disabled></button>",
+ "<button disabled></button>",
+
+ "<button type=\"reset\" tabindex=\"0\" disabled></button>",
+ "<button type=\"reset\" disabled></button>",
+
+ "<button type=\"submit\" tabindex=\"0\" disabled></button>",
+ "<button type=\"submit\" disabled></button>",
+
+ "<div></div>",
+ "<div disabled></div>",
+
+ "<img>",
+ "<img disabled>",
+ "<img contenteditable=\"true\">",
+
+ "<img usemap=\"#map\">",
+ "<img usemap=\"#map\" tabindex=\"-1\">",
+ "<img usemap=\"#map\" tabindex=\"0\">",
+ "<img usemap=\"#map\" tabindex=\"0\" disabled>",
+ "<img usemap=\"#map\" tabindex=\"1\">",
+ "<img usemap=\"#map\" disabled>",
+ "<img usemap=\"#map\" contenteditable=\"true\">",
+
+ "<input tabindex=\"0\" disabled>",
+ "<input disabled>",
+
+ "<input type=\"button\" tabindex=\"0\" disabled>",
+ "<input type=\"button\" disabled>",
+
+ "<input type=\"checkbox\" tabindex=\"0\" disabled>",
+ "<input type=\"checkbox\" disabled>",
+
+ "<input type=\"file\" tabindex=\"0\" disabled>",
+ "<input type=\"file\" disabled>",
+
+ "<input type=\"hidden\">",
+ "<input type=\"hidden\" tabindex=\"-1\">",
+ "<input type=\"hidden\" tabindex=\"0\">",
+ "<input type=\"hidden\" tabindex=\"0\" disabled>",
+ "<input type=\"hidden\" tabindex=\"1\">",
+ "<input type=\"hidden\" disabled>",
+ "<input type=\"hidden\" contenteditable=\"true\">",
+
+ "<input type=\"image\" tabindex=\"0\" disabled>",
+ "<input type=\"image\" disabled>",
+
+ "<input type=\"password\" tabindex=\"0\" disabled>",
+ "<input type=\"password\" disabled>",
+
+ "<input type=\"radio\" tabindex=\"0\" disabled>",
+ "<input type=\"radio\" disabled>",
+
+ "<input type=\"reset\" tabindex=\"0\" disabled>",
+ "<input type=\"reset\" disabled>",
+
+ "<input type=\"submit\" tabindex=\"0\" disabled>",
+ "<input type=\"submit\" disabled>",
+
+ "<input type=\"text\" tabindex=\"0\" disabled>",
+ "<input type=\"text\" disabled>",
+
+ "<object></object>",
+
+ "<select tabindex=\"0\" disabled></select>",
+ "<select disabled></select>",
+
+ "<option></option>",
+ "<option tabindex='1' disabled></option>",
+
+ "<optgroup></optgroup>",
+ "<optgroup tabindex='1' disabled></optgroup>"
+];
+
+var focusableInContentEditable = [
+ "<button></button>",
+ "<button tabindex=\"-1\"></button>",
+ "<button tabindex=\"0\"></button>",
+ "<button tabindex=\"1\"></button>",
+ "<button contenteditable=\"true\"></button>",
+
+ "<button type=\"reset\"></button>",
+ "<button type=\"reset\" tabindex=\"-1\"></button>",
+ "<button type=\"reset\" tabindex=\"0\"></button>",
+ "<button type=\"reset\" tabindex=\"1\"></button>",
+ "<button type=\"reset\" contenteditable=\"true\"></button>",
+
+ "<button type=\"submit\"></button>",
+ "<button type=\"submit\" tabindex=\"-1\"></button>",
+ "<button type=\"submit\" tabindex=\"0\"></button>",
+ "<button type=\"submit\" tabindex=\"1\"></button>",
+ "<button type=\"submit\" contenteditable=\"true\"></button>",
+
+ "<div tabindex=\"-1\"></div>",
+ "<div tabindex=\"0\"></div>",
+ "<div tabindex=\"1\"></div>",
+ "<div tabindex=\"0\" disabled></div>",
+
+ "<embed>",
+ "<embed tabindex=\"-1\">",
+ "<embed tabindex=\"0\">",
+ "<embed tabindex=\"0\" disabled>",
+ "<embed tabindex=\"1\">",
+ "<embed disabled>",
+ "<embed contenteditable=\"true\">",
+
+ "<iframe src=\"about:blank\"></iframe>",
+ "<iframe></iframe>",
+ "<iframe src=\"about:blank\" disabled></iframe>",
+ "<iframe disabled></iframe>",
+ "<iframe src=\"about:blank\" tabindex=\"-1\"></iframe>",
+ "<iframe tabindex=\"-1\"></iframe>",
+ "<iframe src=\"about:blank\" tabindex=\"0\"></iframe>",
+ "<iframe tabindex=\"0\"></iframe>",
+ "<iframe src=\"about:blank\" tabindex=\"0\" disabled></iframe>",
+ "<iframe tabindex=\"0\" disabled></iframe>",
+ "<iframe src=\"about:blank\" tabindex=\"1\"></iframe>",
+ "<iframe tabindex=\"1\"></iframe>",
+ "<iframe src=\"about:blank\" contenteditable=\"true\"></iframe>",
+ "<iframe contenteditable=\"true\"></iframe>",
+
+ "<img tabindex=\"-1\">",
+ "<img tabindex=\"0\">",
+ "<img tabindex=\"0\" disabled>",
+ "<img tabindex=\"1\">",
+
+ "<input>",
+ "<input tabindex=\"-1\">",
+ "<input tabindex=\"0\">",
+ "<input tabindex=\"1\">",
+ "<input contenteditable=\"true\">",
+
+ "<input type=\"button\">",
+ "<input type=\"button\" tabindex=\"-1\">",
+ "<input type=\"button\" tabindex=\"0\">",
+ "<input type=\"button\" tabindex=\"1\">",
+ "<input type=\"button\" contenteditable=\"true\">",
+
+ "<input type=\"file\">",
+ "<input type=\"file\" tabindex=\"-1\">",
+ "<input type=\"file\" tabindex=\"0\">",
+ "<input type=\"file\" tabindex=\"1\">",
+ "<input type=\"file\" contenteditable=\"true\">",
+
+ "<input type=\"checkbox\">",
+ "<input type=\"checkbox\" tabindex=\"-1\">",
+ "<input type=\"checkbox\" tabindex=\"0\">",
+ "<input type=\"checkbox\" tabindex=\"1\">",
+ "<input type=\"checkbox\" contenteditable=\"true\">",
+
+ "<input type=\"image\">",
+ "<input type=\"image\" tabindex=\"-1\">",
+ "<input type=\"image\" tabindex=\"0\">",
+ "<input type=\"image\" tabindex=\"1\">",
+ "<input type=\"image\" contenteditable=\"true\">",
+
+ "<input type=\"password\">",
+ "<input type=\"password\" tabindex=\"-1\">",
+ "<input type=\"password\" tabindex=\"0\">",
+ "<input type=\"password\" tabindex=\"1\">",
+ "<input type=\"password\" contenteditable=\"true\">",
+
+ "<input type=\"radio\">",
+ "<input type=\"radio\" tabindex=\"-1\">",
+ "<input type=\"radio\" tabindex=\"0\">",
+ "<input type=\"radio\" tabindex=\"1\">",
+ "<input type=\"radio\" contenteditable=\"true\">",
+ "<input type=\"radio\" checked>",
+ "<form><input type=\"radio\" name=\"foo\"></form>",
+
+ "<input type=\"reset\">",
+ "<input type=\"reset\" tabindex=\"-1\">",
+ "<input type=\"reset\" tabindex=\"0\">",
+ "<input type=\"reset\" tabindex=\"1\">",
+ "<input type=\"reset\" contenteditable=\"true\">",
+
+ "<input type=\"submit\">",
+ "<input type=\"submit\" tabindex=\"-1\">",
+ "<input type=\"submit\" tabindex=\"0\">",
+ "<input type=\"submit\" tabindex=\"1\">",
+ "<input type=\"submit\" contenteditable=\"true\">",
+
+ "<input type=\"text\">",
+ "<input type=\"text\" tabindex=\"-1\">",
+ "<input type=\"text\" tabindex=\"0\">",
+ "<input type=\"text\" tabindex=\"1\">",
+ "<input type=\"text\" contenteditable=\"true\">",
+
+ "<input type=\"number\">",
+ "<input type=\"number\" tabindex=\"-1\">",
+ "<input type=\"number\" tabindex=\"0\">",
+ "<input type=\"number\" tabindex=\"1\">",
+ "<input type=\"number\" contenteditable=\"true\">",
+
+ "<object tabindex=\"-1\"></object>",
+ "<object tabindex=\"0\"></object>",
+ "<object tabindex=\"1\"></object>",
+
+ // Disabled doesn't work for <object>.
+ "<object tabindex=\"0\" disabled></object>",
+ "<object disabled></object>",
+
+ "<select></select>",
+ "<select tabindex=\"-1\"></select>",
+ "<select tabindex=\"0\"></select>",
+ "<select tabindex=\"1\"></select>",
+ "<select contenteditable=\"true\"></select>",
+
+ "<option tabindex='-1'></option>",
+ "<option tabindex='0'></option>",
+ "<option tabindex='1'></option>",
+
+ "<optgroup tabindex='-1'></optgroup>",
+ "<optgroup tabindex='0'></optgroup>",
+ "<optgroup tabindex='1'></optgroup>"
+];
+
+var focusableInDesignMode = [
+ "<embed>",
+ "<embed tabindex=\"-1\">",
+ "<embed tabindex=\"0\">",
+ "<embed tabindex=\"0\" disabled>",
+ "<embed tabindex=\"1\">",
+ "<embed disabled>",
+ "<embed contenteditable=\"true\">",
+
+ "<img tabindex=\"-1\">",
+ "<img tabindex=\"0\">",
+ "<img tabindex=\"0\" disabled>",
+ "<img tabindex=\"1\">",
+];
+
+// Can't currently test these, need a plugin.
+var focusableElementsTODO = [
+ "<object classid=\"java:a\"></object>",
+ "<object classid=\"java:a\" tabindex=\"-1\"></object>",
+ "<object classid=\"java:a\" tabindex=\"0\"></object>",
+ "<object classid=\"java:a\" tabindex=\"0\" disabled></object>",
+ "<object classid=\"java:a\" tabindex=\"1\"></object>",
+ "<object classid=\"java:a\" disabled></object>",
+ "<object classid=\"java:a\" contenteditable=\"true\"></object>",
+];
+
+var serializer = new XMLSerializer();
+
+function testElements(parent, tags, shouldBeFocusable)
+{
+ var focusable, errorSuffix = "";
+ if (parent.ownerDocument.designMode == "on") {
+ focusable = focusableInDesignMode;
+ errorSuffix = " in a document with designMode=on";
+ }
+ else if (parent.contentEditable == "true") {
+ focusable = focusableInContentEditable;
+ }
+
+ for (var tag of tags) {
+ parent.ownerDocument.body.focus();
+
+ if (focusableElementsTODO.indexOf(tag) > -1) {
+ todo_is(parent.ownerDocument.activeElement, parent.firstChild,
+ tag + " should be focusable" + errorSuffix);
+ continue;
+ }
+
+ parent.innerHTML = tag;
+
+ // Focus the deepest descendant.
+ var descendant = parent;
+ while ((descendant = descendant.firstChild))
+ element = descendant;
+
+ if (element.nodeName == "IFRAME")
+ var foo = element.contentDocument;
+
+ element.focus();
+
+ var errorPrefix = serializer.serializeToString(element) + " in " +
+ serializer.serializeToString(parent);
+
+ try {
+ // Make sure activeElement doesn't point to a
+ // native anonymous element.
+ parent.ownerDocument.activeElement.localName;
+ } catch (ex) {
+ ok(false, ex + errorPrefix + errorSuffix);
+ }
+ if (focusable ? focusable.indexOf(tag) > -1 : shouldBeFocusable) {
+ is(parent.ownerDocument.activeElement, element,
+ errorPrefix + " should be focusable" + errorSuffix);
+ }
+ else {
+ isnot(parent.ownerDocument.activeElement, element,
+ errorPrefix + " should not be focusable" + errorSuffix);
+ }
+
+ parent.innerHTML = "";
+ }
+}
+
+function test()
+{
+ var parent = document.getElementById("parent");
+ var editableParent = document.getElementById("editableParent");
+
+ testElements(parent, focusableElements, true);
+ testElements(parent, nonFocusableElements, false);
+
+ testElements(editableParent, focusableElements, true);
+ testElements(editableParent, nonFocusableElements, false);
+
+ var frame = document.getElementById("frame");
+ frame.contentDocument.body.innerHTML = document.getElementById("content").innerHTML;
+ frame.contentDocument.designMode = "on";
+ parent = frame.contentDocument.getElementById("parent");
+ editableParent = frame.contentDocument.getElementById("editableParent");
+
+ testElements(parent, focusableElements, false);
+ testElements(parent, nonFocusableElements, false);
+
+ testElements(editableParent, focusableElements, false);
+ testElements(editableParent, nonFocusableElements, false);
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(test);
+addLoadEvent(SimpleTest.finish);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug435128.html b/dom/html/test/test_bug435128.html
new file mode 100644
index 0000000000..0f4cf7cdb0
--- /dev/null
+++ b/dom/html/test/test_bug435128.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=435128
+-->
+<head>
+ <title>Test for Bug 435128</title>
+ <script type="application/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=435128">Mozilla Bug 435128</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<iframe id="content" src="data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%3C/head%3E%3Cbody%3E%0A%3Ciframe%20id%3D%22a%22%3E%3C/iframe%3E%0A%3Cscript%3E%0Afunction%20doe%28%29%20%7B%0Avar%20x%20%3D%20window.frames%5B0%5D.document%3B%0A%0Avar%20y%3Ddocument.getElementById%28%27a%27%29%3B%0Ay.parentNode.removeChild%28y%29%3B%0A%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0A%7D%0AsetTimeout%28%27doe%28%29%27%2C20%29%3B%0A%3C/script%3E%0A%3C/body%3E%3C/html%3E" style="width: 1000px; height: 200px;"></iframe>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 435128 **/
+
+SimpleTest.waitForExplicitFinish();
+
+setTimeout(finish, 60000);
+
+function doe2() {
+ document.getElementById('content').src = document.getElementById('content').src;
+}
+setInterval(doe2, 400);
+
+function finish()
+{
+ ok(true, "This is a mochikit version of a crash test. To complete is to pass.");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug441930.html b/dom/html/test/test_bug441930.html
new file mode 100644
index 0000000000..dcb7926734
--- /dev/null
+++ b/dom/html/test/test_bug441930.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=441930
+-->
+<head>
+ <title>Test for Bug 441930</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=441930">Mozilla Bug 441930</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 441930: see bug441930_iframe.html **/
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+<p id="display">
+ <iframe src="bug441930_iframe.html"></iframe>
+</p>
+<div id="content" style="display: none">
+</div>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug442801.html b/dom/html/test/test_bug442801.html
new file mode 100644
index 0000000000..1a93d94f14
--- /dev/null
+++ b/dom/html/test/test_bug442801.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=442801
+-->
+<head>
+ <title>Test for Bug 442801</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=442801">Mozilla Bug 442801</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+<div contenteditable="true">
+<p id="ce_true" contenteditable="true">contenteditable true</p>
+</div>
+
+<div contenteditable="true">
+<p id="ce_false" contenteditable="false">contenteditable false</p>
+</div>
+
+<div contenteditable="true">
+<p id="ce_empty" contenteditable="">contenteditable empty</p>
+</div>
+
+<div contenteditable="true">
+<p id="ce_inherit" contenteditable="inherit">contenteditable inherit</p>
+</div>
+
+<div contenteditable="true">
+<p id="ce_none" >contenteditable none</p>
+</div>
+
+
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 442801 **/
+
+is(window.getComputedStyle($("ce_true")).getPropertyValue("-moz-user-modify"),
+ "read-write",
+ "parent contenteditable is true, contenteditable is true; user-modify should be read-write");
+is(window.getComputedStyle($("ce_false")).getPropertyValue("-moz-user-modify"),
+ "read-only",
+ "parent contenteditable is true, contenteditable is false; user-modify should be read-only");
+is(window.getComputedStyle($("ce_empty")).getPropertyValue("-moz-user-modify"),
+ "read-write",
+ "parent contenteditable is true, contenteditable is empty; user-modify should be read-write");
+is(window.getComputedStyle($("ce_inherit")).getPropertyValue("-moz-user-modify"),
+ "read-write",
+ "parent contenteditable is true, contenteditable is inherit; user-modify should be read-write");
+is(window.getComputedStyle($("ce_none")).getPropertyValue("-moz-user-modify"),
+ "read-write",
+ "parent contenteditable is true, contenteditable is none; user-modify should be read-write");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug445004.html b/dom/html/test/test_bug445004.html
new file mode 100644
index 0000000000..02fc79f425
--- /dev/null
+++ b/dom/html/test/test_bug445004.html
@@ -0,0 +1,138 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=445004
+-->
+<head>
+ <title>Test for Bug 445004</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=445004">Mozilla Bug 445004</a>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 445004 **/
+is(window.location.hostname, "mochi.test", "Unexpected hostname");
+is(window.location.port, "8888", "Unexpected port; fix testcase");
+
+SimpleTest.waitForExplicitFinish();
+
+var loads = 1;
+
+function loadStarted() {
+ ++loads;
+}
+function loadEnded() {
+ --loads;
+ if (loads == 0) {
+ doTest();
+ }
+}
+
+window.onload = loadEnded;
+
+function getMessage(evt) {
+ ok(evt.data == "start" || evt.data == "end", "Must have start or end");
+ if (evt.data == "start")
+ loadStarted();
+ else
+ loadEnded();
+}
+
+window.addEventListener("message", getMessage);
+
+function checkURI(uri, name, type) {
+ var host = uri.match(/^http:\/\/([a-z.0-9]*)/)[1];
+ var file = uri.match(/([^\/]*).png$/)[1];
+ is(host, file, "Unexpected base URI for test " + name +
+ " when testing " + type);
+}
+
+function checkFrame(num) {
+ // Just snarf our data
+ var outer = SpecialPowers.wrap(window.frames[num]);
+ name = outer.name;
+
+ is(outer.document.baseURI,
+ "http://example.org/tests/dom/html/test/bug445004-outer.html",
+ "Unexpected base URI for " + name);
+
+ var iswrite = name.match(/write/);
+
+ var inner = outer.frames[0];
+ if (iswrite) {
+ is(inner.document.baseURI,
+ "http://example.org/tests/dom/html/test/bug445004-outer.html",
+ "Unexpected inner base URI for " + name);
+ } else {
+ is(inner.document.baseURI,
+ "http://test1.example.org/tests/dom/html/test/bug445004-inner.html",
+ "Unexpected inner base URI for " + name);
+ }
+
+ var isrel = name.match(/rel/);
+ var offsite = name.match(/offsite/);
+
+ if (!iswrite) {
+ if ((isrel && !offsite) || (!isrel && offsite)) {
+ is(inner.location.hostname, outer.location.hostname,
+ "Unexpected hostnames for " + name);
+ } else {
+ isnot(inner.location.hostname, outer.location.hostname,
+ "Unexpected hostnames for " + name);
+ }
+ }
+
+ checkURI(inner.frames[0].location.href, name, "direct location");
+ checkURI(inner.frames[1].document.getElementsByTagName("img")[0].src,
+ name, "direct write");
+ if (!iswrite) {
+ is(inner.frames[1].location.hostname, inner.location.hostname,
+ "Incorrect hostname for " + name + " direct write")
+ }
+ checkURI(inner.frames[2].location.href, name, "indirect location");
+ checkURI(inner.frames[3].document.getElementsByTagName("img")[0].src,
+ name, "indirect write");
+ if (!iswrite) {
+ is(inner.frames[3].location.hostname, outer.location.hostname,
+ "Incorrect hostname for " + name + " indirect write")
+ }
+ checkURI(inner.document.getElementsByTagName("img")[0].src,
+ name, "direct image load");
+}
+
+
+function doTest() {
+ for (var num = 0; num < 5; ++num) {
+ checkFrame(num);
+ }
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+<p id="display">
+ <iframe
+ src="http://example.org/tests/dom/html/test/bug445004-outer-rel.html"
+ name="bug445004-outer-rel.html"></iframe>
+ <iframe
+ src="http://test1.example.org/tests/dom/html/test/bug445004-outer-rel.html"
+ name="bug445004-outer-rel.html offsite"></iframe>
+ <iframe
+ src="http://example.org/tests/dom/html/test/bug445004-outer-abs.html"
+ name="bug445004-outer-abs.html"></iframe>
+ <iframe
+ src="http://test1.example.org/tests/dom/html/test/bug445004-outer-abs.html"
+ name="bug445004-outer-abs.html offsite"></iframe>
+ <iframe
+ src="http://example.org/tests/dom/html/test/bug445004-outer-write.html"
+ name="bug445004-outer-write.html"></iframe>
+</p>
+</body>
+</html>
diff --git a/dom/html/test/test_bug446483.html b/dom/html/test/test_bug446483.html
new file mode 100644
index 0000000000..9821670da7
--- /dev/null
+++ b/dom/html/test/test_bug446483.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=446483
+-->
+<head>
+ <title>Test for Bug 446483</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=446483">Mozilla Bug 446483</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 446483 **/
+
+function gc() {
+ SpecialPowers.gc();
+}
+
+function runTest() {
+ document.getElementById('display').innerHTML =
+ '<iframe src="bug446483-iframe.html"><\/iframe>\n' +
+ '<iframe src="bug446483-iframe.html"><\/iframe>\n';
+
+ setInterval(gc, 1000);
+
+ setTimeout(function() {
+ document.getElementById('display').innerHTML = '';
+ ok(true, '');
+ SimpleTest.finish();
+ }, 4000);
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+addLoadEvent(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug448166.html b/dom/html/test/test_bug448166.html
new file mode 100644
index 0000000000..45b47fcb3f
--- /dev/null
+++ b/dom/html/test/test_bug448166.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=448166
+-->
+<head>
+ <meta charset="utf-8" />
+ <title>Test for Bug 448166</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=448166">Mozilla Bug 448166</a>
+<p id="display">
+ <a id="test" href="http://www.moz&#xdc00;illa.org">should not be Mozilla</a>
+ <a id="control" href="http://www.mozilla.org">should not be Mozilla</a>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 448166 **/
+isnot($("test").href, "http://www.mozilla.org/",
+ "Should notice unpaired surrogate");
+is($("test").href, "http://www.moz�illa.org",
+ "URL parser fails. Href returns original input string");
+
+SimpleTest.doesThrow(() => { new URL($("test").href);}, "URL parser rejects input");
+
+is($("control").href, "http://www.mozilla.org/",
+ "Just making sure .href works");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug448564.html b/dom/html/test/test_bug448564.html
new file mode 100644
index 0000000000..bfb61af8dd
--- /dev/null
+++ b/dom/html/test/test_bug448564.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=448564
+-->
+<head>
+ <title>Test for Bug 448564</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=448564">Mozilla Bug 448564</a>
+<p id="display">
+ <iframe src="bug448564-iframe-1.html"></iframe>
+ <iframe src="bug448564-iframe-2.html"></iframe>
+ <iframe src="bug448564-iframe-3.html"></iframe>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 448564 **/
+
+/**
+ * The three iframes are going to be loaded with some dirty constructed forms.
+ * Each of them will be submitted before the load event and a SJS will replace
+ * the frame content with the query string.
+ * Then, on the load event, our test file will check the content of each iframes
+ * and check if the query string were correctly formatted (implying that all
+ * iframes were correctly submitted.
+ */
+
+function checkQueryString(frame) {
+ var queryString = frame.document.body.textContent;
+ is(queryString.split("&").sort().join("&"),
+ "a=aval&b=bval&c=cval&d=dval",
+ "Not all form fields were properly submitted.");
+}
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ checkQueryString(frames[0]);
+ checkQueryString(frames[1]);
+ checkQueryString(frames[2]);
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug456229.html b/dom/html/test/test_bug456229.html
new file mode 100644
index 0000000000..c9d6c36054
--- /dev/null
+++ b/dom/html/test/test_bug456229.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=456229
+-->
+<head>
+ <title>Test for Bug 456229</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=456229">Mozilla Bug 456229</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <input id='i' type="search">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 456229 **/
+
+// More checks are done in test_bug551670.html.
+
+var i = document.getElementById('i');
+is(i.type, 'search', "Search state should be recognized");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug458037.xhtml b/dom/html/test/test_bug458037.xhtml
new file mode 100644
index 0000000000..c8ae3e1191
--- /dev/null
+++ b/dom/html/test/test_bug458037.xhtml
@@ -0,0 +1,112 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=458037
+-->
+<head>
+ <title>Test for Bug 458037</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=458037">Mozilla Bug 458037</a>
+<p id="display"></p>
+<div id="content" >
+<div id="a"></div>
+<div id="b" contenteditable="true"></div>
+<div id="c" contenteditable="false"></div>
+<div id="d" contenteditable="inherit"></div>
+<div contenteditable="true">
+ <div id="e"></div>
+</div>
+<div contenteditable="false">
+ <div id="f"></div>
+</div>
+<div contenteditable="true">
+ <div id="g" contenteditable="false"></div>
+</div>
+<div contenteditable="false">
+ <div id="h" contenteditable="true"></div>
+</div>
+<div contenteditable="true">
+ <div id="i" contenteditable="inherit"></div>
+</div>
+<div contenteditable="false">
+ <div id="j" contenteditable="inherit"></div>
+</div>
+<div contenteditable="true">
+ <xul:box>
+ <div id="k"></div>
+ </xul:box>
+</div>
+<div contenteditable="false">
+ <xul:box>
+ <div id="l"></div>
+ </xul:box>
+</div>
+<div contenteditable="true">
+ <xul:box>
+ <div id="m" contenteditable="inherit"></div>
+ </xul:box>
+</div>
+<div contenteditable="false">
+ <xul:box>
+ <div id="n" contenteditable="inherit"></div>
+ </xul:box>
+</div>
+<div id="x"></div>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 458037 **/
+
+function test(id, expected) {
+ is(document.getElementById(id).isContentEditable, expected,
+ "Element " + id + " should " + (expected ? "" : "not ") + "be editable");
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+ test("a", false);
+ test("b", true);
+ test("c", false);
+ test("d", false);
+ test("e", true);
+ test("f", false);
+ test("g", false);
+ test("h", true);
+ test("i", true);
+ test("j", false);
+ test("k", true);
+ test("l", false);
+ test("m", true);
+ test("n", false);
+
+ var d = document.getElementById("x");
+ test("x", false);
+ d.setAttribute("contenteditable", "true");
+ test("x", true);
+ d.setAttribute("contenteditable", "false");
+ test("x", false);
+ d.setAttribute("contenteditable", "inherit");
+ test("x", false);
+ d.removeAttribute("contenteditable");
+ test("x", false);
+ d.contentEditable = "true";
+ test("x", true);
+ d.contentEditable = "false";
+ test("x", false);
+ d.contentEditable = "inherit";
+ test("x", false);
+
+ // Make sure that isContentEditable is read-only
+ var origValue = d.isContentEditable;
+ d.isContentEditable = !origValue;
+ is(d.isContentEditable, origValue, "isContentEditable should be read only");
+});
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug460568.html b/dom/html/test/test_bug460568.html
new file mode 100644
index 0000000000..db379e6fcc
--- /dev/null
+++ b/dom/html/test/test_bug460568.html
@@ -0,0 +1,144 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=460568
+-->
+<head>
+ <title>Test for Bug 460568</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=460568">Mozilla Bug 460568</a>
+<p id="display"><a href="" id="anchor">a[href]</a></p>
+<div id="editor">
+ <a href="" id="anchorInEditor">a[href] in editor</a>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 460568 **/
+
+function runTest()
+{
+ var editor = document.getElementById("editor");
+ var anchor = document.getElementById("anchor");
+ var anchorInEditor = document.getElementById("anchorInEditor");
+
+ var focused;
+ anchorInEditor.onfocus = function() { focused = true; };
+
+ function isReallyEditable()
+ {
+ editor.focus();
+ var range = document.createRange();
+ range.selectNodeContents(editor);
+ var prevStr = range.toString();
+
+ var docShell = SpecialPowers.wrap(window).docShell;
+ var controller =
+ docShell.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+ .getInterface(SpecialPowers.Ci.nsISelectionDisplay)
+ .QueryInterface(SpecialPowers.Ci.nsISelectionController);
+ var sel = controller.getSelection(controller.SELECTION_NORMAL);
+ sel.collapse(anchorInEditor, 0);
+ sendString("a");
+ range.selectNodeContents(editor);
+ return prevStr != range.toString();
+ }
+
+ focused = false;
+ anchor.focus();
+ editor.setAttribute("contenteditable", "true");
+ anchorInEditor.focus();
+ is(focused, false, "focus moved to element in contenteditable=true");
+ is(isReallyEditable(), true, "cannot edit by a key event");
+
+ // for bug 502273
+ focused = false;
+ anchor.focus();
+ editor.setAttribute("dummy", "dummy");
+ editor.removeAttribute("dummy");
+ anchorInEditor.focus();
+ is(focused, false, "focus moved to element in contenteditable=true (after dummy attribute was removed)");
+ is(isReallyEditable(), true, "cannot edit by a key event");
+
+ focused = false;
+ anchor.focus();
+ editor.setAttribute("contenteditable", "false");
+ anchorInEditor.focus();
+ is(focused, true, "focus didn't move to element in contenteditable=false");
+ is(isReallyEditable(), false, "can edit by a key event");
+
+ // for bug 502273
+ focused = false;
+ anchor.focus();
+ editor.setAttribute("dummy", "dummy");
+ editor.removeAttribute("dummy");
+ anchorInEditor.focus();
+ is(focused, true, "focus moved to element in contenteditable=true (after dummy attribute was removed)");
+ is(isReallyEditable(), false, "cannot edit by a key event");
+
+ focused = false;
+ anchor.focus();
+ editor.setAttribute("contenteditable", "true");
+ anchorInEditor.focus();
+ is(focused, false, "focus moved to element in contenteditable=true");
+ is(isReallyEditable(), true, "cannot edit by a key event");
+
+ // for bug 502273
+ focused = false;
+ anchor.focus();
+ editor.setAttribute("dummy", "dummy");
+ editor.removeAttribute("dummy");
+ anchorInEditor.focus();
+ is(focused, false, "focus moved to element in contenteditable=true (after dummy attribute was removed)");
+ is(isReallyEditable(), true, "cannot edit by a key event");
+
+ focused = false;
+ anchor.focus();
+ editor.removeAttribute("contenteditable");
+ anchorInEditor.focus();
+ is(focused, true, "focus didn't move to element in contenteditable removed element");
+ is(isReallyEditable(), false, "can edit by a key event");
+
+ focused = false;
+ anchor.focus();
+ editor.contentEditable = true;
+ anchorInEditor.focus();
+ is(focused, false, "focus moved to element in contenteditable=true by property");
+ is(isReallyEditable(), true, "cannot edit by a key event");
+
+ focused = false;
+ anchor.focus();
+ editor.contentEditable = false;
+ anchorInEditor.focus();
+ is(focused, true, "focus didn't move to element in contenteditable=false by property");
+ is(isReallyEditable(), false, "can edit by a key event");
+
+ focused = false;
+ anchor.focus();
+ editor.setAttribute("contenteditable", "true");
+ anchorInEditor.focus();
+ is(focused, false, "focus moved to element in contenteditable=true");
+ is(isReallyEditable(), true, "cannot edit by a key event");
+
+ // for bug 502273
+ focused = false;
+ anchor.focus();
+ editor.setAttribute("dummy", "dummy");
+ editor.removeAttribute("dummy");
+ anchorInEditor.focus();
+ is(focused, false, "focus moved to element in contenteditable=true (after dummy attribute was removed)");
+ is(isReallyEditable(), true, "cannot edit by a key event");
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+addLoadEvent(SimpleTest.finish);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug463104.html b/dom/html/test/test_bug463104.html
new file mode 100644
index 0000000000..c44419120d
--- /dev/null
+++ b/dom/html/test/test_bug463104.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Noninteger coordinates test</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="a" style="position: fixed; left: 5.5px; top: 5.5px; width: 100px; height: 100px; background: blue"></div>
+<p style="margin-top: 110px">
+<script>
+var a = document.getElementById("a");
+isnot(a, document.elementFromPoint(5, 5), "a shouldn't be found");
+isnot(a, document.elementFromPoint(5.25, 5.25), "a shouldn't be found");
+is(a, document.elementFromPoint(5.5, 5.5), "a should be found");
+is(a, document.elementFromPoint(5.75, 5.75), "a should be found");
+is(a, document.elementFromPoint(6, 6), "a should be found");
+is(a, document.elementFromPoint(105, 105), "a should be found");
+is(a, document.elementFromPoint(105.25, 105.25), "a should be found");
+isnot(a, document.elementFromPoint(105.5, 105.5), "a shouldn't be found");
+isnot(a, document.elementFromPoint(105.75, 105.75), "a shouldn't be found");
+isnot(a, document.elementFromPoint(106, 106), "a shouldn't be found");
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug478251.html b/dom/html/test/test_bug478251.html
new file mode 100644
index 0000000000..e33e7b04e2
--- /dev/null
+++ b/dom/html/test/test_bug478251.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=478251
+-->
+<head>
+ <title>Test for Bug 478251</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=478251">Mozilla Bug 478251</a>
+<p id="display"><iframe id="t"></iframe></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 478251 **/
+var doc = $("t").contentDocument;
+doc.open();
+doc.write();
+doc.close();
+is(doc.documentElement.textContent, "", "Writing || failed");
+
+doc.open();
+doc.write(null);
+doc.close();
+is(doc.documentElement.textContent, "null", "Writing |null| failed");
+
+doc.open();
+doc.write(null, null);
+doc.close();
+is(doc.documentElement.textContent, "nullnull", "Writing |null, null| failed");
+
+doc.open();
+doc.write(undefined);
+doc.close();
+is(doc.documentElement.textContent, "undefined", "Writing |undefined| failed");
+
+doc.open();
+doc.write(undefined, undefined);
+doc.close();
+is(doc.documentElement.textContent, "undefinedundefined", "Writing |undefined, undefined| failed");
+
+doc.open();
+doc.writeln();
+doc.close();
+ok(doc.documentElement.textContent == "\n" || doc.documentElement.textContent == "", "Writing |\\n| failed");
+
+doc.open();
+doc.writeln(null);
+doc.close();
+is(doc.documentElement.textContent, "null\n", "Writing |null\\n| failed");
+
+doc.open();
+doc.writeln(null, null);
+doc.close();
+is(doc.documentElement.textContent, "nullnull\n", "Writing |null, null\\n| failed");
+
+doc.open();
+doc.writeln(undefined);
+doc.close();
+is(doc.documentElement.textContent, "undefined\n", "Writing |undefined\\n| failed");
+
+doc.open();
+doc.writeln(undefined, undefined);
+doc.close();
+is(doc.documentElement.textContent, "undefinedundefined\n", "Writing |undefined, undefined\\n| failed");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug481335.xhtml b/dom/html/test/test_bug481335.xhtml
new file mode 100644
index 0000000000..8fdd145222
--- /dev/null
+++ b/dom/html/test/test_bug481335.xhtml
@@ -0,0 +1,122 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=481335
+-->
+<head>
+ <title>Test for Bug 481335</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style type="text/css">
+ a { color:blue; }
+ a:visited { color:red; }
+ </style>
+ <base href="https://example.com/" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=481335">Mozilla Bug 481335</a>
+<p id="display">
+ <a id="t">A link</a>
+ <iframe id="i"></iframe>
+</p>
+<p id="newparent"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+<script type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 481335 **/
+SimpleTest.waitForExplicitFinish();
+var rand = Date.now() + "-" + Math.random();
+
+is($("t").href, "",
+ "Unexpected href before set");
+is($("t").href, "",
+ "Unexpected cached href before set");
+
+$("t").setAttribute("href", rand);
+is($("t").href, "https://example.com/" + rand,
+ "Unexpected href after set");
+is($("t").href, "https://example.com/" + rand,
+ "Unexpected cached href after set");
+const unvisitedColor = "rgb(0, 0, 255)";
+const visitedColor = "rgb(255, 0, 0)";
+
+let tests = testIterator();
+function continueTest() {
+ tests.next();
+}
+
+function checkLinkColor(aElmId, aExpectedColor, aMessage) {
+ // Because link coloring is asynchronous, we wait until we get the right
+ // result, or we will time out (resulting in a failure).
+ function getColor() {
+ var utils = SpecialPowers.getDOMWindowUtils(window);
+ return utils.getVisitedDependentComputedStyle($(aElmId), "", "color");
+ }
+ while (getColor() != aExpectedColor) {
+ requestIdleCallback(continueTest);
+ return false;
+ }
+ is(getColor(), aExpectedColor, aMessage);
+ return true;
+}
+
+let win;
+
+function* testIterator() {
+ // After first load
+ $("newparent").appendChild($("t"));
+ is($("t").href, "https://example.com/" + rand,
+ "Unexpected href after move");
+ is($("t").href, "https://example.com/" + rand,
+ "Unexpected cached href after move");
+ while (!checkLinkColor("t", unvisitedColor, "Should be unvisited now"))
+ yield undefined;
+
+ win.close();
+ win = window.open($("t").href, "_blank");
+
+ // After second load
+ while (!checkLinkColor("t", visitedColor, "Should be visited now"))
+ yield undefined;
+ $("t").pathname = rand;
+ while (!checkLinkColor("t", visitedColor,
+ "Should still be visited after setting pathname to its existing value")) {
+ yield undefined;
+ }
+
+ /* TODO uncomment this test with the landing of bug 534526. See
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=461199#c167
+ $("t").pathname += "x";
+ while (!checkLinkColor("t", unvisitedColor,
+ "Should not be visited after changing pathname")) {
+ yield undefined;
+ }
+ $("t").pathname = $("t").pathname;
+ while (!checkLinkColor("t", unvisitedColor,
+ "Should not be visited after setting unvisited pathname to existing value")) {
+ yield undefined;
+ }
+ */
+
+ win.close();
+ win = window.open($("t").href, "_blank");
+
+ // After third load
+ while (!checkLinkColor("t", visitedColor,
+ "Should be visited now after third load")) {
+ yield undefined;
+ }
+ win.close();
+ SimpleTest.finish();
+}
+
+addLoadEvent(function() {
+ win = window.open($("t").href, "_blank");
+ requestIdleCallback(continueTest);
+});
+]]>
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug481440.html b/dom/html/test/test_bug481440.html
new file mode 100644
index 0000000000..ab26b63e97
--- /dev/null
+++ b/dom/html/test/test_bug481440.html
@@ -0,0 +1,30 @@
+<!--Test must be in quirks mode for document.all to work-->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=481440
+-->
+<head>
+ <title>Test for Bug 481440</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=481440">Mozilla Bug 481440</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <input name="x" id="y">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 481440 **/
+// Do a bunch of getElementById calls to catch hashtables auto-going live
+for (var i = 0; i < 500; ++i) {
+ document.getElementById(i);
+}
+is(document.all.x, document.getElementById("y"),
+ "Unexpected node");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug481647.html b/dom/html/test/test_bug481647.html
new file mode 100644
index 0000000000..b74fb7997e
--- /dev/null
+++ b/dom/html/test/test_bug481647.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=481647
+-->
+<head>
+ <title>Test for Bug 481647</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=481647">Mozilla Bug 481647</a>
+<p id="display">
+ <iframe src="javascript:'aaa'"></iframe>
+ <iframe src="javascript:document.write('aaa'); document.close();"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 481647 **/
+SimpleTest.waitForExplicitFinish()
+
+function testFrame(num) {
+ is(window.frames[num].document.baseURI, document.baseURI,
+ "Unexpected base URI in frame " + num);
+}
+
+addLoadEvent(function() {
+ for (var i = 0; i < 2; ++i) {
+ testFrame(i);
+ }
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug482659.html b/dom/html/test/test_bug482659.html
new file mode 100644
index 0000000000..df2c66a747
--- /dev/null
+++ b/dom/html/test/test_bug482659.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=482659
+-->
+<head>
+ <title>Test for Bug 482659</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=482659">Mozilla Bug 482659</a>
+<p id="display">
+ <iframe></iframe>
+ <iframe src="about:blank"></iframe>
+ <iframe></iframe>
+ <iframe src="about:blank"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 482659 **/
+SimpleTest.waitForExplicitFinish()
+
+function testFrame(num) {
+ is(window.frames[num].document.baseURI, document.baseURI,
+ "Unexpected base URI in frame " + num);
+ is(window.frames[num].document.documentURI, "about:blank",
+ "Unexpected document URI in frame " + num);
+}
+
+function appendScript(doc) {
+ var s = doc.createElement("script");
+ s.textContent = "document.write('executed'); document.close()";
+ doc.body.appendChild(s);
+}
+
+function verifyScriptRan(num) {
+ is(window.frames[num].document.documentElement.textContent, "executed",
+ "write didn't happen in frame " + num);
+}
+
+addLoadEvent(function() {
+/* document.write part of test disabled due to bug 483818
+ appendScript(window.frames[2].document);
+ appendScript(window.frames[3].document);
+
+ verifyScriptRan(2);
+ verifyScriptRan(3);
+*/
+ for (var i = 0; i < 4; ++i) {
+ testFrame(i);
+ }
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug486741.html b/dom/html/test/test_bug486741.html
new file mode 100644
index 0000000000..a20cd44e5e
--- /dev/null
+++ b/dom/html/test/test_bug486741.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=486741
+-->
+<head>
+ <title>Test for Bug 486741</title>
+ <script type="application/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=486741">Mozilla Bug 486741</a>
+<p id="display"><iframe id="f"></iframe></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 486741 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var d = $("f").contentDocument;
+ var root = d.documentElement;
+ is(root.tagName, "HTML", "Unexpected root");
+
+ d.open();
+ isnot(d.documentElement, root, "Shouldn't have the old root element");
+
+ d.write("Test");
+ d.close();
+
+ isnot(d.documentElement, root, "Still shouldn't have the old root element");
+ is(d.documentElement.tagName, "HTML", "Unexpected new root after write");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug489532.html b/dom/html/test/test_bug489532.html
new file mode 100644
index 0000000000..ac28c35482
--- /dev/null
+++ b/dom/html/test/test_bug489532.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=489532
+-->
+<head>
+ <title>Test for Bug 489532</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=489532">Mozilla Bug 489532</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script>
+/** Test for Bug 489532 **/
+try {
+ document.createElement("<div>");
+ ok(false, "Should throw.")
+} catch (e) {
+ is(e.name, "InvalidCharacterError",
+ "Expected InvalidCharacterError.");
+ ok(e instanceof DOMException, "Expected DOMException.");
+ is(e.code, DOMException.INVALID_CHARACTER_ERR,
+ "Expected INVALID_CHARACTER_ERR.");
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug497242.xhtml b/dom/html/test/test_bug497242.xhtml
new file mode 100644
index 0000000000..943c46ddc9
--- /dev/null
+++ b/dom/html/test/test_bug497242.xhtml
@@ -0,0 +1,41 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=497242
+-->
+<head>
+ <title>Test for Bug 497242</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=497242">Mozilla Bug 497242</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form name="foo"/>
+ <form name="foo"/>
+ <form name="bar"/>
+ <form name="bar" xmlns=""/>
+</div>
+<pre id="test">
+<script type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 497242 **/
+is(document.getElementsByName("foo").length, 2,
+ "Should find both forms with name 'foo'");
+is(document.getElementsByName("foo")[0],
+ document.getElementsByTagName("form")[0],
+ "Unexpected first foo");
+is(document.getElementsByName("foo")[1],
+ document.getElementsByTagName("form")[1],
+ "Unexpected second foo");
+is(document.getElementsByName("bar").length, 1,
+ "Should find only the HTML form with name 'bar'");
+is(document.getElementsByName("bar")[0],
+ document.getElementsByTagName("form")[2],
+ "Unexpected bar");
+]]>
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug499092.html b/dom/html/test/test_bug499092.html
new file mode 100644
index 0000000000..d5d019bc54
--- /dev/null
+++ b/dom/html/test/test_bug499092.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=499092
+-->
+<head>
+ <title>Test for Bug 499092</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=499092">Mozilla Bug 499092</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 499092 **/
+SimpleTest.waitForExplicitFinish();
+var content = document.getElementById("content");
+
+function testHtml() {
+ is(this.contentDocument.title, "HTML OK");
+ SimpleTest.finish();
+}
+
+function testXml() {
+ is(this.contentDocument.title, "XML OK");
+ var iframeHtml = document.createElement("iframe");
+ iframeHtml.onload = testHtml;
+ iframeHtml.src = "bug499092.html";
+ content.appendChild(iframeHtml);
+}
+
+var iframeXml = document.createElement("iframe");
+iframeXml.onload = testXml;
+iframeXml.src = "bug499092.xml";
+content.appendChild(iframeXml);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug500885.html b/dom/html/test/test_bug500885.html
new file mode 100644
index 0000000000..3ab9225a4c
--- /dev/null
+++ b/dom/html/test/test_bug500885.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=500885
+-->
+<head>
+ <title>Test for Bug 500885</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/paint_listener.js"></script>
+ <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=500885">Mozilla Bug 500885</a>
+<div>
+ <input id="file" type="file" />
+</div>
+<script type="text/javascript">
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+async function test() {
+ // SpecialPowers.DOMWindowUtils doesn't appear to fire mouseEvents correctly
+ var wu = SpecialPowers.getDOMWindowUtils(window);
+
+ try {
+ var domActivateEvents;
+ var fileInput = document.getElementById("file");
+ var rect = fileInput.getBoundingClientRect();
+
+ fileInput.addEventListener ("DOMActivate", function (e) {
+ ok("detail" in e, "DOMActivate should have .detail");
+ is(e.detail, 1, ".detail should be 1");
+ domActivateEvents++;
+ });
+
+ fileInput.scrollIntoView({ behaviour: "smooth" });
+ await promiseApzFlushedRepaints();
+
+ domActivateEvents = 0;
+ wu.sendMouseEvent("mousedown", rect.left + 5, rect.top + 5, 0, 1, 0);
+ wu.sendMouseEvent("mouseup", rect.left + 5, rect.top + 5, 0, 1, 0);
+ is(domActivateEvents, 1, "click on button should fire 1 DOMActivate event");
+
+ domActivateEvents = 0;
+ wu.sendMouseEvent("mousedown", rect.right - 5, rect.top + 5, 0, 1, 0);
+ wu.sendMouseEvent("mouseup", rect.right - 5, rect.top + 5, 0, 1, 0);
+ is(domActivateEvents, 1, "click on text field should fire 1 DOMActivate event");
+ } finally {
+ SimpleTest.executeSoon(function() {
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+ });
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(test);
+
+</script>
+</body>
+
+</html>
diff --git a/dom/html/test/test_bug512367.html b/dom/html/test/test_bug512367.html
new file mode 100644
index 0000000000..35af18a5a1
--- /dev/null
+++ b/dom/html/test/test_bug512367.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=512367
+-->
+<head>
+ <title>Test for Bug 512367</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=512367">Mozilla Bug 512367</a>
+<p id="display">
+ <iframe src="bug369370-popup.png" id="i" style="width:200px; height:200px"></iframe>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+var frame = document.getElementById("i");
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ SpecialPowers.setFullZoom(frame.contentWindow, 1.5);
+
+ setTimeout(function() {
+ synthesizeMouse(frame, 30, 30, {});
+
+ is(SpecialPowers.getFullZoom(frame.contentWindow), 1.5, "Zoom in the image frame should not have been reset");
+
+ SimpleTest.finish();
+ }, 0);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug514856.html b/dom/html/test/test_bug514856.html
new file mode 100644
index 0000000000..77b8feecbd
--- /dev/null
+++ b/dom/html/test/test_bug514856.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=514856
+-->
+<head>
+ <title>Test for Bug 514856</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=514856">Mozilla Bug 514856</a>
+<p id="display"></p>
+<div id="content">
+ <iframe id="testFrame" src="bug514856_iframe.html"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 514856 **/
+
+function beginTest() {
+ var ifr = document.getElementById("testFrame");
+ var win = ifr.contentWindow;
+
+ // After the click, the load event should be fired.
+ ifr.addEventListener('load', function() {
+ testDone();
+ });
+
+ // synthesizeMouse adds getBoundingClientRect left and top to the offsets but
+ // in that particular case, we don't want that.
+ var rect = ifr.getBoundingClientRect();
+ var left = rect.left;
+ var top = rect.top;
+
+ synthesizeMouse(ifr, 10 - left, 10 - top, { type: "mousemove" }, win);
+ synthesizeMouse(ifr, 12 - left, 12 - top, { type: "mousemove" }, win);
+ synthesizeMouse(ifr, 14 - left, 14 - top, { type: "mousemove" }, win);
+ synthesizeMouse(ifr, 16 - left, 16 - top, { }, win);
+}
+
+function testDone() {
+ var ifr = document.getElementById("testFrame");
+ var url = new String(ifr.contentWindow.location);
+
+ is(url.indexOf("?10,10"), -1, "Shouldn't have ?10,10 in the URL!");
+ is(url.indexOf("?12,12"), -1, "Shouldn't have ?12,12 in the URL!");
+ is(url.indexOf("?14,14"), -1, "Shouldn't have ?14,14 in the URL!");
+ isnot(url.indexOf("?16,16"), -1, "Should have ?16,16 in the URL!");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(beginTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug518122.html b/dom/html/test/test_bug518122.html
new file mode 100644
index 0000000000..acb9d78d0a
--- /dev/null
+++ b/dom/html/test/test_bug518122.html
@@ -0,0 +1,126 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=518122
+-->
+<head>
+ <title>Test for Bug 518122</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=518122">Mozilla Bug 518122</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 518122 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTests);
+
+var simple_tests = [ ["foo", "foo"],
+ ["", ""],
+ [null, ""],
+ [undefined , "undefined"],
+ ["\n", "\n"],
+ ["\r", "\n"],
+ ["\rfoo", "\nfoo"],
+ ["foo\r", "foo\n"],
+ ["foo\rbar", "foo\nbar"],
+ ["foo\rbar\r", "foo\nbar\n"],
+ ["\r\n", "\n"],
+ ["\r\nfoo", "\nfoo"],
+ ["foo\r\n", "foo\n"],
+ ["foo\r\nbar", "foo\nbar"],
+ ["foo\r\nbar\r\n", "foo\nbar\n"] ];
+
+var value_append_tests = [ ["foo", "bar", "foobar"],
+ ["foo", "foo", "foofoo"],
+ ["foobar", "bar", "foobarbar"],
+ ["foobar", "foo", "foobarfoo"],
+ ["foo\n", "foo", "foo\nfoo"],
+ ["foo\r", "foo", "foo\nfoo"],
+ ["foo\r\n", "foo", "foo\nfoo"],
+ ["\n", "\n", "\n\n"],
+ ["\r", "\r", "\n\n"],
+ ["\r\n", "\r\n", "\n\n"],
+ ["\r", "\r\n", "\n\n"],
+ ["\r\n", "\r", "\n\n"],
+ [null, null, "null"],
+ [null, undefined, "undefined"],
+ ["", "", ""]
+ ];
+
+
+var simple_tests_for_input = [ ["foo", "foo"],
+ ["", ""],
+ [null, ""],
+ [undefined , "undefined"],
+ ["\n", ""],
+ ["\r", ""],
+ ["\rfoo", "foo"],
+ ["foo\r", "foo"],
+ ["foo\rbar", "foobar"],
+ ["foo\rbar\r", "foobar"],
+ ["\r\n", ""],
+ ["\r\nfoo", "foo"],
+ ["foo\r\n", "foo"],
+ ["foo\r\nbar", "foobar"],
+ ["foo\r\nbar\r\n", "foobar"] ];
+
+var value_append_tests_for_input = [ ["foo", "bar", "foobar"],
+ ["foo", "foo", "foofoo"],
+ ["foobar", "bar", "foobarbar"],
+ ["foobar", "foo", "foobarfoo"],
+ ["foo\n", "foo", "foofoo"],
+ ["foo\r", "foo", "foofoo"],
+ ["foo\r\n", "foo", "foofoo"],
+ ["\n", "\n", ""],
+ ["\r", "\r", ""],
+ ["\r\n", "\r\n", ""],
+ ["\r", "\r\n", ""],
+ ["\r\n", "\r", ""],
+ [null, null, "null"],
+ [null, undefined, "undefined"],
+ ["", "", ""]
+ ];
+function runTestsFor(el, simpleTests, appendTests) {
+ for(var i = 0; i < simpleTests.length; ++i) {
+ el.value = simpleTests[i][0];
+ is(el.value, simpleTests[i][1], "Wrong value (wrap=" + el.getAttribute('wrap') + ", simple_test=" + i + ")");
+ }
+ for (var j = 0; j < appendTests.length; ++j) {
+ el.value = appendTests[j][0];
+ el.value += appendTests[j][1];
+ is(el.value, appendTests[j][2], "Wrong value (wrap=" + el.getAttribute('wrap') + ", value_append_test=" + j + ")");
+ }
+}
+
+function runTests() {
+ var textareas = document.getElementsByTagName("textarea");
+ for (var i = 0; i < textareas.length; ++i) {
+ runTestsFor(textareas[i], simple_tests, value_append_tests);
+ }
+ var input = document.getElementsByTagName("input")[0];
+ runTestsFor(input, simple_tests_for_input, value_append_tests_for_input);
+ // initialize the editor
+ input.focus();
+ input.blur();
+ runTestsFor(input, simple_tests_for_input, value_append_tests_for_input);
+ SimpleTest.finish();
+}
+
+
+</script>
+</pre>
+<textarea cols="30" rows="7" wrap="none"></textarea>
+<textarea cols="30" rows="7" wrap="off"></textarea><br>
+<textarea cols="30" rows="7" wrap="soft"></textarea>
+<textarea cols="30" rows="7" wrap="hard"></textarea>
+<input type="text">
+</body>
+</html>
diff --git a/dom/html/test/test_bug519987.html b/dom/html/test/test_bug519987.html
new file mode 100644
index 0000000000..875368c9b0
--- /dev/null
+++ b/dom/html/test/test_bug519987.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=519987
+-->
+<head>
+ <title>Test for Bug 519987</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=519987">Mozilla Bug 519987</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 519987 **/
+var xmlns = 'http://www.w3.org/1999/xhtml';
+is((new Image()).namespaceURI, xmlns, "Unexpected namespace for new Image()");
+is((new Audio()).namespaceURI, xmlns, "Unexpected namespace for new Audio()");
+var titles = document.getElementsByTagName("title");
+var t = titles[0];
+t.remove();
+document.title = "abcdefg";
+is(titles[0].namespaceURI, xmlns, "Unexpected namespace for new <title>");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug523771.html b/dom/html/test/test_bug523771.html
new file mode 100644
index 0000000000..9f6af3de76
--- /dev/null
+++ b/dom/html/test/test_bug523771.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=523771
+-->
+<head>
+ <title>Test for Bug 523771</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=523771">Mozilla Bug 523771</a>
+<p id="display"></p>
+<iframe name="target_iframe" id="target_iframe"></iframe>
+<form action="form_submit_server.sjs" target="target_iframe" id="form"
+method="POST" enctype="multipart/form-data">
+ <input id=singleFile name=singleFile type=file>
+ <input id=multiFile name=multiFile type=file multiple>
+</form>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+singleFileInput = document.getElementById('singleFile');
+multiFileInput = document.getElementById('multiFile');
+var input1File = { name: "523771_file1", type: "", body: "file1 contents"};
+var input2Files =
+ [{ name: "523771_file2", type: "", body: "second file contents" },
+ { name: "523771_file3.txt", type: "text/plain", body: "123456" },
+ { name: "523771_file4.html", type: "text/html", body: "<html>content</html>" }
+ ];
+
+SimpleTest.waitForExplicitFinish();
+
+function setFileInputs () {
+ var f = createFileWithData(input1File.name, input1File.body, input1File.type);
+ SpecialPowers.wrap(singleFileInput).mozSetFileArray([f]);
+
+ var input2FileNames = [];
+ for (file of input2Files) {
+ f = createFileWithData(file.name, file.body, file.type);
+ input2FileNames.push(f);
+ }
+ SpecialPowers.wrap(multiFileInput).mozSetFileArray(input2FileNames);
+}
+
+function createFileWithData(fileName, fileData, fileType) {
+ return new File([fileData], fileName, { type: fileType });
+}
+
+function cleanupFiles() {
+ singleFileInput.value = "";
+ multiFileInput.value = "";
+}
+
+is(singleFileInput.files.length, 0, "single-file .files.length"); // bug 524421
+is(multiFileInput.files.length, 0, "multi-file .files.length"); // bug 524421
+
+setFileInputs();
+
+is(singleFileInput.multiple, false, "single-file input .multiple");
+is(multiFileInput.multiple, true, "multi-file input .multiple");
+is(singleFileInput.value, 'C:\\fakepath\\' + input1File.name, "single-file input .value");
+is(multiFileInput.value, 'C:\\fakepath\\' + input2Files[0].name, "multi-file input .value");
+is(singleFileInput.files[0].name, input1File.name, "single-file input .files[n].name");
+is(singleFileInput.files[0].size, input1File.body.length, "single-file input .files[n].size");
+is(singleFileInput.files[0].type, input1File.type, "single-file input .files[n].type");
+for(i = 0; i < input2Files.length; ++i) {
+ is(multiFileInput.files[i].name, input2Files[i].name, "multi-file input .files[n].name");
+ is(multiFileInput.files[i].size, input2Files[i].body.length, "multi-file input .files[n].size");
+ is(multiFileInput.files[i].type, input2Files[i].type, "multi-file input .files[n].type");
+}
+
+document.getElementById('form').submit();
+iframe = document.getElementById('target_iframe');
+iframe.onload = function() {
+ response = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ is(response[0].headers["Content-Disposition"],
+ "form-data; name=\"singleFile\"; filename=\"" + input1File.name +
+ "\"",
+ "singleFile Content-Disposition");
+ is(response[0].headers["Content-Type"], input1File.type || "application/octet-stream",
+ "singleFile Content-Type");
+ is(response[0].body, input1File.body, "singleFile body");
+
+ for(i = 0; i < input2Files.length; ++i) {
+ is(response[i + 1].headers["Content-Disposition"],
+ "form-data; name=\"multiFile\"; filename=\"" + input2Files[i].name +
+ "\"",
+ "multiFile Content-Disposition");
+ is(response[i + 1].headers["Content-Type"], input2Files[i].type || "application/octet-stream",
+ "multiFile Content-Type");
+ is(response[i + 1].body, input2Files[i].body, "multiFile body");
+ }
+
+ cleanupFiles();
+
+ is(singleFileInput.files.length, 0, "single-file .files.length"); // bug 524421
+ is(multiFileInput.files.length, 0, "multi-file .files.length"); // bug 524421
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug529819.html b/dom/html/test/test_bug529819.html
new file mode 100644
index 0000000000..4147a341b2
--- /dev/null
+++ b/dom/html/test/test_bug529819.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=529819
+-->
+<head>
+ <title>Test for Bug 529819</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529819">Mozilla Bug 529819</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<form id="form">
+ <input name="foo" id="foo">
+ <input name="bar" type="radio">
+ <input name="bar" id="bar" type="radio">
+</form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 529819 **/
+is($("form").elements.foo instanceof HTMLInputElement, true, "Should have an element here");
+is($("form").elements.bar instanceof HTMLInputElement, false, "Should have a list here");
+is($("form").elements.bar.length, 2, "Should have a list with two elements here");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug529859.html b/dom/html/test/test_bug529859.html
new file mode 100644
index 0000000000..9d607c8a22
--- /dev/null
+++ b/dom/html/test/test_bug529859.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=529859
+-->
+<head>
+ <title>Test for Bug 529859</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529859">Mozilla Bug 529859</a>
+<div id="content">
+ <iframe name="target_iframe" id="target_iframe"></iframe>
+ <form action="form_submit_server.sjs" target="target_iframe" id="form"
+ method="POST" enctype="multipart/form-data">
+ <input id="emptyFileInput" name="emptyFileInput" type="file">
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 529859 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ $("target_iframe").onload = function() {
+ var response = JSON.parse(this.contentDocument.documentElement.textContent);
+ is(response.length, 1, "Unexpected number of inputs");
+ is(response[0].headers["Content-Disposition"],
+ "form-data; name=\"emptyFileInput\"; filename=\"\"",
+ "Incorrect content-disposition");
+ is(response[0].headers["Content-Type"], "application/octet-stream",
+ "Unexpected content-type");
+ SimpleTest.finish();
+ }
+ $("form").submit();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug535043.html b/dom/html/test/test_bug535043.html
new file mode 100644
index 0000000000..3eb046b3c5
--- /dev/null
+++ b/dom/html/test/test_bug535043.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=535043
+-->
+<head>
+ <title>Test for Bug 535043</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=535043">Mozilla Bug 535043</a>
+<p id="display"></p>
+<div id="content">
+ <textarea></textarea>
+ <textarea maxlength="-1"></textarea>
+ <textarea maxlength="0"></textarea>
+ <textarea maxlength="2"></textarea>
+</div>
+<pre id="test">
+<script type="text/javascript">
+
+/** Test for Bug 535043 **/
+function checkTextArea(textArea) {
+ textArea.value = '';
+ textArea.focus();
+ for (var j = 0; j < 3; j++) {
+ sendString("x");
+ }
+ var htmlMaxLength = textArea.getAttribute('maxlength');
+ var domMaxLength = textArea.maxLength;
+ if (htmlMaxLength == null) {
+ is(domMaxLength, -1,
+ 'maxlength is unset but maxLength DOM attribute is not -1');
+ } else if (htmlMaxLength < 0) {
+ // Per the HTML5 spec, out-of-range values are supposed to translate to -1,
+ // not 0, but they don't?
+ is(domMaxLength, -1,
+ 'maxlength is out of range but maxLength DOM attribute is not -1');
+ } else {
+ is(domMaxLength, parseInt(htmlMaxLength),
+ 'maxlength in DOM does not match provided value');
+ }
+ if (textArea.maxLength == -1) {
+ is(textArea.value.length, 3,
+ 'textarea with maxLength -1 should have no length limit');
+ } else {
+ is(textArea.value.length, textArea.maxLength, 'textarea has maxLength ' +
+ textArea.maxLength + ' but length ' + textArea.value.length );
+ }
+}
+
+SimpleTest.waitForFocus(function() {
+ var textAreas = document.getElementsByTagName('textarea');
+ for (var i = 0; i < textAreas.length; i++) {
+ checkTextArea(textAreas[i]);
+ }
+
+ textArea = textAreas[0];
+ testNums = [-42, -1, 0, 2];
+ for (var i = 0; i < testNums.length; i++) {
+ textArea.removeAttribute('maxlength');
+
+ var caught = false;
+ try {
+ textArea.maxLength = testNums[i];
+ } catch (e) {
+ caught = true;
+ }
+ if (testNums[i] < 0) {
+ ok(caught, 'Setting negative maxLength should throw exception');
+ } else {
+ ok(!caught, 'Setting nonnegative maxLength should not throw exception');
+ }
+ checkTextArea(textArea);
+
+ textArea.setAttribute('maxlength', testNums[i]);
+ checkTextArea(textArea);
+ }
+
+ SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug536891.html b/dom/html/test/test_bug536891.html
new file mode 100644
index 0000000000..89bb93d1b0
--- /dev/null
+++ b/dom/html/test/test_bug536891.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=536891
+-->
+<head>
+ <title>Test for Bug 536891</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=536891">Mozilla Bug 536891</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<textarea id="t" maxlength="-2" minlength="-2"></textarea>
+<input id="i" type="text" maxlength="-2" minlength="-2">
+<input id="p" type="password" maxlength="-2" minlength="-2">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 536891 **/
+
+function checkNegativeMinMaxLength(element)
+{
+ for(let type of ["min", "max"]) {
+ /* value is set to -2 initially in the document, see above */
+ is(element[type + "Length"], -1, "negative " + type + "Length should be considered invalid and represented as -1");
+
+ // changing the property to an negative value should throw (see bug 536895).
+ for(let value of [-15, -2147483648]) { // PR_INT32_MIN
+ let threw = false;
+ try {
+ element[type + "Length"] = value;
+ } catch(e) {
+ threw = true;
+ }
+ is(threw, true, "setting " + type + "Length property to " + value + " should throw");
+ }
+ element[type + "Length"] = "non-numerical value";
+ is(element[type + "Length"], 0, "setting " + type + "Length property to a non-numerical value should set it to zero");
+
+
+ element.setAttribute(type + 'Length', -15);
+ is(element[type + "Length"], -1, "negative " + type + "Length is not processed correctly when set dynamically");
+ is(element.getAttribute(type + 'Length'), "-15", type + "Length attribute doesn't return the correct value");
+
+ element.setAttribute(type + 'Length', 0);
+ is(element[type + "Length"], 0, "zero " + type + "Length is not processed correctly");
+ element.setAttribute(type + 'Length', 2147483647); // PR_INT32_MAX
+ is(element[type + "Length"], 2147483647, "negative " + type + "Length is not processed correctly");
+ element.setAttribute(type + 'Length', -2147483648); // PR_INT32_MIN
+ is(element[type + "Length"], -1, "negative " + type + "Length is not processed correctly");
+ element.setAttribute(type + 'Length', 'non-numerical-value');
+ is(element[type + "Length"], -1, "non-numerical value should be considered invalid and represented as -1");
+ }
+}
+
+/* TODO: correct behavior may be checked for email, telephone, url and search input types */
+checkNegativeMinMaxLength(document.getElementById('t'));
+checkNegativeMinMaxLength(document.getElementById('i'));
+checkNegativeMinMaxLength(document.getElementById('p'));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug536895.html b/dom/html/test/test_bug536895.html
new file mode 100644
index 0000000000..c135432260
--- /dev/null
+++ b/dom/html/test/test_bug536895.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=536895
+-->
+<head>
+ <title>Test for Bug 536895</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=536895">Mozilla Bug 536895</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<textarea id="t"></textarea>
+<input id="i" type="text">
+<input id="p" type="password">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 536895 **/
+
+function checkNegativeMaxLengthException(element)
+{
+ caught = false;
+ try {
+ element.setAttribute('maxLength', -10);
+ } catch(e) {
+ caught = true;
+ }
+ ok(!caught, "Setting maxLength attribute to a negative value shouldn't throw an exception");
+
+ caught = false;
+ try {
+ element.maxLength = -20;
+ } catch(e) {
+ is(e.name, "IndexSizeError", "Should be an IndexSizeError exception");
+ caught = true;
+ }
+ ok(caught, "Setting negative maxLength from the DOM should throw an exception");
+
+ is(element.getAttribute('maxLength'), "-10", "When the exception is raised, the maxLength attribute shouldn't change");
+}
+
+/* TODO: correct behavior may be checked for email, telephone, url and search input types */
+checkNegativeMaxLengthException(document.getElementById('t'));
+checkNegativeMaxLengthException(document.getElementById('i'));
+checkNegativeMaxLengthException(document.getElementById('p'));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug546995.html b/dom/html/test/test_bug546995.html
new file mode 100644
index 0000000000..ff4d80ec45
--- /dev/null
+++ b/dom/html/test/test_bug546995.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=546995
+-->
+<head>
+ <title>Test for Bug 546995</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=546995">Mozilla Bug 546995</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <select id='s'></select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 546995 **/
+
+/* This test in only testing IDL reflection, another one is testing the behavior */
+
+function checkAutofocusIDLAttribute(element)
+{
+ ok('autofocus' in element, "Element has the autofocus IDL attribute");
+ ok(!element.autofocus, "autofocus default value is false");
+ element.setAttribute('autofocus', 'autofocus');
+ ok(element.autofocus, "autofocus should be enabled");
+ element.removeAttribute('autofocus');
+ ok(!element.autofocus, "autofocus should be disabled");
+}
+
+// TODO: keygen should be added when correctly implemented, see bug 101019.
+checkAutofocusIDLAttribute(document.getElementById('s'));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug547850.html b/dom/html/test/test_bug547850.html
new file mode 100644
index 0000000000..a2e0323ec8
--- /dev/null
+++ b/dom/html/test/test_bug547850.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=547850
+-->
+<head>
+ <title>Test for Bug 547850</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=547850">Mozilla Bug 547850</a>
+<script>
+document.write("<div id=content><f\u00c5></f\u00c5><r\u00e5></r\u00e5>");
+document.write("<span g\u00c5=a1 t\u00e5=a2></span></div>");
+</script>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var ch = $('content').childNodes;
+is(ch[0].localName, "f\u00c5", "upper case localName");
+is(ch[1].localName, "r\u00e5", "lower case localName");
+is(ch[0].nodeName, "F\u00c5", "upper case nodeName");
+is(ch[1].nodeName, "R\u00e5", "lower case nodeName");
+is(ch[0].tagName, "F\u00c5", "upper case tagName");
+is(ch[1].tagName, "R\u00e5", "lower case tagName");
+is(ch[2].getAttribute("g\u00c5"), "a1", "upper case attr name");
+is(ch[2].getAttribute("t\u00e5"), "a2", "lower case attr name");
+is(ch[2].getAttribute("G\u00c5"), "a1", "upper case attr name");
+is(ch[2].getAttribute("T\u00e5"), "a2", "lower case attr name");
+is(ch[2].getAttribute("g\u00e5"), null, "wrong lower case attr name");
+is(ch[2].getAttribute("t\u00c5"), null, "wrong upper case attr name");
+is($('content').getElementsByTagName("f\u00c5")[0], ch[0], "gEBTN upper case");
+is($('content').getElementsByTagName("f\u00c5").length, 1, "gEBTN upper case length");
+is($('content').getElementsByTagName("r\u00e5")[0], ch[1], "gEBTN lower case");
+is($('content').getElementsByTagName("r\u00e5").length, 1, "gEBTN lower case length");
+is($('content').getElementsByTagName("F\u00c5")[0], ch[0], "gEBTN upper case");
+is($('content').getElementsByTagName("F\u00c5").length, 1, "gEBTN upper case length");
+is($('content').getElementsByTagName("R\u00e5")[0], ch[1], "gEBTN lower case");
+is($('content').getElementsByTagName("R\u00e5").length, 1, "gEBTN lower case length");
+is($('content').getElementsByTagName("f\u00e5").length, 0, "gEBTN wrong upper case");
+is($('content').getElementsByTagName("r\u00c5").length, 0, "gEBTN wrong lower case");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug551846.html b/dom/html/test/test_bug551846.html
new file mode 100644
index 0000000000..4950b1e452
--- /dev/null
+++ b/dom/html/test/test_bug551846.html
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=551846
+-->
+<head>
+ <title>Test for Bug 551846</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=551846">Mozilla Bug 551846</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <select id='s'>
+ <option>Tulip</option>
+ <option>Lily</option>
+ <option>Gagea</option>
+ <option>Snowflake</option>
+ <option>Ismene</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 551846 **/
+
+function checkSizeReflection(element, defaultValue)
+{
+ is(element.size, defaultValue, "Default size should be " + defaultValue);
+
+ element.setAttribute('size', -15);
+ is(element.size, defaultValue,
+ "The reflecting IDL attribute should return the default value when content attribute value is invalid");
+ is(element.getAttribute('size'), "-15",
+ "The content attribute should containt the previously set value");
+
+ element.setAttribute('size', 0);
+ is(element.size, 0,
+ "0 should be considered as a valid value");
+ is(element.getAttribute('size'), "0",
+ "The content attribute should containt the previously set value");
+
+ element.setAttribute('size', 2147483647); /* PR_INT32_MAX */
+ is(element.size, 2147483647,
+ "PR_INT32_MAX should be considered as a valid value");
+ is(element.getAttribute('size'), "2147483647",
+ "The content attribute should containt the previously set value");
+
+ element.setAttribute('size', -2147483648); /* PR_INT32_MIN */
+ is(element.size, defaultValue,
+ "The reflecting IDL attribute should return the default value when content attribute value is invalid");
+ is(element.getAttribute('size'), "-2147483648",
+ "The content attribute should containt the previously set value");
+
+ element.setAttribute('size', 'non-numerical-value');
+ is(element.size, defaultValue,
+ "The reflecting IDL attribute should return the default value when content attribute value is invalid");
+ is(element.getAttribute('size'), 'non-numerical-value',
+ "The content attribute should containt the previously set value");
+
+ element.setAttribute('size', 4294967294); /* PR_INT32_MAX * 2 */
+ is(element.size, defaultValue,
+ "Value greater than PR_INT32_MAX should be considered as invalid");
+ is(element.getAttribute('size'), "4294967294",
+ "The content attribute should containt the previously set value");
+
+ element.setAttribute('size', -4294967296); /* PR_INT32_MIN * 2 */
+ is(element.size, defaultValue,
+ "The reflecting IDL attribute should return the default value when content attribute value is invalid");
+ is(element.getAttribute('size'), "-4294967296",
+ "The content attribute should containt the previously set value");
+
+ element.size = defaultValue + 1;
+ element.removeAttribute('size');
+ is(element.size, defaultValue,
+ "When the attribute is removed, the size should be the default size");
+
+ element.setAttribute('size', 'foobar');
+ is(element.size, defaultValue,
+ "The reflecting IDL attribute should return the default value when content attribute value is invalid");
+ element.removeAttribute('size');
+ is(element.size, defaultValue,
+ "When the attribute is removed, the size should be the default size");
+}
+
+function checkSetSizeException(element)
+{
+ var caught = false;
+
+ try {
+ element.size = 1;
+ } catch(e) {
+ caught = true;
+ }
+ ok(!caught, "Setting a positive size shouldn't throw an exception");
+
+ caught = false;
+ try {
+ element.size = 0;
+ } catch(e) {
+ caught = true;
+ }
+ ok(!caught, "Setting a size to 0 from the IDL shouldn't throw an exception");
+
+ element.size = 1;
+
+ caught = false;
+ try {
+ element.size = -1;
+ } catch(e) {
+ caught = true;
+ }
+ ok(!caught, "Setting a negative size from the IDL shouldn't throw an exception");
+
+ is(element.size, 0, "The size should now be equal to the minimum non-negative value");
+
+ caught = false;
+ try {
+ element.setAttribute('size', -10);
+ } catch(e) {
+ caught = true;
+ }
+ ok(!caught, "Setting an invalid size in the content attribute shouldn't throw an exception");
+
+ // reverting to defalut
+ element.removeAttribute('size');
+}
+
+function checkSizeWhenChangeMultiple(element, aDefaultNonMultiple, aDefaultMultiple)
+{
+ s.setAttribute('size', -1)
+ is(s.size, aDefaultNonMultiple, "Size IDL attribute should be 1");
+
+ s.multiple = true;
+ is(s.size, aDefaultMultiple, "Size IDL attribute should be 4");
+
+ is(s.getAttribute('size'), "-1", "Size content attribute should be -1");
+
+ s.setAttribute('size', -2);
+ is(s.size, aDefaultMultiple, "Size IDL attribute should be 4");
+
+ s.multiple = false;
+ is(s.size, aDefaultNonMultiple, "Size IDL attribute should be 1");
+
+ is(s.getAttribute('size'), "-2", "Size content attribute should be -2");
+}
+
+var s = document.getElementById('s');
+
+checkSizeReflection(s, 0);
+checkSetSizeException(s);
+
+s.setAttribute('multiple', 'true');
+checkSizeReflection(s, 0);
+checkSetSizeException(s);
+s.removeAttribute('multiple');
+
+checkSizeWhenChangeMultiple(s, 0, 0);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug555567.html b/dom/html/test/test_bug555567.html
new file mode 100644
index 0000000000..0857955275
--- /dev/null
+++ b/dom/html/test/test_bug555567.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=555567
+-->
+<head>
+ <title>Test for Bug 555567</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=555567">Mozilla Bug 555567</a>
+<div id='content' style="display: none">
+ <form>
+ <fieldset>
+ <legend id="a"></legend>
+ </fieldset>
+ <legend id="b"></legend>
+ </form>
+ <legend id="c"></legend>
+</div>
+<pre id="test">
+<p id="display"></p>
+<script type="application/javascript">
+
+/** Test for Bug 555567 **/
+
+var a = document.getElementById('a');
+var b = document.getElementById('b');
+var c = document.getElementById('c');
+
+isnot(a.form, null,
+ "First legend element should have a not null form IDL attribute");
+is(b.form, null,
+ "Second legend element should have a null form IDL attribute");
+is(c.form, null,
+ "Third legend element should have a null form IDL attribute");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug556645.html b/dom/html/test/test_bug556645.html
new file mode 100644
index 0000000000..3c308f9ef6
--- /dev/null
+++ b/dom/html/test/test_bug556645.html
@@ -0,0 +1,73 @@
+<html>
+<head>
+ <title>Test for Bug 556645 and Bug 1848196</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ const object = document.createElement("object");
+ object.setAttribute("type", "text/html");
+ object.setAttribute("width", "200");
+ object.setAttribute("height", "200");
+ document.body.appendChild(object);
+ const promiseLoadObject = new Promise(resolve => {
+ object.addEventListener("load", resolve, {once: true});
+ });
+ object.setAttribute("data", "object_bug556645.html");
+ await promiseLoadObject;
+ runTest(object);
+ object.remove();
+
+ const embed = document.createElement("embed");
+ embed.setAttribute("type", "text/html");
+ embed.setAttribute("width", "200");
+ embed.setAttribute("height", "200");
+ document.body.appendChild(embed);
+ const promiseLoadEmbed = new Promise(resolve => {
+ embed.addEventListener("load", resolve, {once: true});
+ });
+ embed.setAttribute("src", "object_bug556645.html");
+ await promiseLoadEmbed;
+ runTest(embed);
+ embed.remove();
+
+ SimpleTest.finish();
+});
+
+function runTest(aObjectOrEmbed)
+{
+ const desc = `<${aObjectOrEmbed.tagName.toLowerCase()}>`;
+ const childDoc = aObjectOrEmbed.contentDocument || aObjectOrEmbed.getSVGDocument();
+ const body = childDoc.body;
+ is(document.activeElement, document.body, `${desc}: focus in parent before`);
+ is(childDoc.activeElement, body, `${desc}: focus in child before`);
+
+ const button = childDoc.querySelector("button");
+ button.focus();
+ childDoc.defaultView.focus();
+ is(document.activeElement, aObjectOrEmbed, `${desc}: focus in parent after focus()`);
+ is(childDoc.activeElement, button, `${desc}: focus in child after focus()`);
+
+ button.blur();
+ const pbutton = document.getElementById("pbutton");
+ pbutton.focus();
+
+ synthesizeKey("KEY_Tab");
+ is(document.activeElement, aObjectOrEmbed, `${desc}: focus in parent after tab`);
+ is(childDoc.activeElement, childDoc.documentElement, `${desc}: focus in child after tab`);
+
+ synthesizeKey("KEY_Tab");
+ is(document.activeElement, aObjectOrEmbed, `${desc}: focus in parent after tab 2`);
+ is(childDoc.activeElement, button, `${desc}: focus in child after tab 2`);
+}
+
+</script>
+
+<button id="pbutton">Parent</button>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug557087-1.html b/dom/html/test/test_bug557087-1.html
new file mode 100644
index 0000000000..9bd2068e8d
--- /dev/null
+++ b/dom/html/test/test_bug557087-1.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=557087
+-->
+<head>
+ <title>Test for Bug 557087</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script>
+
+/** Test for Bug 557087 **/
+
+function checkDisabledAttribute(aFieldset)
+{
+ ok('disabled' in aFieldset,
+ "fieldset elements should have the disabled attribute");
+
+ ok(!aFieldset.disabled,
+ "fieldset elements disabled attribute should be disabled");
+ is(aFieldset.getAttribute('disabled'), null,
+ "fieldset elements disabled attribute should be disabled");
+
+ aFieldset.disabled = true;
+ ok(aFieldset.disabled,
+ "fieldset elements disabled attribute should be enabled");
+ isnot(aFieldset.getAttribute('disabled'), null,
+ "fieldset elements disabled attribute should be enabled");
+
+ aFieldset.removeAttribute('disabled');
+ aFieldset.setAttribute('disabled', '');
+ ok(aFieldset.disabled,
+ "fieldset elements disabled attribute should be enabled");
+ isnot(aFieldset.getAttribute('disabled'), null,
+ "fieldset elements disabled attribute should be enabled");
+
+ aFieldset.removeAttribute('disabled');
+ ok(!aFieldset.disabled,
+ "fieldset elements disabled attribute should be disabled");
+ is(aFieldset.getAttribute('disabled'), null,
+ "fieldset elements disabled attribute should be disabled");
+}
+
+function checkDisabledPseudoClass(aFieldset)
+{
+ is(document.querySelector(":disabled"), null,
+ "no elements should have :disabled applied to them");
+
+ aFieldset.disabled = true;
+ is(document.querySelector(":disabled"), aFieldset,
+ ":disabled should apply to fieldset elements");
+
+ aFieldset.disabled = false;
+ is(document.querySelector(":disabled"), null,
+ "no elements should have :disabled applied to them");
+}
+
+function checkEnabledPseudoClass(aFieldset)
+{
+ is(document.querySelector(":enabled"), aFieldset,
+ ":enabled should apply to fieldset elements");
+
+ aFieldset.disabled = true;
+ is(document.querySelector(":enabled"), null,
+ "no elements should have :enabled applied to them");
+
+ aFieldset.disabled = false;
+ is(document.querySelector(":enabled"), aFieldset,
+ ":enabled should apply to fieldset elements");
+}
+
+function checkFocus(aFieldset)
+{
+ aFieldset.disabled = true;
+ aFieldset.setAttribute('tabindex', 1);
+
+ aFieldset.focus();
+
+ isnot(document.activeElement, aFieldset,
+ "fieldset can't be focused when disabled");
+ aFieldset.removeAttribute('tabindex');
+ aFieldset.disabled = false;
+}
+
+function checkClickEvent(aFieldset)
+{
+ var clickHandled = false;
+
+ aFieldset.disabled = true;
+
+ aFieldset.addEventListener("click", function(aEvent) {
+ clickHandled = true;
+ }, {once: true});
+
+ sendMouseEvent({type:'click'}, aFieldset);
+ SimpleTest.executeSoon(function() {
+ ok(clickHandled, "When disabled, fieldset should not prevent click events");
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({
+ set: [["dom.forms.fieldset_disable_only_descendants.enabled", true]]
+}).then(() => {
+ var fieldset = document.createElement("fieldset");
+ var content = document.getElementById('content');
+ content.appendChild(fieldset);
+
+ checkDisabledAttribute(fieldset);
+ checkDisabledPseudoClass(fieldset);
+ checkEnabledPseudoClass(fieldset);
+ checkFocus(fieldset);
+ checkClickEvent(fieldset);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug557087-2.html b/dom/html/test/test_bug557087-2.html
new file mode 100644
index 0000000000..435e924f84
--- /dev/null
+++ b/dom/html/test/test_bug557087-2.html
@@ -0,0 +1,363 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=557087
+-->
+<head>
+ <title>Test for Bug 557087</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a>
+<p id="display"></p>
+<div id="content" style="display:none;">
+</div>
+<pre id="test">
+<script>
+
+/** Test for Bug 557087 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var elementsPreventingClick = [ "input", "button", "select", "textarea" ];
+var elementsWithClick = [ "option", "optgroup", "output", "label", "object", "fieldset" ];
+var gHandled = 0;
+
+function clickShouldNotHappenHandler(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldNotHappenHandler);
+ ok(false, "click event should be prevented! (test1)");
+ if (++gHandled >= elementsWithClick.length) {
+ test2();
+ }
+}
+
+function clickShouldNotHappenHandler2(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldNotHappenHandler3);
+ ok(false, "click event should be prevented! (test2)");
+ if (++gHandled >= elementsWithClick.length) {
+ test3();
+ }
+}
+
+function clickShouldNotHappenHandler5(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldNotHappenHandler5);
+ ok(false, "click event should be prevented! (test5)");
+ if (++gHandled >= elementsWithClick.length) {
+ test6();
+ }
+}
+
+function clickShouldNotHappenHandler7(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldNotHappenHandler7);
+ ok(false, "click event should be prevented! (test7)");
+ if (++gHandled >= elementsWithClick.length) {
+ test8();
+ }
+}
+
+function clickShouldHappenHandler(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldHappenHandler);
+ ok(true, "click event has been correctly received (test1)");
+ if (++gHandled >= elementsWithClick.length) {
+ test2();
+ }
+}
+
+function clickShouldHappenHandler2(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldHappenHandler2);
+ ok(true, "click event has been correctly received (test2)");
+ if (++gHandled >= elementsWithClick.length) {
+ test3();
+ }
+}
+
+function clickShouldHappenHandler3(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldHappenHandler3);
+ ok(true, "click event has been correctly received (test3)");
+ if (++gHandled >= (elementsWithClick.length +
+ elementsPreventingClick.length)) {
+ test4();
+ }
+}
+
+function clickShouldHappenHandler4(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldHappenHandler4);
+ ok(true, "click event has been correctly received (test4)");
+ if (++gHandled >= (elementsWithClick.length +
+ elementsPreventingClick.length)) {
+ test5();
+ }
+}
+
+function clickShouldHappenHandler5(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldHappenHandler5);
+ ok(true, "click event has been correctly received (test5)");
+ if (++gHandled >= elementsWithClick.length) {
+ test6();
+ }
+}
+
+function clickShouldHappenHandler6(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldHappenHandler6);
+ ok(true, "click event has been correctly received (test6)");
+ if (++gHandled >= (elementsWithClick.length +
+ elementsPreventingClick.length)) {
+ test7();
+ }
+}
+
+function clickShouldHappenHandler7(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldHappenHandler7);
+ ok(true, "click event has been correctly received (test5)");
+ if (++gHandled >= elementsWithClick.length) {
+ test8();
+ }
+}
+
+function clickShouldHappenHandler8(aEvent)
+{
+ aEvent.target.removeEventListener("click", clickShouldHappenHandler8);
+ ok(true, "click event has been correctly received (test8)");
+ if (++gHandled >= (elementsWithClick.length +
+ elementsPreventingClick.length)) {
+ SimpleTest.finish();
+ }
+}
+
+var fieldset1 = document.createElement("fieldset");
+var fieldset2 = document.createElement("fieldset");
+var legendA = document.createElement("legend");
+var legendB = document.createElement("legend");
+var content = document.getElementById('content');
+fieldset1.disabled = true;
+content.appendChild(fieldset1);
+fieldset1.appendChild(fieldset2);
+
+function clean()
+{
+ var count = fieldset2.children.length;
+ for (var i=0; i<count; ++i) {
+ if (fieldset2.children[i] != legendA &&
+ fieldset2.children[i] != legendB) {
+ fieldset2.removeChild(fieldset2.children[i]);
+ }
+ }
+}
+
+function test1()
+{
+ gHandled = 0;
+
+ // Initialize children without click expected.
+ for (var name of elementsPreventingClick) {
+ var element = document.createElement(name);
+ fieldset2.appendChild(element);
+ element.addEventListener("click", clickShouldNotHappenHandler);
+ sendMouseEvent({type:'click'}, element);
+ }
+
+ // Initialize children with click expected.
+ for (var name of elementsWithClick) {
+ var element = document.createElement(name);
+ fieldset2.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler);
+ sendMouseEvent({type:'click'}, element);
+ }
+}
+
+function test2()
+{
+ gHandled = 0;
+ fieldset1.disabled = false;
+ fieldset2.disabled = true;
+
+ // Initialize children without click expected.
+ for (var name of elementsPreventingClick) {
+ var element = document.createElement(name);
+ fieldset2.appendChild(element);
+ element.addEventListener("click", clickShouldNotHappenHandler2);
+ sendMouseEvent({type:'click'}, element);
+ }
+
+ // Initialize children with click expected.
+ for (var name of elementsWithClick) {
+ var element = document.createElement(name);
+ fieldset2.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler2);
+ sendMouseEvent({type:'click'}, element);
+ }
+}
+
+function test3()
+{
+ gHandled = 0;
+ fieldset1.disabled = false;
+ fieldset2.disabled = false;
+
+ // All elements should accept the click.
+ for (var name of elementsPreventingClick) {
+ var element = document.createElement(name);
+ fieldset2.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler3);
+ sendMouseEvent({type:'click'}, element);
+ }
+
+ // Initialize children with click expected.
+ for (var name of elementsWithClick) {
+ var element = document.createElement(name);
+ fieldset2.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler3);
+ sendMouseEvent({type:'click'}, element);
+ }
+}
+
+function test4()
+{
+ gHandled = 0;
+ fieldset1.disabled = false;
+ fieldset2.disabled = true;
+
+ fieldset2.appendChild(legendA);
+
+ // All elements should accept the click.
+ for (var name of elementsPreventingClick) {
+ var element = document.createElement(name);
+ legendA.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler4);
+ sendMouseEvent({type:'click'}, element);
+ }
+
+ // Initialize children with click expected.
+ for (var name of elementsWithClick) {
+ var element = document.createElement(name);
+ legendA.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler4);
+ sendMouseEvent({type:'click'}, element);
+ }
+}
+
+function test5()
+{
+ gHandled = 0;
+ fieldset2.insertBefore(legendB, legendA);
+
+ // Initialize children without click expected.
+ for (var name of elementsPreventingClick) {
+ var element = document.createElement(name);
+ legendA.appendChild(element);
+ element.addEventListener("click", clickShouldNotHappenHandler5);
+ sendMouseEvent({type:'click'}, element);
+ }
+
+ // Initialize children with click expected.
+ for (var name of elementsWithClick) {
+ var element = document.createElement(name);
+ legendA.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler5);
+ sendMouseEvent({type:'click'}, element);
+ }
+}
+
+function test6()
+{
+ gHandled = 0;
+ fieldset2.removeChild(legendB);
+ fieldset1.disabled = true;
+ fieldset2.disabled = false;
+
+ fieldset1.appendChild(legendA);
+ legendA.appendChild(fieldset2);
+
+ // All elements should accept the click.
+ for (var name of elementsPreventingClick) {
+ var element = document.createElement(name);
+ fieldset2.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler6);
+ sendMouseEvent({type:'click'}, element);
+ }
+
+ // Initialize children with click expected.
+ for (var name of elementsWithClick) {
+ var element = document.createElement(name);
+ fieldset2.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler6);
+ sendMouseEvent({type:'click'}, element);
+ }
+}
+
+function test7()
+{
+ gHandled = 0;
+ fieldset1.disabled = true;
+ fieldset2.disabled = false;
+
+ fieldset1.appendChild(fieldset2);
+ fieldset2.appendChild(legendA);
+
+ // All elements should accept the click.
+ for (var name of elementsPreventingClick) {
+ var element = document.createElement(name);
+ legendA.appendChild(element);
+ element.addEventListener("click", clickShouldNotHappenHandler7);
+ sendMouseEvent({type:'click'}, element);
+ }
+
+ // Initialize children with click expected.
+ for (var name of elementsWithClick) {
+ var element = document.createElement(name);
+ legendA.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler7);
+ sendMouseEvent({type:'click'}, element);
+ }
+}
+
+function test8()
+{
+ gHandled = 0;
+ fieldset1.disabled = true;
+ fieldset2.disabled = true;
+
+ fieldset1.appendChild(legendA);
+ legendA.appendChild(fieldset2);
+ fieldset2.appendChild(legendB);
+
+ // All elements should accept the click.
+ for (var name of elementsPreventingClick) {
+ var element = document.createElement(name);
+ legendB.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler8);
+ sendMouseEvent({type:'click'}, element);
+ }
+
+ // Initialize children with click expected.
+ for (var name of elementsWithClick) {
+ var element = document.createElement(name);
+ legendB.appendChild(element);
+ element.addEventListener("click", clickShouldHappenHandler8);
+ sendMouseEvent({type:'click'}, element);
+ }
+}
+
+SpecialPowers.pushPrefEnv({
+ set: [["dom.forms.fieldset_disable_only_descendants.enabled", true]]
+}).then(() => {
+ test1();
+})
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug557087-3.html b/dom/html/test/test_bug557087-3.html
new file mode 100644
index 0000000000..98d0b0de4b
--- /dev/null
+++ b/dom/html/test/test_bug557087-3.html
@@ -0,0 +1,215 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=557087
+-->
+<head>
+ <title>Test for Bug 557087</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 557087 **/
+
+function checkValueMissing(aElement, aExpected)
+{
+ var msg = aExpected ? aElement.tagName + " should suffer from value missing"
+ : aElement.tagName + " should not suffer from value missing"
+ is(aElement.validity.valueMissing, aExpected, msg);
+}
+
+function checkCandidateForConstraintValidation(aElement, aExpected)
+{
+ var msg = aExpected ? aElement.tagName + " should be candidate for constraint validation"
+ : aElement.tagName + " should not be candidate for constraint validation"
+ is(aElement.willValidate, aExpected, msg);
+}
+
+function checkDisabledPseudoClass(aElement, aDisabled)
+{
+ var disabledElements = document.querySelectorAll(":disabled");
+ var found = false;
+
+ for (var e of disabledElements) {
+ if (aElement == e) {
+ found = true;
+ break;
+ }
+ }
+
+ var msg = aDisabled ? aElement.tagName + " should have :disabled applying"
+ : aElement.tagName + " should not have :disabled applying";
+ ok(aDisabled ? found : !found, msg);
+}
+
+function checkEnabledPseudoClass(aElement, aEnabled)
+{
+ var enabledElements = document.querySelectorAll(":enabled");
+ var found = false;
+
+ for (var e of enabledElements) {
+ if (aElement == e) {
+ found = true;
+ break;
+ }
+ }
+
+ var msg = aEnabled ? aElement.tagName + " should have :enabled applying"
+ : aElement.tagName + " should not have :enabled applying";
+ ok(aEnabled ? found : !found, msg);
+}
+
+function checkFocus(aElement, aExpected)
+{
+ aElement.setAttribute('tabindex', 1);
+
+ // We use the focus manager so we can test <label>.
+ var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]
+ .getService(SpecialPowers.Ci.nsIFocusManager);
+ fm.setFocus(aElement, 0);
+
+ if (aExpected) {
+ is(document.activeElement, aElement, "element should be focused");
+ } else {
+ isnot(document.activeElement, aElement, "element should not be focused");
+ }
+
+ aElement.blur();
+ aElement.removeAttribute('tabindex');
+}
+
+var elements = [ "input", "button", "select", "textarea", "fieldset", "option",
+ "optgroup", "label", "output", "object" ];
+
+var testData = {
+/* tag name | affected by disabled | test focus | test pseudo-classes | test willValidate */
+ "INPUT": [ true, true, true, true, true ],
+ "BUTTON": [ true, true, true, false, false ],
+ "SELECT": [ true, true, true, true, false ],
+ "TEXTAREA": [ true, true, true, true, true ],
+ "FIELDSET": [ true, true, true, false, false ],
+ "OPTION": [ false, true, true, false, false ],
+ "OPTGROUP": [ false, true, true, false, false ],
+ "OBJECT": [ false, true, false, false, false ],
+ "LABEL": [ false, true, false, false, false ],
+ "OUTPUT": [ false, true, false, false, false ],
+};
+
+/**
+ * For not candidate elements without disabled attribute and not submittable,
+ * we only have to check that focus and click works even inside a disabled
+ * fieldset.
+ */
+function checkElement(aElement, aDisabled)
+{
+ var data = testData[aElement.tagName];
+ var expected = data[0] ? !aDisabled : true;
+
+ if (data[1]) {
+ checkFocus(aElement, expected);
+ }
+
+ if (data[2]) {
+ checkEnabledPseudoClass(aElement, data[0] ? !aDisabled : true);
+ checkDisabledPseudoClass(aElement, data[0] ? aDisabled : false);
+ }
+
+ if (data[3]) {
+ checkCandidateForConstraintValidation(aElement, expected);
+ }
+
+ if (data[4]) {
+ checkValueMissing(aElement, expected);
+ }
+}
+
+var fieldset1 = document.createElement("fieldset");
+var fieldset2 = document.createElement("fieldset");
+var legendA = document.createElement("legend");
+var legendB = document.createElement("legend");
+var content = document.getElementById('content');
+content.appendChild(fieldset1);
+fieldset1.appendChild(fieldset2);
+fieldset2.disabled = true;
+
+for (var data of elements) {
+ var element = document.createElement(data);
+
+ if (data[4]) {
+ element.required = true;
+ }
+
+ fieldset1.disabled = false;
+ fieldset2.appendChild(element);
+
+ checkElement(element, fieldset2.disabled);
+
+ // Make sure changes are correctly managed.
+ fieldset2.disabled = false;
+ checkElement(element, fieldset2.disabled);
+ fieldset2.disabled = true;
+ checkElement(element, fieldset2.disabled);
+
+ // Make sure if a fieldset which is not the first fieldset is disabled, the
+ // elements inside the second fielset are disabled.
+ fieldset2.disabled = false;
+ fieldset1.disabled = true;
+ checkElement(element, fieldset1.disabled);
+
+ // Make sure the state change of the inner fieldset will not confuse.
+ fieldset2.disabled = true;
+ fieldset2.disabled = false;
+ checkElement(element, fieldset1.disabled);
+
+
+ /* legend tests */
+
+ // elements in the first legend of a disabled fieldset should not be disabled.
+ fieldset2.disabled = true;
+ fieldset1.disabled = false;
+ legendA.appendChild(element);
+ fieldset2.appendChild(legendA);
+ checkElement(element, false);
+
+ // elements in the second legend should be disabled
+ fieldset2.insertBefore(legendB, legendA);
+ checkElement(element, fieldset2.disabled);
+ fieldset2.removeChild(legendB);
+
+ // Elements in the first legend of a fieldset disabled by another fieldset
+ // should be disabled.
+ fieldset1.disabled = true;
+ checkElement(element, fieldset1.disabled);
+
+ // Elements inside a fieldset inside the first legend of a disabled fieldset
+ // should not be diasbled.
+ fieldset2.disabled = false;
+ fieldset1.appendChild(legendA);
+ legendA.appendChild(fieldset2);
+ fieldset2.appendChild(element);
+ checkElement(element, false);
+
+ // Elements inside the first legend of a disabled fieldset inside the first
+ // legend of a disabled fieldset should not be disabled.
+ fieldset2.disabled = false;
+ fieldset2.appendChild(legendB);
+ legendB.appendChild(element);
+ checkElement(element, false);
+ fieldset2.removeChild(legendB);
+ fieldset1.appendChild(fieldset2);
+
+ element.remove();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug557087-4.html b/dom/html/test/test_bug557087-4.html
new file mode 100644
index 0000000000..72aa0f1dc2
--- /dev/null
+++ b/dom/html/test/test_bug557087-4.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=557087
+-->
+<head>
+ <title>Test for Bug 557087</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a>
+<p id="display"></p>
+<div id="content">
+ <iframe name='f'></iframe>
+ <form target='f' action="data:text/html">
+ <input type='text' id='a'>
+ <input type='checkbox' id='b'>
+ <input type='radio' id='c'>
+ <fieldset disabled>
+ <fieldset>
+ <input type='submit' id='s'>
+ </fieldset>
+ </fieldset>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 557087 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var gExpectedSubmits = 6;
+var gSubmitReceived = 0;
+var gEnd = false;
+
+var fieldsets = document.getElementsByTagName("fieldset");
+var form = document.forms[0];
+
+form.addEventListener("submit", function() {
+ ok(gEnd, gEnd ? "expected submit" : "non expected submit");
+ if (++gSubmitReceived >= gExpectedSubmits) {
+ form.removeEventListener("submit", arguments.callee);
+ SimpleTest.finish();
+ }
+});
+
+var inputs = [
+ document.getElementById('a'),
+ document.getElementById('b'),
+ document.getElementById('c'),
+];
+
+function doSubmit()
+{
+ for (e of inputs) {
+ e.focus();
+ synthesizeKey("KEY_Enter");
+ }
+}
+
+SimpleTest.waitForFocus(function() {
+ doSubmit();
+
+ fieldsets[1].disabled = true;
+ fieldsets[0].disabled = false;
+ doSubmit();
+
+ fieldsets[0].disabled = false;
+ fieldsets[1].disabled = false;
+
+ gEnd = true;
+ doSubmit();
+
+ // Simple check that we can submit from inside a legend even if the fieldset
+ // is disabled.
+ var legend = document.createElement("legend");
+ fieldsets[0].appendChild(legend);
+ fieldsets[0].disabled = true;
+ legend.appendChild(document.getElementById('s'));
+
+ doSubmit();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug557087-5.html b/dom/html/test/test_bug557087-5.html
new file mode 100644
index 0000000000..9d58e0ba93
--- /dev/null
+++ b/dom/html/test/test_bug557087-5.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=557087
+-->
+<head>
+ <title>Test for Bug 557087</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a>
+<p id="display"></p>
+<div id="content">
+ <iframe name='t'></iframe>
+ <form target='t' action="dummy_page.html">
+ <fieldset disabled>
+ <fieldset>
+ <input name='i' value='i'>
+ <textarea name='t'>t</textarea>
+ <select name='s'><option>s</option></select>
+ </fieldset>
+ </fieldset>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 557087 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+const BASE_URI = `${location.origin}/tests/dom/html/test/dummy_page.html`;
+var testResults = [
+ BASE_URI + "?",
+ BASE_URI + "?",
+ BASE_URI + "?i=i&t=t&s=s",
+ BASE_URI + "?i=i&t=t&s=s",
+];
+var gTestCount = 0;
+
+var form = document.forms[0];
+var iframe = document.getElementsByTagName('iframe')[0];
+var fieldsets = document.getElementsByTagName('fieldset');
+
+function runTest()
+{
+ iframe.addEventListener("load", function() {
+ is(iframe.contentWindow.location.href, testResults[gTestCount],
+ testResults[gTestCount] + " should have been loaded");
+
+ switch (++gTestCount) {
+ case 1:
+ fieldsets[1].disabled = true;
+ fieldsets[0].disabled = false;
+ form.submit();
+ SimpleTest.executeSoon(function() {
+ form.submit()
+ });
+ break;
+ case 2:
+ fieldsets[0].disabled = false;
+ fieldsets[1].disabled = false;
+ SimpleTest.executeSoon(function() {
+ form.submit()
+ });
+ break;
+ case 3:
+ // Elements inside the first legend of a disabled fieldset are submittable.
+ fieldsets[0].disabled = true;
+ fieldsets[1].disabled = true;
+ var legend = document.createElement("legend");
+ fieldsets[0].appendChild(legend);
+ while (fieldsets[1].firstChild) {
+ legend.appendChild(fieldsets[1].firstChild);
+ }
+ SimpleTest.executeSoon(function() {
+ form.submit()
+ });
+ break;
+ default:
+ iframe.removeEventListener("load", arguments.callee);
+ SimpleTest.executeSoon(SimpleTest.finish);
+ }
+ });
+
+ form.submit();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug557087-6.html b/dom/html/test/test_bug557087-6.html
new file mode 100644
index 0000000000..8d7bf08e5c
--- /dev/null
+++ b/dom/html/test/test_bug557087-6.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=557087
+-->
+<head>
+ <title>Test for Bug 557087</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <fieldset disabled>
+ <input>
+ </fieldset>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 557087 **/
+
+// Testing random stuff following review comments.
+
+var fieldset = document.getElementsByTagName("fieldset")[0];
+
+is(fieldset.elements.length, 1,
+ "there should be one element inside the fieldset");
+is(fieldset.elements[0], document.getElementsByTagName("input")[0],
+ "input should be the element inside the fieldset");
+
+document.body.removeChild(document.getElementById('content'));
+is(fieldset.querySelector("input:disabled"), fieldset.elements[0],
+ "the input should still be disabled");
+
+fieldset.disabled = false;
+is(fieldset.querySelector("input:enabled"), fieldset.elements[0],
+ "the input should be enabled");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug557620.html b/dom/html/test/test_bug557620.html
new file mode 100644
index 0000000000..9a05988963
--- /dev/null
+++ b/dom/html/test/test_bug557620.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=557620
+-->
+<head>
+ <title>Test for Bug 557620</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557620">Mozilla Bug 557620</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <input type="tel" id='i'>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 557620 **/
+
+// More checks are done in test_bug551670.html.
+
+var tel = document.getElementById('i');
+is(tel.type, 'tel', "input with type='tel' should return 'tel'");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug558788-1.html b/dom/html/test/test_bug558788-1.html
new file mode 100644
index 0000000000..5dc4a1f34b
--- /dev/null
+++ b/dom/html/test/test_bug558788-1.html
@@ -0,0 +1,212 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=558788
+-->
+<head>
+ <title>Test for Bug 558788</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input, textarea { background-color: rgb(0,0,0) !important; }
+ :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; }
+ :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=558788">Mozilla Bug 558788</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 558788 **/
+
+/**
+ * This test checks the behavior of :valid and :invalid pseudo-classes
+ * when the user is typing/interacting with the element.
+ * Only <input> and <textarea> elements can have there validity changed by an
+ * user input.
+ */
+
+var gContent = document.getElementById('content');
+
+function checkValidApplies(elmt)
+{
+ is(window.getComputedStyle(elmt).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+}
+
+function checkInvalidApplies(elmt, aTodo)
+{
+ if (aTodo) {
+ todo_is(window.getComputedStyle(elmt).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+ return;
+ }
+ is(window.getComputedStyle(elmt).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+}
+
+function checkMissing(elementName)
+{
+ var element = document.createElement(elementName);
+ element.required = true;
+ gContent.appendChild(element);
+ checkInvalidApplies(element);
+
+ element.focus();
+
+ sendString("a");
+ checkValidApplies(element);
+
+ synthesizeKey("KEY_Backspace");
+ checkInvalidApplies(element);
+
+ gContent.removeChild(element);
+}
+
+function checkTooLong(elementName)
+{
+ var element = document.createElement(elementName);
+ element.value = "foo";
+ element.maxLength = 2;
+ gContent.appendChild(element);
+ checkInvalidApplies(element, true);
+
+ element.focus();
+
+ synthesizeKey("KEY_Backspace");
+ checkValidApplies(element);
+ gContent.removeChild(element);
+}
+
+function checkTextAreaMissing()
+{
+ checkMissing('textarea');
+}
+
+function checkTextAreaTooLong()
+{
+ checkTooLong('textarea');
+}
+
+function checkTextArea()
+{
+ checkTextAreaMissing();
+ checkTextAreaTooLong();
+}
+
+function checkInputMissing()
+{
+ checkMissing('input');
+}
+
+function checkInputTooLong()
+{
+ checkTooLong('input');
+}
+
+function checkInputEmail()
+{
+ var element = document.createElement('input');
+ element.type = 'email';
+ gContent.appendChild(element);
+ checkValidApplies(element);
+
+ element.focus();
+
+ sendString("a");
+ checkInvalidApplies(element);
+
+ sendString("@b.c");
+ checkValidApplies(element);
+
+ synthesizeKey("KEY_Backspace");
+ for (var i=0; i<4; ++i) {
+ if (i == 1) {
+ // a@b is a valid value.
+ checkValidApplies(element);
+ } else {
+ checkInvalidApplies(element);
+ }
+ synthesizeKey("KEY_Backspace");
+ }
+ checkValidApplies(element);
+
+ gContent.removeChild(element);
+}
+
+function checkInputURL()
+{
+ var element = document.createElement('input');
+ element.type = 'url';
+ gContent.appendChild(element);
+ checkValidApplies(element);
+
+ element.focus();
+
+ sendString("h");
+ checkInvalidApplies(element);
+
+ sendString("ttp://mozilla.org");
+ checkValidApplies(element);
+
+ for (var i=0; i<10; ++i) {
+ synthesizeKey("KEY_Backspace");
+ checkValidApplies(element);
+ }
+
+ synthesizeKey("KEY_Backspace");
+ // "http://" is now invalid
+ for (var i=0; i<7; ++i) {
+ checkInvalidApplies(element);
+ synthesizeKey("KEY_Backspace");
+ }
+ checkValidApplies(element);
+
+ gContent.removeChild(element);
+}
+
+function checkInputPattern()
+{
+ var element = document.createElement('input');
+ element.pattern = "[0-9]*"
+ gContent.appendChild(element);
+ checkValidApplies(element);
+
+ element.focus();
+
+ sendString("0");
+ checkValidApplies(element);
+
+ sendString("a");
+ checkInvalidApplies(element);
+
+ synthesizeKey("KEY_Backspace");
+ checkValidApplies(element);
+
+ synthesizeKey("KEY_Backspace");
+ checkValidApplies(element);
+
+ gContent.removeChild(element);
+}
+
+function checkInput()
+{
+ checkInputMissing();
+ checkInputTooLong();
+ checkInputEmail();
+ checkInputURL();
+ checkInputPattern();
+}
+
+checkTextArea();
+checkInput();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug558788-2.html b/dom/html/test/test_bug558788-2.html
new file mode 100644
index 0000000000..8224159d38
--- /dev/null
+++ b/dom/html/test/test_bug558788-2.html
@@ -0,0 +1,174 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=558788
+-->
+<head>
+ <title>Test for Bug 558788</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=558788">Mozilla Bug 558788</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 558788 **/
+
+var validElementsDescription = [
+ /* element type value required pattern maxlength minlength */
+ /* <input> */
+ [ "input", null, null, null, null, null, null ],
+ /* <input required value='foo'> */
+ [ "input", null, "foo", true, null, null, null ],
+ /* <input type='email'> */
+ [ "input", "email", null, null, null, null, null ],
+ /* <input type='email' value='foo@mozilla.org'> */
+ [ "input", "email", "foo@mozilla.org", null, null, null, null ],
+ /* <input type='url'> */
+ [ "input", "url", null, null, null, null, null ],
+ /* <input type='url' value='http://mozilla.org'> */
+ [ "input", "url", "http://mozilla.org", null, null, null, null ],
+ /* <input pattern='\\d\\d'> */
+ [ "input", null, null, null, "\\d\\d", null, null ],
+ /* <input pattern='\\d\\d' value='42'> */
+ [ "input", null, "42", null, "\\d\\d", null, null ],
+ /* <input maxlength='3'> - still valid until user interaction */
+ [ "input", null, null, null, null, "3", null ],
+ /* <input maxlength='3'> */
+ [ "input", null, "fooo", null, null, "3", null ],
+ /* <input minlength='3'> - still valid until user interaction */
+ [ "input", null, null, null, null, null, "3" ],
+ /* <input minlength='3'> */
+ [ "input", null, "fo", null, null, null, "3" ],
+ /* <textarea></textarea> */
+ [ "textarea", null, null, null, null, null, null ],
+ /* <textarea required>foo</textarea> */
+ [ "textarea", null, "foo", true, null, null, null ]
+];
+
+var invalidElementsDescription = [
+ /* element type value required pattern maxlength minlength valid-value */
+ /* <input required> */
+ [ "input", null, null, true, null, null, null, "foo" ],
+ /* <input type='email' value='foo'> */
+ [ "input", "email", "foo", null, null, null, null, "foo@mozilla.org" ],
+ /* <input type='url' value='foo'> */
+ [ "input", "url", "foo", null, null, null, null, "http://mozilla.org" ],
+ /* <input pattern='\\d\\d' value='foo'> */
+ [ "input", null, "foo", null, "\\d\\d", null, null, "42" ],
+ /* <input maxlength='3'> - still valid until user interaction */
+ [ "input", null, "foooo", null, null, "3", null, "foo" ],
+ /* <input minlength='3'> - still valid until user interaction */
+ [ "input", null, "foo", null, null, null, "3", "foo" ],
+ /* <textarea required></textarea> */
+ [ "textarea", null, null, true, null, null, null, "foo" ],
+];
+
+var validElements = [];
+var invalidElements = [];
+
+function appendElements(aElementsDesc, aElements)
+{
+ var content = document.getElementById('content');
+ var length = aElementsDesc.length;
+
+ for (var i=0; i<length; ++i) {
+ var e = document.createElement(aElementsDesc[i][0]);
+ if (aElementsDesc[i][1]) {
+ e.type = aElementsDesc[i][1];
+ }
+ if (aElementsDesc[i][2]) {
+ e.value = aElementsDesc[i][2];
+ }
+ if (aElementsDesc[i][3]) {
+ e.required = true;
+ }
+ if (aElementsDesc[i][4]) {
+ e.pattern = aElementsDesc[i][4];
+ }
+ if (aElementsDesc[i][5]) {
+ e.maxLength = aElementsDesc[i][5];
+ }
+ if (aElementsDesc[i][6]) {
+ e.minLength = aElementsDesc[i][6];
+ }
+
+ content.appendChild(e);
+
+ // Adding the element to the appropriate list.
+ aElements.push(e);
+ }
+}
+
+function compareArrayWithSelector(aElements, aSelector)
+{
+ var aSelectorElements = document.querySelectorAll(aSelector);
+
+ is(aSelectorElements.length, aElements.length,
+ aSelector + " selector should return the correct number of elements");
+
+ if (aSelectorElements.length != aElements.length) {
+ return;
+ }
+
+ var length = aElements.length;
+ for (var i=0; i<length; ++i) {
+ is(aSelectorElements[i], aElements[i],
+ aSelector + " should return the correct elements");
+ }
+}
+
+function makeMinMaxLengthElementsActuallyInvalid(aInvalidElements,
+ aInvalidElementsDesc)
+{
+ // min/maxlength elements are not invalid until user edits them
+ var length = aInvalidElementsDesc.length;
+
+ for (var i=0; i<length; ++i) {
+ var e = aInvalidElements[i];
+ if (aInvalidElementsDesc[i][5]) { // maxlength
+ e.focus();
+ synthesizeKey("KEY_Backspace");
+ } else if (aInvalidElementsDesc[i][6]) { // minlength
+ e.focus();
+ synthesizeKey("KEY_Backspace");
+ }
+ }
+}
+
+function makeInvalidElementsValid(aInvalidElements,
+ aInvalidElementsDesc,
+ aValidElements)
+{
+ var length = aInvalidElementsDesc.length;
+
+ for (var i=0; i<length; ++i) {
+ var e = aInvalidElements.shift();
+ e.value = aInvalidElementsDesc[i][7];
+ aValidElements.push(e);
+ }
+}
+
+appendElements(validElementsDescription, validElements);
+appendElements(invalidElementsDescription, invalidElements);
+
+makeMinMaxLengthElementsActuallyInvalid(invalidElements, invalidElementsDescription);
+
+compareArrayWithSelector(validElements, ":valid");
+compareArrayWithSelector(invalidElements, ":invalid");
+
+makeInvalidElementsValid(invalidElements, invalidElementsDescription,
+ validElements);
+
+compareArrayWithSelector(validElements, ":valid");
+compareArrayWithSelector(invalidElements, ":invalid");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug560112.html b/dom/html/test/test_bug560112.html
new file mode 100644
index 0000000000..48aaad8dc5
--- /dev/null
+++ b/dom/html/test/test_bug560112.html
@@ -0,0 +1,211 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=560112
+-->
+<head>
+ <title>Test for Bug 560112</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=560112">Mozilla Bug 560112</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 560112 **/
+
+/**
+ * Sets dataset property. Checks data attribute "attr".
+ * Gets dataset property. Checks data attribute "attr".
+ * Overwrites dataset property Checks data attribute "attr".
+ * Deletes dataset property. Checks data attribute "attr".
+ */
+function SetGetOverwriteDel(attr, prop)
+{
+ var el = document.createElement('div');
+
+ // Set property.
+ is(prop in el.dataset, false, 'Property should not be in dataset before setting.');
+ el.dataset[prop] = "zzzzzz";
+ is(prop in el.dataset, true, 'Property should be in dataset after setting.');
+ ok(el.hasAttribute(attr), 'Element should have data attribute for dataset property "' + prop + '".');
+
+ // Get property.
+ is(el.dataset[prop], "zzzzzz", 'Dataset property "' + prop + '" should have value "zzzzzz".');
+ is(el.getAttribute(attr), "zzzzzz", 'Attribute "' + attr + '" should have value "zzzzzz".');
+
+ // Overwrite property.
+ el.dataset[prop] = "yyyyyy";
+ is(el.dataset[prop], "yyyyyy", 'Dataset property "' + prop + '" should have value "yyyyyy".');
+ is(el.getAttribute(attr), "yyyyyy", 'Attribute "' + attr + '" should have value "yyyyyy".');
+
+ // Delete property.
+ delete el.dataset[prop];
+ ok(!el.hasAttribute(attr), 'Element should not have data attribute for dataset property "' + prop + '".');
+ is(prop in el.dataset, false, 'Deleted property should not be in dataset.');
+}
+
+/**
+ * Sets dataset property and expects exception.
+ */
+function SetExpectException(prop)
+{
+ var el = document.createElement('div');
+
+ try {
+ el.dataset[prop] = "xxxxxx";
+ ok(false, 'Exception should have been thrown.');
+ } catch (ex) {
+ ok(true, 'Exception should have been thrown.');
+ }
+}
+
+/**
+ * Adds attributes in "attrList" to element.
+ * Deletes attributes in "delList" from element.
+ * Checks for "numProp" properties in dataset.
+ */
+function DelAttrEnumerate(attrList, delList, numProp)
+{
+ var el = document.createElement('div');
+
+ // Adds attributes in "attrList".
+ for (var i = 0; i < attrList.length; ++i) {
+ el.setAttribute(attrList[i], "aaaaaa");
+ }
+
+ // Remove attributes in "delList".
+ for (var i = 0; i < delList.length; ++i) {
+ el.removeAttribute(delList[i]);
+ }
+
+ var numPropCounted = 0;
+
+ for (var prop in el.dataset) {
+ if (el.dataset[prop] == "aaaaaa") {
+ ++numPropCounted;
+ }
+ }
+
+ is(numPropCounted, numProp, 'Number of enumerable dataset properties is incorrent after attribute removal.');
+}
+
+/**
+ * Adds attributes in "attrList" to element.
+ * Checks for "numProp" properties in dataset.
+ */
+function Enumerate(attrList, numProp)
+{
+ var el = document.createElement('div');
+
+ // Adds attributes in "attrList" to element.
+ for (var i = 0; i < attrList.length; ++i) {
+ el.setAttribute(attrList[i], "aaaaaa");
+ }
+
+ var numPropCounted = 0;
+
+ for (var prop in el.dataset) {
+ if (el.dataset[prop] == "aaaaaa") {
+ ++numPropCounted;
+ }
+ }
+
+ is(numPropCounted, numProp, 'Number of enumerable dataset properties is incorrect.');
+}
+
+/**
+ * Adds dataset property then removes attribute from element and check for presence of
+ * properties using the "in" operator.
+ */
+function AddPropDelAttr(attr, prop)
+{
+ var el = document.createElement('div');
+
+ el.dataset[prop] = 'dddddd';
+ is(prop in el.dataset, true, 'Operator "in" should return true after setting property.');
+ el.removeAttribute(attr);
+ is(prop in el.dataset, false, 'Operator "in" should return false for removed attribute.');
+}
+
+/**
+ * Adds then removes attribute from element and check for presence of properties using the
+ * "in" operator.
+ */
+function AddDelAttr(attr, prop)
+{
+ var el = document.createElement('div');
+
+ el.setAttribute(attr, 'dddddd');
+ is(prop in el.dataset, true, 'Operator "in" should return true after setting attribute.');
+ el.removeAttribute(attr);
+ is(prop in el.dataset, false, 'Operator "in" should return false for removed attribute.');
+}
+
+// Typical use case.
+SetGetOverwriteDel('data-property', 'property');
+SetGetOverwriteDel('data-a-longer-property', 'aLongerProperty');
+
+AddDelAttr('data-property', 'property');
+AddDelAttr('data-a-longer-property', 'aLongerProperty');
+
+AddPropDelAttr('data-property', 'property');
+AddPropDelAttr('data-a-longer-property', 'aLongerProperty');
+
+// Empty property name.
+SetGetOverwriteDel('data-', '');
+
+// Leading dash characters.
+SetGetOverwriteDel('data--', '-');
+SetGetOverwriteDel('data--d', 'D');
+SetGetOverwriteDel('data---d', '-D');
+
+// Trailing dash characters.
+SetGetOverwriteDel('data-d-', 'd-');
+SetGetOverwriteDel('data-d--', 'd--');
+SetGetOverwriteDel('data-d-d-', 'dD-');
+
+// "data-" in attribute name.
+SetGetOverwriteDel('data-data-', 'data-');
+SetGetOverwriteDel('data-data-data-', 'dataData-');
+
+// Longer attribute.
+SetGetOverwriteDel('data-long-long-long-long-long-long-long-long-long-long-long-long-long', 'longLongLongLongLongLongLongLongLongLongLongLongLong');
+
+var longAttr = 'data-long';
+var longProp = 'long';
+for (var i = 0; i < 30000; ++i) {
+ // Create really long attribute and property names.
+ longAttr += '-long';
+ longProp += 'Long';
+}
+
+SetGetOverwriteDel(longAttr, longProp);
+
+// Syntax error in setting dataset property (dash followed by lower case).
+SetExpectException('-a');
+SetExpectException('a-a');
+SetExpectException('a-a-a');
+
+// Invalid character.
+SetExpectException('a a');
+
+// Enumeration over dataset properties.
+Enumerate(['data-a-big-fish'], 1);
+Enumerate(['dat-a-big-fish'], 0);
+Enumerate(['data-'], 1);
+Enumerate(['data-', 'data-more-data'], 2);
+Enumerate(['daaata-', 'data-more-data'], 1);
+
+// Delete data attributes and enumerate properties.
+DelAttrEnumerate(['data-one', 'data-two'], ['data-one'], 1);
+DelAttrEnumerate(['data-one', 'data-two'], ['data-three'], 2);
+DelAttrEnumerate(['data-one', 'data-two'], ['one'], 2);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug561634.html b/dom/html/test/test_bug561634.html
new file mode 100644
index 0000000000..1eab90508f
--- /dev/null
+++ b/dom/html/test/test_bug561634.html
@@ -0,0 +1,126 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=561634
+-->
+<head>
+ <title>Test for Bug 561634</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=561634">Mozilla Bug 561634</a>
+<p id="display"></p>
+<div id="content" style="display: none;">
+ <form>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 561634 **/
+
+function checkEmptyForm()
+{
+ ok(document.forms[0].checkValidity(), "An empty form is valid");
+}
+
+function checkBarredFromConstraintValidation()
+{
+ var f = document.forms[0];
+ var fs = document.createElement('fieldset');
+ var i = document.createElement('input');
+
+ f.appendChild(fs);
+ i.type = 'hidden';
+ f.appendChild(i);
+
+ fs.setCustomValidity("foo");
+ i.setCustomValidity("foo");
+
+ ok(f.checkValidity(),
+ "A form with invalid element barred from constraint validation should be valid");
+
+ f.removeChild(i);
+ f.removeChild(fs);
+}
+
+function checkValid()
+{
+ var f = document.forms[0];
+ var i = document.createElement('input');
+ f.appendChild(i);
+
+ ok(f.checkValidity(), "A form with valid elements is valid");
+
+ f.removeChild(i);
+}
+
+function checkInvalid()
+{
+ var f = document.forms[0];
+ var i = document.createElement('input');
+ f.appendChild(i);
+
+ i.setCustomValidity("foo");
+ ok(!f.checkValidity(), "A form with invalid elements is invalid");
+
+ var i2 = document.createElement('input');
+ f.appendChild(i2);
+ ok(!f.checkValidity(),
+ "A form with at least one invalid element is invalid");
+
+ f.removeChild(i2);
+ f.removeChild(i);
+}
+
+function checkInvalidEvent()
+{
+ var f = document.forms[0];
+ var i = document.createElement('input');
+ f.appendChild(i);
+ var i2 = document.createElement('input');
+ f.appendChild(i2);
+
+ i.setCustomValidity("foo");
+
+ var invalidEventForInvalidElement = false;
+ var invalidEventForValidElement = false;
+
+ i.addEventListener("invalid", function (e) {
+ invalidEventForInvalidElement = true;
+ ok(e.cancelable, "invalid event should be cancelable");
+ ok(!e.bubbles, "invalid event should not bubble");
+ });
+
+ i2.addEventListener("invalid", function (e) {
+ invalidEventForValidElement = true;
+ });
+
+ f.checkValidity();
+
+ setTimeout(function() {
+ ok(invalidEventForInvalidElement,
+ "invalid event should be fired on invalid elements");
+ ok(!invalidEventForValidElement,
+ "invalid event should not be fired on valid elements");
+
+ f.removeChild(i2);
+ f.removeChild(i);
+
+ SimpleTest.finish();
+ }, 0);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+checkEmptyForm();
+checkBarredFromConstraintValidation();
+checkValid();
+checkInvalid();
+checkInvalidEvent(); // will call finish().
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug561636.html b/dom/html/test/test_bug561636.html
new file mode 100644
index 0000000000..da51b07607
--- /dev/null
+++ b/dom/html/test/test_bug561636.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=561636
+-->
+<head>
+ <title>Test for Bug 561636</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=561636">Mozilla Bug 561636</a>
+<p id="display"></p>
+<iframe style='width:50px; height: 50px;' name='t'></iframe>
+<iframe style='width:50px; height: 50px;' name='t2' id='i'></iframe>
+<div id="content">
+ <form target='t' action='data:text/html,'>
+ <input required>
+ <input id='a' type='submit'>
+ </form>
+ <form target='t' action='data:text/html,'>
+ <input type='checkbox' required>
+ <button id='b' type='submit'></button>
+ </form>
+ <form target='t' action='data:text/html,'>
+ <input id='c' required>
+ </form>
+ <form target='t' action='data:text/html,'>
+ <input>
+ <input id='s2' type='submit'>
+ </form>
+ <form target='t2' action='data:text/html,'>
+ <input required>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 561636 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+function runTest()
+{
+ var formSubmitted = [ false, false ];
+ var invalidHandled = false;
+
+ // Initialize
+ document.forms[0].addEventListener('submit', function(aEvent) {
+ formSubmitted[0] = true;
+ }, {once: true});
+
+ document.forms[1].addEventListener('submit', function(aEvent) {
+ formSubmitted[1] = true;
+ }, {once: true});
+
+ document.forms[2].addEventListener('submit', function(aEvent) {
+ formSubmitted[2] = true;
+ }, {once: true});
+
+ document.forms[3].addEventListener('submit', function(aEvent) {
+ formSubmitted[3] = true;
+
+ ok(!formSubmitted[0], "Form 1 should not have been submitted because invalid");
+ ok(!formSubmitted[1], "Form 2 should not have been submitted because invalid");
+ ok(!formSubmitted[2], "Form 3 should not have been submitted because invalid");
+ ok(formSubmitted[3], "Form 4 should have been submitted because valid");
+
+ // Next test.
+ document.forms[4].submit();
+ }, {once: true});
+
+ document.forms[4].elements[0].addEventListener('invalid', function(aEvent) {
+ invalidHandled = true;
+ }, {once: true});
+
+ document.getElementById('i').addEventListener('load', function(aEvent) {
+ SimpleTest.executeSoon(function () {
+ ok(true, "Form 5 should have been submitted because submit() has been used even if invalid");
+ ok(!invalidHandled, "Invalid event should not have been sent");
+
+ SimpleTest.finish();
+ });
+ }, {once: true});
+
+ document.getElementById('a').click();
+ document.getElementById('b').click();
+ var c = document.getElementById('c');
+ c.focus();
+ synthesizeKey("KEY_Enter");
+ document.getElementById('s2').click();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug561640.html b/dom/html/test/test_bug561640.html
new file mode 100644
index 0000000000..ed7bf84126
--- /dev/null
+++ b/dom/html/test/test_bug561640.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=561640
+-->
+<head>
+ <title>Test for Bug 561640</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ input, textarea { background-color: rgb(0,0,0) !important; }
+ :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; }
+ :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=561640">Mozilla Bug 561640</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 561640 **/
+
+var elements = [ 'input', 'textarea' ];
+var content = document.getElementById('content');
+
+function checkValid(elmt)
+{
+ ok(!elmt.validity.tooLong, "element should not be too long");
+ is(window.getComputedStyle(elmt).getPropertyValue('background-color'),
+ "rgb(0, 255, 0)", ":valid pseudo-class should apply");
+}
+
+function checkInvalid(elmt)
+{
+ todo(elmt.validity.tooLong, "element should be too long");
+ todo_is(window.getComputedStyle(elmt).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+}
+
+for (var elmtName of elements) {
+ var elmt = document.createElement(elmtName);
+ content.appendChild(elmt);
+
+ if (elmtName == 'textarea') {
+ elmt.textContent = 'foo';
+ } else {
+ elmt.setAttribute('value', 'foo');
+ }
+ elmt.maxLength = 2;
+ checkValid(elmt);
+
+ elmt.value = 'a';
+ checkValid(elmt);
+
+ if (elmtName == 'textarea') {
+ elmt.textContent = 'f';
+ } else {
+ elmt.setAttribute('value', 'f');
+ }
+ elmt.value = 'bar';
+ checkInvalid(elmt);
+
+ content.removeChild(elmt);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug564001.html b/dom/html/test/test_bug564001.html
new file mode 100644
index 0000000000..3815ac8cf9
--- /dev/null
+++ b/dom/html/test/test_bug564001.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=564001
+-->
+<head>
+ <title>Test for Bug 564001</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=564001">Mozilla Bug 564001</a>
+<p id="display"><img usemap=#map src=image.png></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script>
+/** Test for Bug 564001 **/
+SimpleTest.waitForExplicitFinish();
+
+var wrongArea = document.createElement("area");
+wrongArea.shape = "default";
+wrongArea.href = "#FAIL";
+var wrongMap = document.createElement("map");
+wrongMap.name = "map";
+wrongMap.appendChild(wrongArea);
+document.body.appendChild(wrongMap);
+
+var rightArea = document.createElement("area");
+rightArea.shape = "default";
+rightArea.href = "#PASS";
+var rightMap = document.createElement("map");
+rightMap.name = "map";
+rightMap.appendChild(rightArea);
+document.body.insertBefore(rightMap, wrongMap);
+
+var images = document.getElementsByTagName("img");
+onhashchange = function() {
+ is(location.hash, "#PASS", "Should get the first map in tree order.");
+ SimpleTest.finish();
+};
+SimpleTest.waitForFocus(() => synthesizeMouse(images[0], 50, 50, {}));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug566046.html b/dom/html/test/test_bug566046.html
new file mode 100644
index 0000000000..88c59a4e61
--- /dev/null
+++ b/dom/html/test/test_bug566046.html
@@ -0,0 +1,200 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=566046
+-->
+<head>
+ <title>Test for Bug 566046</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <base>
+ <base target='frame2'>
+ <base target=''>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566046">Mozilla Bug 566046</a>
+<p id="display"></p>
+<style>
+ iframe { width: 130px; height: 100px;}
+</style>
+<iframe name='frame1' id='frame1'></iframe>
+<iframe name='frame2' id='frame2'></iframe>
+<iframe name='frame3' id='frame3'></iframe>
+<iframe name='frame4' id='frame4'></iframe>
+<iframe name='frame5' id='frame5'></iframe>
+<iframe name='frame5bis' id='frame5bis'></iframe>
+<iframe name='frame6' id='frame6'></iframe>
+<iframe name='frame7' id='frame7'></iframe>
+<iframe name='frame8' id='frame8'></iframe>
+<iframe name='frame9' id='frame9'></iframe>
+<div id="content">
+ <form target='frame1' action="dummy_page.html" method="GET">
+ <input name='foo' value='foo'>
+ </form>
+ <form action="dummy_page.html" method="GET">
+ <input name='bar' value='bar'>
+ </form>
+ <form target="">
+ </form>
+
+ <!-- submit controls with formtarget that are validated with a CLICK -->
+ <form target="tulip" action="dummy_page.html" method="GET">
+ <input name='tulip' value='tulip'>
+ <input type='submit' id='is' formtarget='frame3'>
+ </form>
+ <form action="dummy_page.html" method="GET">
+ <input name='foobar' value='foobar'>
+ <input type='image' id='ii' formtarget='frame4'>
+ </form>
+ <form action="dummy_page.html" method="GET">
+ <input name='tulip2' value='tulip2'>
+ <button type='submit' id='bs' formtarget='frame5'>submit</button>
+ </form>
+ <form action="dummy_page.html" method="GET">
+ <input name='tulip3' value='tulip3'>
+ <button type='submit' id='bsbis' formtarget='frame5bis'>submit</button>
+ </form>
+
+ <!-- submit controls with formtarget that are validated with ENTER -->
+ <form target="tulip" action="dummy_page.html" method="GET">
+ <input name='footulip' value='footulip'>
+ <input type='submit' id='is2' formtarget='frame6'>
+ </form>
+ <form action="dummy_page.html" method="GET">
+ <input name='tulipfoobar' value='tulipfoobar'>
+ <input type='image' id='ii2' formtarget='frame7'>
+ </form>
+ <form action="dummy_page.html" method="GET">
+ <input name='tulipbar' value='tulipbar'>
+ <button type='submit' id='bs2' formtarget='frame8'>submit</button>
+ </form>
+
+ <!-- check that a which is not a submit control do not use @formtarget -->
+ <form target='frame9' action="dummy_page.html" method="GET">
+ <input id='enter' name='input' value='enter' formtarget='frame6'>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 566046 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ setTimeout(runTests, 0);
+});
+
+const BASE_URI = `${location.origin}/tests/dom/html/test/dummy_page.html`;
+var gTestResults = {
+ frame1: BASE_URI + "?foo=foo",
+ frame2: BASE_URI + "?bar=bar",
+ frame3: BASE_URI + "?tulip=tulip",
+ frame4: BASE_URI + "?foobar=foobar&x=0&y=0",
+ frame5: BASE_URI + "?tulip2=tulip2",
+ frame5bis: BASE_URI + "?tulip3=tulip3",
+ frame6: BASE_URI + "?footulip=footulip",
+ frame7: BASE_URI + "?tulipfoobar=tulipfoobar&x=0&y=0",
+ frame8: BASE_URI + "?tulipbar=tulipbar",
+ frame9: BASE_URI + "?input=enter",
+};
+
+var gPendingLoad = 0; // Has to be set after depending on the frames number.
+
+function runTests()
+{
+ // Check the target IDL attribute.
+ for (var i=0; i<document.forms.length; ++i) {
+ var testValue = document.forms[i].getAttribute('target');
+ is(document.forms[i].target, testValue ? testValue : "",
+ "target IDL attribute should reflect the target content attribute");
+ }
+
+ // We add a load event for the frames which will be called when the forms
+ // will be submitted.
+ var frames = [ document.getElementById('frame1'),
+ document.getElementById('frame2'),
+ document.getElementById('frame3'),
+ document.getElementById('frame4'),
+ document.getElementById('frame5'),
+ document.getElementById('frame5bis'),
+ document.getElementById('frame6'),
+ document.getElementById('frame7'),
+ document.getElementById('frame8'),
+ document.getElementById('frame9'),
+ ];
+ gPendingLoad = frames.length;
+
+ for (var i=0; i<frames.length; i++) {
+ frames[i].setAttribute('onload', "frameLoaded(this);");
+ }
+
+ // Submitting only the forms with a valid target.
+ document.forms[0].submit();
+ document.forms[1].submit();
+
+ /**
+ * We are going to focus each element before interacting with either for
+ * simulating the ENTER key (synthesizeKey) or a click (synthesizeMouse) or
+ * using .click(). This because it may be needed (ENTER) and because we want
+ * to have the element visible in the iframe.
+ *
+ * Focusing the first element (id='is') is launching the tests.
+ */
+ document.getElementById('is').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('is'), 5, 5, {});
+ document.getElementById('ii').focus();
+ }, {once: true});
+
+ document.getElementById('ii').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('ii'), 5, 5, {});
+ document.getElementById('bs').focus();
+ }, {once: true});
+
+ document.getElementById('bs').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('bs'), 5, 5, {});
+ document.getElementById('bsbis').focus();
+ }, {once: true});
+
+ document.getElementById('bsbis').addEventListener('focus', function(aEvent) {
+ document.getElementById('bsbis').click();
+ document.getElementById('is2').focus();
+ }, {once: true});
+
+ document.getElementById('is2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('ii2').focus();
+ }, {once: true});
+
+ document.getElementById('ii2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('bs2').focus();
+ }, {once: true});
+
+ document.getElementById('bs2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('enter').focus();
+ }, {once: true});
+
+ document.getElementById('enter').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ }, {once: true});
+
+ document.getElementById('is').focus();
+}
+
+function frameLoaded(aFrame) {
+ // Check if when target is unspecified, it fallback correctly to the base
+ // element target attribute.
+ is(aFrame.contentWindow.location.href, gTestResults[aFrame.name],
+ "the target attribute doesn't have the correct behavior");
+
+ if (--gPendingLoad == 0) {
+ SimpleTest.finish();
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug567938-1.html b/dom/html/test/test_bug567938-1.html
new file mode 100644
index 0000000000..0c5d78f1c0
--- /dev/null
+++ b/dom/html/test/test_bug567938-1.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=567938
+-->
+<head>
+ <title>Test for Bug 567938</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="runTests();">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567938">Mozilla Bug 567938</a>
+<p id="display"></p>
+<iframe id='iframe' name="submit_frame" style="visibility: hidden;"></iframe>
+<div id="content" style="display: none">
+ <form id='f' method='get' target='submit_frame'>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 567938 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var gTestData = ["submit", "image"];
+var gCurrentTest = 0;
+
+function initializeNextTest()
+{
+ var form = document.forms[0];
+
+ // Cleaning-up.
+ form.textContent = "";
+
+ // Add the new element.
+ var element = document.createElement("input");
+ element.id = 'i';
+ element.type = gTestData[gCurrentTest];
+ element.onclick = function() { form.submit(); return false; };
+ form.action = gTestData[gCurrentTest];
+ form.appendChild(element);
+
+ sendMouseEvent({type: 'click'}, 'i');
+}
+
+function runTests()
+{
+ document.getElementById('iframe').addEventListener('load', function(aEvent) {
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/${gTestData[gCurrentTest]}?`,
+ "The form should have been submitted");
+ gCurrentTest++;
+ if (gCurrentTest < gTestData.length) {
+ initializeNextTest();
+ } else {
+ aEvent.target.removeEventListener('load', arguments.callee);
+ SimpleTest.finish();
+ }
+ });
+
+ initializeNextTest();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug567938-2.html b/dom/html/test/test_bug567938-2.html
new file mode 100644
index 0000000000..4a97516805
--- /dev/null
+++ b/dom/html/test/test_bug567938-2.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=567938
+-->
+<head>
+ <title>Test for Bug 567938</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567938">Mozilla Bug 567938</a>
+<p id="display"></p>
+<iframe id='iframe' name="submit_frame" style="visibility: hidden;"></iframe>
+<div id="content" style="display: none">
+ <form id='f' method='get' target='submit_frame'>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 567938 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTests);
+
+var gTestData = ["submit", "image"];
+var gCurrentTest = 0;
+
+function initializeNextTest()
+{
+ var form = document.forms[0];
+
+ // Cleaning-up.
+ form.textContent = "";
+
+ // Add the new element.
+ var element = document.createElement("input");
+ element.id = 'i';
+ element.type = gTestData[gCurrentTest];
+ element.onclick = function() { form.submit(); element.type='text'; };
+ form.action = gTestData[gCurrentTest];
+ form.appendChild(element);
+
+ sendMouseEvent({type: 'click'}, 'i');
+}
+
+function runTests()
+{
+ document.getElementById('iframe').addEventListener('load', function(aEvent) {
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/${gTestData[gCurrentTest]}?`,
+ "The form should have been submitted");
+ gCurrentTest++;
+ if (gCurrentTest < gTestData.length) {
+ initializeNextTest();
+ } else {
+ aEvent.target.removeEventListener('load', arguments.callee);
+ SimpleTest.finish();
+ }
+ });
+
+ initializeNextTest();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug567938-3.html b/dom/html/test/test_bug567938-3.html
new file mode 100644
index 0000000000..3c23129466
--- /dev/null
+++ b/dom/html/test/test_bug567938-3.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=567938
+-->
+<head>
+ <title>Test for Bug 567938</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567938">Mozilla Bug 567938</a>
+<p id="display"></p>
+<iframe id='iframe' name="submit_frame" style="visibility: hidden;"></iframe>
+<div id="content" style="display: none">
+ <form id='f' method='get' target='submit_frame'>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 567938 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTests);
+
+var gTestData = ["submit", "image"];
+var gCurrentTest = 0;
+
+function initializeNextTest()
+{
+ var form = document.forms[0];
+
+ // Cleaning-up.
+ form.textContent = "";
+
+ // Add the new element.
+ var element = document.createElement("input");
+ element.id = 'i';
+ element.type = gTestData[gCurrentTest];
+ // eslint-disable-next-line no-implied-eval
+ element.onclick = function() { setTimeout("document.forms[0].submit();",0); return false; };
+ form.appendChild(element);
+ form.action = gTestData[gCurrentTest];
+
+ sendMouseEvent({type: 'click'}, 'i');
+}
+
+function runTests()
+{
+ document.getElementById('iframe').addEventListener('load', function(aEvent) {
+ is(frames.submit_frame.location.href,
+ `${location.origin}/tests/dom/html/test/${gTestData[gCurrentTest]}?`,
+ "The form should have been submitted");
+ gCurrentTest++;
+ if (gCurrentTest < gTestData.length) {
+ initializeNextTest();
+ } else {
+ aEvent.target.removeEventListener('load', arguments.callee);
+ SimpleTest.finish();
+ }
+ });
+
+ initializeNextTest();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug567938-4.html b/dom/html/test/test_bug567938-4.html
new file mode 100644
index 0000000000..f04ac27f5f
--- /dev/null
+++ b/dom/html/test/test_bug567938-4.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=567938
+-->
+<head>
+ <title>Test for Bug 567938</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567938">Mozilla Bug 567938</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <input id='i' type='checkbox' onclick="return false;">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 567938 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTests);
+
+function runTests()
+{
+ document.getElementById('i').checked = false;
+
+ document.getElementById('i').addEventListener('click', function(aEvent) {
+ SimpleTest.executeSoon(function() {
+ ok(!aEvent.target.checked, "the input should not be checked");
+ SimpleTest.finish();
+ });
+ }, {once: true});
+
+ sendMouseEvent({type: 'click'}, 'i');
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug569955.html b/dom/html/test/test_bug569955.html
new file mode 100644
index 0000000000..0ed5ce88c7
--- /dev/null
+++ b/dom/html/test/test_bug569955.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=569955
+-->
+<head>
+ <title>Test for Bug 569955</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=569955">Mozilla Bug 569955</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <input id='i' autofocus>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 569955 **/
+
+function runTests()
+{
+ isnot(document.activeElement, document.getElementById('i'),
+ "not rendered elements can't be autofocused");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ setTimeout(runTests, 0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug573969.html b/dom/html/test/test_bug573969.html
new file mode 100644
index 0000000000..f6020d1445
--- /dev/null
+++ b/dom/html/test/test_bug573969.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=573969
+-->
+<head>
+ <title>Test for Bug 573969</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=573969">Mozilla Bug 573969</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <xmp id='x'></xmp>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 573969 **/
+
+var testData = [
+ '<div>foo</div>',
+ '&lt;div&gt;&lt;/div&gt;',
+];
+
+var x = document.getElementById('x');
+
+for (v of testData) {
+ x.innerHTML = v;
+ is(x.innerHTML, v, "innerHTML value should not be escaped");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug57600.html b/dom/html/test/test_bug57600.html
new file mode 100644
index 0000000000..fc7037bd32
--- /dev/null
+++ b/dom/html/test/test_bug57600.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=57600
+-->
+<head>
+ <title>Test for Bug 57600</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=57600">Mozilla Bug 57600</a>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 57600 **/
+SimpleTest.waitForExplicitFinish();
+var count = 0;
+function disp(win) {
+ var d = win ? win.document : self.testname.document;
+ var str = 'You should see this';
+ d.open();
+ d.write(str);
+ d.close();
+ is(d.documentElement.textContent, str, "Unexpected text");
+ if (++count == 2) {
+ SimpleTest.finish();
+ }
+}
+</script>
+</pre>
+<p id="display">
+ <iframe src="javascript:'<body onload=&quot;this.onerror = parent.onerror; parent.disp(self)&quot;></body>'">
+ </iframe>
+ <iframe name="testname" src="javascript:'<body onload=&quot;this.onerror = parent.onerror; parent.disp()&quot;></body>'">
+ </iframe>
+</p>
+</body>
+</html>
diff --git a/dom/html/test/test_bug579079.html b/dom/html/test/test_bug579079.html
new file mode 100644
index 0000000000..e5ec429226
--- /dev/null
+++ b/dom/html/test/test_bug579079.html
@@ -0,0 +1,40 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=579079
+-->
+<head>
+ <title>Test for Bug 579079</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=579079">Mozilla Bug 579079</a>
+
+<div id="foo">
+ <img name="img1">
+ <form name="form1"></form>
+ <applet name="applet1"></applet>
+ <embed name="embed1"></embed>
+ <object name="object1"></object>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var img = document.img1;
+var form = document.form1;
+var embed = document.embed1;
+var object = document.object1;
+$("foo").innerHTML = $("foo").innerHTML;
+isnot(document.img1, img);
+ok(document.img1 instanceof HTMLImageElement);
+isnot(document.form1, form);
+ok(document.form1 instanceof HTMLFormElement);
+isnot(document.embed1, embed);
+ok(document.embed1 instanceof HTMLEmbedElement);
+isnot(document.object1, object);
+ok(document.object1 instanceof HTMLObjectElement);
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug582412-1.html b/dom/html/test/test_bug582412-1.html
new file mode 100644
index 0000000000..f1256e3d5a
--- /dev/null
+++ b/dom/html/test/test_bug582412-1.html
@@ -0,0 +1,200 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=566160
+-->
+<head>
+ <title>Test for Bug 566160</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566160">Mozilla Bug 566160</a>
+<p id="display"></p>
+<style>
+ iframe { width: 130px; height: 100px;}
+</style>
+<iframe name='frame1' id='frame1'></iframe>
+<iframe name='frame2' id='frame2'></iframe>
+<iframe name='frame3' id='frame3'></iframe>
+<iframe name='frame3bis' id='frame3bis'></iframe>
+<iframe name='frame4' id='frame4'></iframe>
+<iframe name='frame5' id='frame5'></iframe>
+<iframe name='frame6' id='frame6'></iframe>
+<iframe name='frame7' id='frame7'></iframe>
+<iframe name='frame8' id='frame8'></iframe>
+<iframe name='frame9' id='frame9'></iframe>
+<div id="content">
+ <!-- submit controls with formaction that are validated with a CLICK -->
+ <form target="frame1" action="dummy_page.html" method="POST">
+ <input name='foo' value='foo'>
+ <input type='submit' id='is' formmethod="GET">
+ </form>
+ <form target="frame2" action="dummy_page.html" method="POST">
+ <input name='bar' value='bar'>
+ <input type='image' id='ii' formmethod="GET">
+ </form>
+ <form target="frame3" action="dummy_page.html" method="POST">
+ <input name='tulip' value='tulip'>
+ <button type='submit' id='bs' formmethod="GET">submit</button>
+ </form>
+ <form target="frame3bis" action="dummy_page.html" method="POST">
+ <input name='tulipbis' value='tulipbis'>
+ <button type='submit' id='bsbis' formmethod="GET">submit</button>
+ </form>
+
+ <!-- submit controls with formaction that are validated with ENTER -->
+ <form target="frame4" action="dummy_page.html" method="POST">
+ <input name='footulip' value='footulip'>
+ <input type='submit' id='is2' formmethod="GET">
+ </form>
+ <form target="frame5" action="dummy_page.html" method="POST">
+ <input name='foobar' value='foobar'>
+ <input type='image' id='ii2' formmethod="GET">
+ </form>
+ <form target="frame6" action="dummy_page.html" method="POST">
+ <input name='tulip2' value='tulip2'>
+ <button type='submit' id='bs2' formmethod="GET">submit</button>
+ </form>
+
+ <!-- check that when submitting a from from an element
+ which is not a submit control, @formaction isn't used -->
+ <form target='frame7' action="dummy_page.html" method="GET">
+ <input id='enter' name='input' value='enter' formmethod="POST">
+ </form>
+
+ <!-- If formmethod isn't set, it's default value shouldn't be used -->
+ <form target="frame8" action="dummy_page.html" method="POST">
+ <input name='tulip8' value='tulip8'>
+ <input type='submit' id='i8'>
+ </form>
+
+ <!-- If formmethod is set but has an invalid value, the default value should
+ be used. -->
+ <form target="frame9" action="dummy_page.html" method="POST">
+ <input name='tulip9' value='tulip9'>
+ <input type='submit' id='i9' formmethod="">
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 566160 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ setTimeout(runTests, 0);
+});
+
+const BASE_URI = `${location.origin}/tests/dom/html/test/dummy_page.html`;
+var gTestResults = {
+ frame1: BASE_URI + "?foo=foo",
+ frame2: BASE_URI + "?bar=bar&x=0&y=0",
+ frame3: BASE_URI + "?tulip=tulip",
+ frame3bis: BASE_URI + "?tulipbis=tulipbis",
+ frame4: BASE_URI + "?footulip=footulip",
+ frame5: BASE_URI + "?foobar=foobar&x=0&y=0",
+ frame6: BASE_URI + "?tulip2=tulip2",
+ frame7: BASE_URI + "?input=enter",
+ frame8: BASE_URI + "",
+ frame9: BASE_URI + "?tulip9=tulip9",
+};
+
+var gPendingLoad = 0; // Has to be set after depending on the frames number.
+
+function runTests()
+{
+ // We add a load event for the frames which will be called when the forms
+ // will be submitted.
+ var frames = [ document.getElementById('frame1'),
+ document.getElementById('frame2'),
+ document.getElementById('frame3'),
+ document.getElementById('frame3bis'),
+ document.getElementById('frame4'),
+ document.getElementById('frame5'),
+ document.getElementById('frame6'),
+ document.getElementById('frame7'),
+ document.getElementById('frame8'),
+ document.getElementById('frame9'),
+ ];
+ gPendingLoad = frames.length;
+
+ for (var i=0; i<frames.length; i++) {
+ frames[i].setAttribute('onload', "frameLoaded(this);");
+ }
+
+ /**
+ * We are going to focus each element before interacting with either for
+ * simulating the ENTER key (synthesizeKey) or a click (synthesizeMouse) or
+ * using .click(). This because it may be needed (ENTER) and because we want
+ * to have the element visible in the iframe.
+ *
+ * Focusing the first element (id='is') is launching the tests.
+ */
+ document.getElementById('is').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('is'), 5, 5, {});
+ document.getElementById('ii').focus();
+ }, {once: true});
+
+ document.getElementById('ii').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('ii'), 5, 5, {});
+ document.getElementById('bs').focus();
+ }, {once: true});
+
+ document.getElementById('bs').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('bs'), 5, 5, {});
+ document.getElementById('bsbis').focus();
+ }, {once: true});
+
+ document.getElementById('bsbis').addEventListener('focus', function(aEvent) {
+ document.getElementById('bsbis').click();
+ document.getElementById('is2').focus();
+ }, {once: true});
+
+ document.getElementById('is2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('ii2').focus();
+ }, {once: true});
+
+ document.getElementById('ii2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('bs2').focus();
+ }, {once: true});
+
+ document.getElementById('bs2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('enter').focus();
+ }, {once: true});
+
+ document.getElementById('enter').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('i8').focus();
+ }, {once: true});
+
+ document.getElementById('i8').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('i9').focus();
+ }, {once: true});
+
+ document.getElementById('i9').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ }, {once: true});
+
+ document.getElementById('is').focus();
+}
+
+function frameLoaded(aFrame) {
+ // Check if formaction/action has the correct behavior.
+ is(aFrame.contentWindow.location.href, gTestResults[aFrame.name],
+ "the method/formmethod attribute doesn't have the correct behavior");
+
+ if (--gPendingLoad == 0) {
+ SimpleTest.finish();
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug582412-2.html b/dom/html/test/test_bug582412-2.html
new file mode 100644
index 0000000000..b5ff8fc81e
--- /dev/null
+++ b/dom/html/test/test_bug582412-2.html
@@ -0,0 +1,199 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=566160
+-->
+<head>
+ <title>Test for Bug 566160</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566160">Mozilla Bug 566160</a>
+<p id="display"></p>
+<style>
+ iframe { width: 130px; height: 100px;}
+</style>
+<iframe name='frame1' id='frame1'></iframe>
+<iframe name='frame2' id='frame2'></iframe>
+<iframe name='frame3' id='frame3'></iframe>
+<iframe name='frame3bis' id='frame3bis'></iframe>
+<iframe name='frame4' id='frame4'></iframe>
+<iframe name='frame5' id='frame5'></iframe>
+<iframe name='frame6' id='frame6'></iframe>
+<iframe name='frame7' id='frame7'></iframe>
+<iframe name='frame8' id='frame8'></iframe>
+<iframe name='frame9' id='frame9'></iframe>
+<div id="content">
+ <!-- submit controls with formaction that are validated with a CLICK -->
+ <form target="frame1" action="form_submit_server.sjs" method="POST">
+ <input name='foo' value='foo'>
+ <input type='submit' id='is' formenctype='multipart/form-data'>
+ </form>
+ <form target="frame2" action="form_submit_server.sjs" method="POST">
+ <input name='bar' value='bar'>
+ <input type='image' id='ii' formenctype='multipart/form-data'>
+ </form>
+ <form target="frame3" action="form_submit_server.sjs" method="POST">
+ <input name='tulip' value='tulip'>
+ <button type='submit' id='bs' formenctype="multipart/form-data">submit</button>
+ </form>
+ <form target="frame3bis" action="form_submit_server.sjs" method="POST">
+ <input name='tulipbis' value='tulipbis'>
+ <button type='submit' id='bsbis' formenctype="multipart/form-data">submit</button>
+ </form>
+
+ <!-- submit controls with formaction that are validated with ENTER -->
+ <form target="frame4" action="form_submit_server.sjs" method="POST">
+ <input name='footulip' value='footulip'>
+ <input type='submit' id='is2' formenctype="multipart/form-data">
+ </form>
+ <form target="frame5" action="form_submit_server.sjs" method="POST">
+ <input name='foobar' value='foobar'>
+ <input type='image' id='ii2' formenctype="multipart/form-data">
+ </form>
+ <form target="frame6" action="form_submit_server.sjs" method="POST">
+ <input name='tulip2' value='tulip2'>
+ <button type='submit' id='bs2' formenctype="multipart/form-data">submit</button>
+ </form>
+
+ <!-- check that when submitting a from from an element
+ which is not a submit control, @formaction isn't used -->
+ <form target='frame7' action="form_submit_server.sjs" method="POST">
+ <input id='enter' name='input' value='enter' formenctype="multipart/form-data">
+ </form>
+
+ <!-- If formenctype isn't set, it's default value shouldn't be used -->
+ <form target="frame8" action="form_submit_server.sjs" method="POST" enctype="multipart/form-data">
+ <input name='tulip8' value='tulip8'>
+ <input type='submit' id='i8'>
+ </form>
+
+ <!-- If formenctype is set but has an invalid value, the default value should
+ be used. -->
+ <form target="frame9" action="form_submit_server.sjs" method="POST" enctype="multipart/form-data">
+ <input name='tulip9' value='tulip9'>
+ <input type='submit' id='i9' formenctype="">
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 566160 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ setTimeout(runTests, 0);
+});
+
+var gTestResults = {
+ frame1: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"foo\\\"\"},\"body\":\"foo\"}]',
+ frame2: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"bar\\\"\"},\"body\":\"bar\"},{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"x\\\"\"},\"body\":\"0\"},{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"y\\\"\"},\"body\":\"0\"}]',
+ frame3: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"tulip\\\"\"},\"body\":\"tulip\"}]',
+ frame3bis: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"tulipbis\\\"\"},\"body\":\"tulipbis\"}]',
+ frame4: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"footulip\\\"\"},\"body\":\"footulip\"}]',
+ frame5: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"foobar\\\"\"},\"body\":\"foobar\"},{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"x\\\"\"},\"body\":\"0\"},{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"y\\\"\"},\"body\":\"0\"}]',
+ frame6: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"tulip2\\\"\"},\"body\":\"tulip2\"}]',
+ frame7: '[]',
+ frame8: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"tulip8\\\"\"},\"body\":\"tulip8\"}]',
+ frame9: '[]',
+};
+
+var gPendingLoad = 0; // Has to be set after depending on the frames number.
+
+function runTests()
+{
+ // We add a load event for the frames which will be called when the forms
+ // will be submitted.
+ var frames = [ document.getElementById('frame1'),
+ document.getElementById('frame2'),
+ document.getElementById('frame3'),
+ document.getElementById('frame3bis'),
+ document.getElementById('frame4'),
+ document.getElementById('frame5'),
+ document.getElementById('frame6'),
+ document.getElementById('frame7'),
+ document.getElementById('frame8'),
+ document.getElementById('frame9'),
+ ];
+ gPendingLoad = frames.length;
+
+ for (var i=0; i<frames.length; i++) {
+ frames[i].setAttribute('onload', "frameLoaded(this);");
+ }
+
+ /**
+ * We are going to focus each element before interacting with either for
+ * simulating the ENTER key (synthesizeKey) or a click (synthesizeMouse) or
+ * using .click(). This because it may be needed (ENTER) and because we want
+ * to have the element visible in the iframe.
+ *
+ * Focusing the first element (id='is') is launching the tests.
+ */
+ document.getElementById('is').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('is'), 5, 5, {});
+ document.getElementById('ii').focus();
+ }, {once: true});
+
+ document.getElementById('ii').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('ii'), 5, 5, {});
+ document.getElementById('bs').focus();
+ }, {once: true});
+
+ document.getElementById('bs').addEventListener('focus', function(aEvent) {
+ synthesizeMouse(document.getElementById('bs'), 5, 5, {});
+ document.getElementById('bsbis').focus();
+ }, {once: true});
+
+ document.getElementById('bsbis').addEventListener('focus', function(aEvent) {
+ document.getElementById('bsbis').click();
+ document.getElementById('is2').focus();
+ }, {once: true});
+
+ document.getElementById('is2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('ii2').focus();
+ }, {once: true});
+
+ document.getElementById('ii2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('bs2').focus();
+ }, {once: true});
+
+ document.getElementById('bs2').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('enter').focus();
+ }, {once: true});
+
+ document.getElementById('enter').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('i8').focus();
+ }, {once: true});
+
+ document.getElementById('i8').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ document.getElementById('i9').focus();
+ }, {once: true});
+
+ document.getElementById('i9').addEventListener('focus', function(aEvent) {
+ synthesizeKey("KEY_Enter");
+ }, {once: true});
+
+ document.getElementById('is').focus();
+}
+
+function frameLoaded(aFrame) {
+ // Check if formaction/action has the correct behavior.
+ is(aFrame.contentDocument.documentElement.textContent, gTestResults[aFrame.name],
+ "the enctype/formenctype attribute doesn't have the correct behavior");
+
+ if (--gPendingLoad == 0) {
+ SimpleTest.finish();
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug583514.html b/dom/html/test/test_bug583514.html
new file mode 100644
index 0000000000..a3dead89aa
--- /dev/null
+++ b/dom/html/test/test_bug583514.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=583514
+-->
+<head>
+ <title>Test for Bug 583514</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=583514">Mozilla Bug 583514</a>
+<p id="display"></p>
+<div id="content">
+ <input>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 583514 **/
+
+var gExpectDivClick = false;
+var gExpectInputClick = false;
+
+var div = document.getElementById('content');
+var input = document.getElementsByTagName('input')[0];
+
+div.addEventListener('click', function() {
+ ok(gExpectDivClick, "click event received on div and expected status was: " +
+ gExpectDivClick);
+});
+
+input.addEventListener('click', function() {
+ ok(gExpectInputClick, "click event received on input and expected status was: " +
+ gExpectInputClick);
+});
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ var body = document.body;
+
+ body.addEventListener('click', function(aEvent) {
+ if (aEvent.target == input) {
+ body.removeEventListener('click', arguments.callee);
+ }
+
+ ok(true, "click event received on body");
+
+ SimpleTest.executeSoon(function() {
+ isnot(document.activeElement, input, "input shouldn't have been focused");
+ isnot(document.activeElement, div, "div shouldn't have been focused");
+
+ if (aEvent.target == input) {
+ SimpleTest.finish();
+ } else {
+ gExpectDivClick = true;
+ gExpectInputClick = true;
+ input.click();
+ }
+ });
+ });
+
+ gExpectDivClick = true;
+ div.click();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug583533.html b/dom/html/test/test_bug583533.html
new file mode 100644
index 0000000000..c0b8c92e95
--- /dev/null
+++ b/dom/html/test/test_bug583533.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+ <!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=583533
+-->
+ <head>
+ <title>Test for Bug 583514</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=583533">Mozilla Bug 583533</a>
+ <p id="display"></p>
+ <div id="content">
+ <div id="e" accesskey="a">
+ </div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+ /** Test for Bug 583533 **/
+
+ var sbs = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1'].
+ getService(SpecialPowers.Ci.nsIStringBundleService);
+ var bundle = sbs.createBundle("chrome://global-platform/locale/platformKeys.properties");
+
+ var shiftText = bundle.GetStringFromName("VK_SHIFT");
+ var altText = bundle.GetStringFromName("VK_ALT");
+ var controlText = bundle.GetStringFromName("VK_CONTROL");
+ var metaText = bundle.GetStringFromName("VK_COMMAND_OR_WIN");
+ var separatorText = bundle.GetStringFromName("MODIFIER_SEPARATOR");
+
+ var modifier = SpecialPowers.getIntPref("ui.key.contentAccess");
+
+ var isShift;
+ var isAlt;
+ var isControl;
+ var isMeta;
+
+ is(modifier < 16 && modifier >= 0, true, "Modifier in range");
+
+ // There are no consts for the mask of this prefs.
+ if (modifier & 8) {
+ isMeta = true;
+ }
+ if (modifier & 1) {
+ isShift = true;
+ }
+ if (modifier & 2) {
+ isControl = true;
+ }
+ if (modifier & 4) {
+ isAlt = true;
+ }
+
+ var label = "";
+
+ if (isControl)
+ label += controlText + separatorText;
+ if (isMeta)
+ label += metaText + separatorText;
+ if (isAlt)
+ label += altText + separatorText;
+ if (isShift)
+ label += shiftText + separatorText;
+
+ label += document.getElementById("e").accessKey;
+
+ is(label, document.getElementById("e").accessKeyLabel, "JS and C++ agree on accessKeyLabel");
+
+ /** Test for Bug 808964 **/
+
+ var div = document.createElement("div");
+ document.body.appendChild(div);
+
+ is(div.accessKeyLabel, "", "accessKeyLabel should be empty string");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug586763.html b/dom/html/test/test_bug586763.html
new file mode 100644
index 0000000000..b396cb8bc9
--- /dev/null
+++ b/dom/html/test/test_bug586763.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=586763
+-->
+<title>Test for Bug 586763</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=586763">Mozilla Bug 586763</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script>
+/** Test for Bug 586763 **/
+var tests = [
+ ["ol", "start", 1],
+ ["li", "value", 0],
+ ["object", "hspace", 0],
+ ["object", "vspace", 0],
+ ["img", "hspace", 0],
+ ["img", "vspace", 0],
+ ["video", "height", 0],
+ ["video", "width", 0],
+ ["pre", "width", 0],
+ ["textarea", "cols", 20],
+ ["textarea", "rows", 2],
+ ["span", "tabIndex", -1],
+ ["frame", "tabIndex", 0],
+ ["a", "tabIndex", 0],
+ ["area", "tabIndex", 0],
+ ["button", "tabIndex", 0],
+ ["input", "tabIndex", 0],
+ ["object", "tabIndex", 0],
+ ["select", "tabIndex", 0],
+ ["textarea", "tabIndex", 0],
+];
+
+for (var i = 0; i < tests.length; i++) {
+ is(document.createElement(tests[i][0])[tests[i][1]], tests[i][2], "Reflected attribute " + tests[i][0] + "." + tests[i][1] + " should default to " + tests[i][2]);
+}
+</script>
+</pre>
diff --git a/dom/html/test/test_bug586786.html b/dom/html/test/test_bug586786.html
new file mode 100644
index 0000000000..3cfa7fa4b4
--- /dev/null
+++ b/dom/html/test/test_bug586786.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=586786
+-->
+<head>
+ <title>Test for Bug 586786</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=586786">Mozilla Bug 586786</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 586786 **/
+
+var elements = ["col", "colgroup", "tbody", "tfoot", "thead", "tr", "td", "th"];
+
+for(var i = 0; i < elements.length; i++)
+{
+ reflectString({
+ element: document.createElement(elements[i]),
+ attribute: "align",
+ otherValues: [ "left", "right", "center", "justify", "char" ]
+ });
+
+ reflectString({
+ element: document.createElement(elements[i]),
+ attribute: "vAlign",
+ otherValues: [ "top", "middle", "bottom", "baseline" ]
+ });
+
+ reflectString({
+ element: document.createElement(elements[i]),
+ attribute: {idl: "ch", content: "char"}
+ });
+}
+
+// table.border, table.width
+reflectString({
+ element: document.createElement("table"),
+ attribute: "border"
+});
+
+reflectString({
+ element: document.createElement("table"),
+ attribute: "width"
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug587469.html b/dom/html/test/test_bug587469.html
new file mode 100644
index 0000000000..b0e941296a
--- /dev/null
+++ b/dom/html/test/test_bug587469.html
@@ -0,0 +1,41 @@
+<!-- Quirks -->
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=587469
+-->
+<head>
+ <title>Test for Bug 587469</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=587469">Mozilla Bug 587469</a>
+<p id="display">
+<map name=a></map>
+<map name=a><area shape=rect coords=25,25,75,75 href=#fail></map>
+<img usemap=#a src=image.png>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 587469 **/
+SimpleTest.waitForExplicitFinish();
+function finish() {
+ is(location.hash, "", "Should not have changed the hash.");
+ SimpleTest.finish();
+}
+SimpleTest.waitForFocus(function() {
+ synthesizeMouse(document.getElementsByTagName("img")[0], 50, 50, {});
+ // Hit the event loop twice before doing the test
+ setTimeout(function() {
+ setTimeout(finish, 0);
+ }, 0);
+});
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug589.html b/dom/html/test/test_bug589.html
new file mode 100644
index 0000000000..3a4ac666a7
--- /dev/null
+++ b/dom/html/test/test_bug589.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=589
+-->
+<head>
+ <title>Test for Bug 589</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+
+<style type="text/css">
+.letters {list-style-type: upper-alpha;}
+.numbers {list-style-type: decimal;}
+</style>
+
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=589">Mozilla Bug 589</a>
+<p id="display"></p>
+<div id="content" >
+
+<OL id="thelist" class="letters">
+<LI id="liA">This list should feature...
+<LI id="liB">...letters for each item...
+<LI id="li3" class="numbers">...except this one.
+</OL>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 589 **/
+
+is(computedStyle($("liA"),"list-style-type"),"upper-alpha");
+is(computedStyle($("liB"),"list-style-type"),"upper-alpha");
+is(computedStyle($("li3"),"list-style-type"),"decimal");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug590353-1.html b/dom/html/test/test_bug590353-1.html
new file mode 100644
index 0000000000..af79317f36
--- /dev/null
+++ b/dom/html/test/test_bug590353-1.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=590353
+-->
+<head>
+ <title>Test for Bug 590353</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=590353">Mozilla Bug 590353</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 590353 **/
+
+var testData = ['checkbox', 'radio'];
+
+for (var data of testData) {
+ var e = document.createElement('input');
+ e.type = data;
+ e.checked = true;
+ e.value = "foo";
+
+ is(e.value, "foo", "foo should be the new " + data + "value");
+ is(e.getAttribute('value'), "foo", "foo should be the new " + data +
+ " value attribute value");
+ ok(e.checked, data + " should still be checked");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug590353-2.html b/dom/html/test/test_bug590353-2.html
new file mode 100644
index 0000000000..9ef6e40e23
--- /dev/null
+++ b/dom/html/test/test_bug590353-2.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=590353
+-->
+<head>
+ <title>Test for Bug 590353</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=590353">Mozilla Bug 590353</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 590353 **/
+
+var testData = [
+ [ "text", "foo", "" ],
+ [ "email", "foo@bar.com", "" ],
+ [ "url", "http:///foo.com", "" ],
+ [ "tel", "555 555 555 555", "" ],
+ [ "search", "foo", "" ],
+ [ "password", "secret", "" ],
+ [ "hidden", "foo", "foo" ],
+ [ "button", "foo", "foo" ],
+ [ "reset", "foo", "foo" ],
+ [ "submit", "foo", "foo" ],
+ [ "checkbox", true, false ],
+ [ "radio", true, false ],
+ [ "file", "590353_file", "" ],
+];
+
+function createFileWithData(fileName, fileData) {
+ return new File([new Blob([fileData], { type: "text/plain" })], fileName);
+}
+
+var content = document.getElementById('content');
+var form = document.createElement('form');
+content.appendChild(form);
+
+for (var data of testData) {
+ var e = document.createElement('input');
+ e.type = data[0];
+
+ if (data[0] == 'checkbox' || data[0] == 'radio') {
+ e.checked = data[1];
+ } else if (data[0] == 'file') {
+ var file = createFileWithData(data[1], "file content");
+ SpecialPowers.wrap(e).mozSetFileArray([file]);
+ } else {
+ e.value = data[1];
+ }
+
+ form.appendChild(e);
+}
+
+form.reset();
+
+var size = form.elements.length;
+for (var i=0; i<size; ++i) {
+ var e = form.elements[i];
+
+ if (e.type == 'radio' || e.type == 'checkbox') {
+ is(e.checked, testData[i][2],
+ "the element checked value should be " + testData[i][2]);
+ } else {
+ is(e.value, testData[i][2],
+ "the element value should be " + testData[i][2]);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug590363.html b/dom/html/test/test_bug590363.html
new file mode 100644
index 0000000000..de12079a71
--- /dev/null
+++ b/dom/html/test/test_bug590363.html
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=590363
+-->
+<head>
+ <title>Test for Bug 590363</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=590363">Mozilla Bug 590363</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 590363 **/
+
+var testData = [
+ /* type to test | is the value reset when changing to file then reverting */
+ [ "button", false ],
+ [ "checkbox", false ],
+ [ "hidden", false ],
+ [ "reset", false ],
+ [ "image", false ],
+ [ "radio", false ],
+ [ "submit", false ],
+ [ "tel", true ],
+ [ "text", true ],
+ [ "url", true ],
+ [ "email", true ],
+ [ "search", true ],
+ [ "password", true ],
+ [ "number", true ],
+ [ "date", true ],
+ [ "time", true ],
+ [ "range", true ],
+ [ "color", true ],
+ [ 'month', true ],
+ [ 'week', true ],
+ [ 'datetime-local', true ]
+ // 'file' is treated separatly.
+];
+
+var nonTrivialSanitizing = [ 'number', 'date', 'time', 'color', 'month', 'week',
+ 'datetime-local' ];
+
+var length = testData.length;
+for (var i=0; i<length; ++i) {
+ for (var j=0; j<length; ++j) {
+ var e = document.createElement('input');
+ e.type = testData[i][0];
+
+ var expectedValue;
+
+ // range will sanitize its value to 50 (the default) if it isn't a valid
+ // number. We need to handle that specially.
+ if (testData[j][0] == 'range' || testData[i][0] == 'range') {
+ if (testData[j][0] == 'date' || testData[j][0] == 'time' ||
+ testData[j][0] == 'month' || testData[j][0] == 'week' ||
+ testData[j][0] == 'datetime-local') {
+ expectedValue = '';
+ } else if (testData[j][0] == 'color') {
+ expectedValue = '#000000';
+ } else {
+ expectedValue = '50';
+ }
+ } else if (testData[i][0] == 'color' || testData[j][0] == 'color') {
+ if (testData[j][0] == 'number' || testData[j][0] == 'date' ||
+ testData[j][0] == 'time' || testData[j][0] == 'month' ||
+ testData[j][0] == 'week' || testData[j][0] == 'datetime-local') {
+ expectedValue = ''
+ } else {
+ expectedValue = '#000000';
+ }
+ } else if (nonTrivialSanitizing.includes(testData[i][0]) &&
+ nonTrivialSanitizing.includes(testData[j][0])) {
+ expectedValue = '';
+ } else if (testData[i][0] == 'number' || testData[j][0] == 'number') {
+ expectedValue = '42';
+ } else if (testData[i][0] == 'date' || testData[j][0] == 'date') {
+ expectedValue = '2012-12-21';
+ } else if (testData[i][0] == 'time' || testData[j][0] == 'time') {
+ expectedValue = '21:21';
+ } else if (testData[i][0] == 'month' || testData[j][0] == 'month') {
+ expectedValue = '2013-03';
+ } else if (testData[i][0] == 'week' || testData[j][0] == 'week') {
+ expectedValue = '2016-W35';
+ } else if (testData[i][0] == 'datetime-local' ||
+ testData[j][0] == 'datetime-local') {
+ expectedValue = '2016-11-07T16:40';
+ } else {
+ expectedValue = "foo";
+ }
+ e.value = expectedValue;
+
+ e.type = testData[j][0];
+ is(e.value, expectedValue, ".value should still return the same value after " +
+ "changing type from " + testData[i][0] + " to " + testData[j][0]);
+ }
+}
+
+// For type='file' .value doesn't behave the same way.
+// We are just going to check that we do not loose the value.
+for (var data of testData) {
+ var e = document.createElement('input');
+ e.type = data[0];
+ e.value = 'foo';
+ e.type = 'file';
+ e.type = data[0];
+
+ if (data[0] == 'range') {
+ is(e.value, '50', ".value should still return the same value after " +
+ "changing type from " + data[0] + " to 'file' then reverting to " + data[0]);
+ } else if (data[0] == 'color') {
+ is(e.value, '#000000', ".value should have been reset to the default color after " +
+ "changing type from " + data[0] + " to 'file' then reverting to " + data[0]);
+ } else if (data[1]) {
+ is(e.value, '', ".value should have been reset to the empty string after " +
+ "changing type from " + data[0] + " to 'file' then reverting to " + data[0]);
+ } else {
+ is(e.value, 'foo', ".value should still return the same value after " +
+ "changing type from " + data[0] + " to 'file' then reverting to " + data[0]);
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug592802.html b/dom/html/test/test_bug592802.html
new file mode 100644
index 0000000000..e8b30d84c8
--- /dev/null
+++ b/dom/html/test/test_bug592802.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=592802
+-->
+<head>
+ <title>Test for Bug 592802</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=592802">Mozilla Bug 592802</a>
+<p id="display"></p>
+<div id="content">
+ <input id='a' type='file'>
+ <input id='a2' type='file'>
+</div>
+<button id='b' onclick="document.getElementById('a').click();">Show Filepicker</button>
+<button id='b2' onclick="document.getElementById('a2').click();">Show Filepicker</button>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 592802 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+var testData = [
+/* visibility | display | multiple */
+ [ "", "", false ],
+ [ "hidden", "", false ],
+ [ "", "none", false ],
+ [ "", "", true ],
+ [ "hidden", "", true ],
+ [ "", "none", true ],
+];
+
+var testCounter = 0;
+var testNb = testData.length;
+
+function finished()
+{
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForFocus(function() {
+ // mockFilePicker will simulate a cancel for the first time the file picker will be shown.
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+
+ var b2 = document.getElementById('b2');
+ b2.focus(); // Be sure the element is visible.
+ document.getElementById('b2').addEventListener("change", function(aEvent) {
+ ok(false, "When cancel is received, change should not fire");
+ }, {once: true});
+ b2.click();
+
+ // Now, we can launch tests when file picker isn't canceled.
+ MockFilePicker.useBlobFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ var b = document.getElementById('b');
+ b.focus(); // Be sure the element is visible.
+
+ document.getElementById('a').addEventListener("change", function(aEvent) {
+ ok(true, "change event correctly sent");
+ ok(aEvent.bubbles, "change event should bubble");
+ ok(!aEvent.cancelable, "change event should not be cancelable");
+ testCounter++;
+
+ if (testCounter >= testNb) {
+ aEvent.target.removeEventListener("change", arguments.callee);
+ SimpleTest.executeSoon(finished);
+ } else {
+ var data = testData[testCounter];
+ var a = document.getElementById('a');
+ a.style.visibility = data[0];
+ a.style.display = data[1];
+ a.multiple = data[2];
+
+ SimpleTest.executeSoon(function() {
+ b.click();
+ });
+ }
+ });
+
+ b.click();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug593689.html b/dom/html/test/test_bug593689.html
new file mode 100644
index 0000000000..14cfa8f0fb
--- /dev/null
+++ b/dom/html/test/test_bug593689.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=593689
+-->
+<head>
+ <title>Test for Bug 593689</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=593689">Mozilla Bug 593689</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 593689 **/
+function testWidth(w) {
+ var img = new Image(w);
+ is(img.width, w|0, "Unexpected handling of '" + w + "' width");
+}
+
+testWidth(1);
+testWidth(0);
+testWidth("xxx");
+testWidth(null);
+testWidth(undefined);
+testWidth({});
+testWidth({ valueOf() { return 10; } });
+
+function testHeight(h) {
+ var img = new Image(100, h);
+ is(img.height, h|0, "Unexpected handling of '" + h + "' height");
+}
+
+testHeight(1);
+testHeight(0);
+testHeight("xxx");
+testHeight(null);
+testHeight(undefined);
+testHeight({});
+testHeight({ valueOf() { return 10; } });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug595429.html b/dom/html/test/test_bug595429.html
new file mode 100644
index 0000000000..9a9215bd6c
--- /dev/null
+++ b/dom/html/test/test_bug595429.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=595429
+-->
+<head>
+ <title>Test for Bug 595429</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=595429">Mozilla Bug 595429</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 595429 **/
+
+var fieldset = document.createElement("fieldset");
+
+ok("name" in fieldset, "<fieldset> should have a name IDL attribute");
+
+var testData = [
+ "",
+ " ",
+ "foo",
+ "foo bar",
+];
+
+is(fieldset.getAttribute("name"), null,
+ "By default, name content attribute should be null");
+is(fieldset.name, "",
+ "By default, name IDL attribute should be the empty string");
+
+for (var data of testData) {
+ fieldset.setAttribute("name", data);
+ is(fieldset.getAttribute("name"), data,
+ "name content attribute should be " + data);
+ is(fieldset.name, data, "name IDL attribute should be " + data);
+
+ fieldset.setAttribute("name", "");
+ fieldset.name = data;
+ is(fieldset.getAttribute("name"), data,
+ "name content attribute should be " + data);
+ is(fieldset.name, data, "name IDL attribute should be " + data);
+}
+
+fieldset.removeAttribute("name");
+is(fieldset.getAttribute("name"), null,
+ "name content attribute should be null");
+is(fieldset.name, "", "name IDL attribute should be the empty string");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug595447.html b/dom/html/test/test_bug595447.html
new file mode 100644
index 0000000000..e647364879
--- /dev/null
+++ b/dom/html/test/test_bug595447.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=595447
+-->
+<head>
+ <title>Test for Bug 595447</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=595447">Mozilla Bug 595447</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 595447 **/
+
+var fieldset = document.createElement("fieldset");
+
+ok("type" in fieldset, "fieldset element should have a type IDL attribute");
+is(fieldset.type, "fieldset", "fieldset.type should return 'fieldset'");
+fieldset.type = "foo";
+is(fieldset.type, "fieldset", "fieldset.type is readonly");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug595449.html b/dom/html/test/test_bug595449.html
new file mode 100644
index 0000000000..5649b246ae
--- /dev/null
+++ b/dom/html/test/test_bug595449.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=595449
+-->
+<head>
+ <title>Test for Bug 595449</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=595449">Mozilla Bug 595449</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 595449 **/
+
+var fieldset = document.createElement("fieldset");
+
+ok("elements" in fieldset,
+ "fieldset element should have an 'elements' IDL attribute");
+
+ok(fieldset.elements instanceof HTMLCollection,
+ "fieldset.elements should be an instance of HTMLCollection");
+
+// https://www.w3.org/Bugs/Public/show_bug.cgi?id=23356
+todo(fieldset.elements instanceof HTMLFormControlsCollection,
+ "fieldset.elements should be an instance of HTMLFormControlsCollection");
+
+is(fieldset.elements.length, 0, "Nothing should be in fieldset.elements");
+
+var oldElements = fieldset.elements;
+
+is(fieldset.elements, oldElements,
+ "fieldset.elements should always return the same object");
+
+var tmpElement = document.createElement("input");
+
+fieldset.appendChild(tmpElement);
+
+is(fieldset.elements.length, 1,
+ "fieldset.elements should now contain one element");
+
+is(fieldset.elements[0], tmpElement,
+ "fieldset.elements[0] should be the input element");
+
+tmpElement.name = "foo";
+is(fieldset.elements.foo, tmpElement,
+ "we should be able to access to an element using it's name as a property on .elements");
+
+is(fieldset.elements, oldElements,
+ "fieldset.elements should always return the same object");
+
+fieldset.removeChild(tmpElement);
+
+var testData = [
+ [ "<input>", 1 , [ HTMLInputElement ] ],
+ [ "<button></button>", 1, [ HTMLButtonElement ] ],
+ [ "<button><input></button>", 2, [ HTMLButtonElement, HTMLInputElement ] ],
+ [ "<object>", 1, [ HTMLObjectElement ] ],
+ [ "<output></output>", 1, [ HTMLOutputElement ] ],
+ [ "<select></select>", 1, [ HTMLSelectElement ] ],
+ [ "<select><option>foo</option></select>", 1, [ HTMLSelectElement ] ],
+ [ "<select><option>foo</option><input></select>", 2, [ HTMLSelectElement, HTMLInputElement ] ],
+ [ "<textarea></textarea>", 1, [ HTMLTextAreaElement ] ],
+ [ "<label>foo</label>", 0 ],
+ [ "<progress>", 0 ],
+ [ "<meter>", 0 ],
+ [ "<keygen>", 0 ],
+ [ "<legend></legend>", 0 ],
+ [ "<legend><input></legend>", 1, [ HTMLInputElement ] ],
+ [ "<legend><input></legend><legend><input></legend>", 2, [ HTMLInputElement, HTMLInputElement ] ],
+ [ "<legend><input></legend><input>", 2, [ HTMLInputElement, HTMLInputElement ] ],
+ [ "<fieldset></fieldset>", 1, [ HTMLFieldSetElement ] ],
+ [ "<fieldset><input></fieldset>", 2, [ HTMLFieldSetElement, HTMLInputElement ] ],
+ [ "<fieldset><fieldset><input></fieldset></fieldset>", 3, [ HTMLFieldSetElement, HTMLFieldSetElement, HTMLInputElement ] ],
+ [ "<button></button><fieldset></fieldset><input><keygen><object><output></output><select></select><textarea></textarea>", 7, [ HTMLButtonElement, HTMLFieldSetElement, HTMLInputElement, HTMLObjectElement, HTMLOutputElement, HTMLSelectElement, HTMLTextAreaElement ] ],
+];
+
+for (var data of testData) {
+ fieldset.innerHTML = data[0];
+ is(fieldset.elements.length, data[1],
+ "fieldset.elements should contain " + data[1] + " elements");
+
+ for (var i=0; i<data[1]; ++i) {
+ ok(fieldset.elements[i] instanceof data[2][i],
+ "fieldset.elements[" + i + "] should be instance of " + data[2][i])
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug596350.html b/dom/html/test/test_bug596350.html
new file mode 100644
index 0000000000..72e2f7ce73
--- /dev/null
+++ b/dom/html/test/test_bug596350.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596350
+-->
+<head>
+ <title>Test for Bug 596350</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596350">Mozilla Bug 596350</a>
+<p id="display"></p>
+<div id="content">
+ <object></object>
+ <object data="iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMsALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0vr4MkhoXe0rZigAAAABJRU5ErkJggg=="></object>
+ <object data="data:text/html,foo"></object>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 596350 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTests);
+
+var testData = [
+// Object 0
+ [ 0, null, 0 ],
+ [ 0, "1", 1 ],
+ [ 0, "-1", -1 ],
+ [ 0, "0", 0 ],
+ [ 0, "foo", 0 ],
+// Object 1
+ [ 1, null, 0 ],
+ [ 1, "1", 1 ],
+// Object 2
+ [ 2, null, 0 ],
+ [ 2, "1", 1 ],
+ [ 2, "-1", -1 ],
+];
+
+var objects = document.getElementsByTagName("object");
+
+function runTests()
+{
+ for (var data of testData) {
+ var obj = objects[data[0]];
+
+ if (data[1]) {
+ obj.setAttribute("tabindex", data[1]);
+ }
+
+ is(obj.tabIndex, data[2], "tabIndex value should be " + data[2]);
+
+ obj.removeAttribute("tabindex");
+ }
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug596511.html b/dom/html/test/test_bug596511.html
new file mode 100644
index 0000000000..42b93e4632
--- /dev/null
+++ b/dom/html/test/test_bug596511.html
@@ -0,0 +1,237 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596511
+-->
+<head>
+ <title>Test for Bug 596511</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ select:valid { background-color: green; }
+ select:invalid { background-color: red; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596511">Mozilla Bug 596511</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script>
+
+/** Test for Bug 596511 **/
+
+function checkNotSufferingFromBeingMissing(element, aTodo)
+{
+ if (aTodo) {
+ ok = todo;
+ is = todo_is;
+ }
+
+ ok(!element.validity.valueMissing,
+ "Element should not suffer from value missing");
+ ok(element.validity.valid, "Element should be valid");
+ ok(element.checkValidity(), "Element should be valid");
+
+ is(element.validationMessage, "",
+ "Validation message should be the empty string");
+
+ ok(element.matches(":valid"), ":valid pseudo-class should apply");
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(0, 128, 0)", ":valid pseudo-class should apply");
+
+ if (aTodo) {
+ ok = SimpleTest.ok;
+ is = SimpleTest.is;
+ }
+}
+
+function checkSufferingFromBeingMissing(element, aTodo)
+{
+ if (aTodo) {
+ ok = todo;
+ is = todo_is;
+ }
+
+ ok(element.validity.valueMissing, "Element should suffer from value missing");
+ ok(!element.validity.valid, "Element should not be valid");
+ ok(!element.checkValidity(), "Element should not be valid");
+
+ is(element.validationMessage, "Please select an item in the list.",
+ "Validation message is wrong");
+
+ is(window.getComputedStyle(element).getPropertyValue('background-color'),
+ "rgb(255, 0, 0)", ":invalid pseudo-class should apply");
+
+ if (aTodo) {
+ ok = SimpleTest.ok;
+ is = SimpleTest.is;
+ }
+}
+
+function checkRequiredAttribute(element)
+{
+ ok('required' in element, "select should have a required attribute");
+
+ ok(!element.required, "select required attribute should be disabled");
+ is(element.getAttribute('required'), null,
+ "select required attribute should be disabled");
+
+ element.required = true;
+ ok(element.required, "select required attribute should be enabled");
+ isnot(element.getAttribute('required'), null,
+ "select required attribute should be enabled");
+
+ element.removeAttribute('required');
+ element.setAttribute('required', '');
+ ok(element.required, "select required attribute should be enabled");
+ isnot(element.getAttribute('required'), null,
+ "select required attribute should be enabled");
+
+ element.removeAttribute('required');
+ ok(!element.required, "select required attribute should be disabled");
+ is(element.getAttribute('required'), null,
+ "select required attribute should be disabled");
+}
+
+function checkRequiredAndOptionalSelectors(element)
+{
+ is(document.querySelector("select:optional"), element,
+ "select should be optional");
+ is(document.querySelector("select:required"), null,
+ "select shouldn't be required");
+
+ element.required = true;
+
+ is(document.querySelector("select:optional"), null,
+ "select shouldn't be optional");
+ is(document.querySelector("select:required"), element,
+ "select should be required");
+
+ element.required = false;
+}
+
+function checkInvalidWhenValueMissing(element)
+{
+ checkNotSufferingFromBeingMissing(select);
+
+ element.required = true;
+ checkSufferingFromBeingMissing(select);
+
+ /**
+ * Non-multiple and size=1.
+ */
+ select.appendChild(new Option());
+ checkSufferingFromBeingMissing(select);
+
+ // When removing the required attribute, element should not be invalid.
+ element.required = false;
+ checkNotSufferingFromBeingMissing(select);
+
+ element.required = true;
+ select.options[0].textContent = "foo";
+ // TODO: having that working would require us to add a mutation observer on
+ // the select element.
+ checkNotSufferingFromBeingMissing(select, true);
+
+ select.remove(0);
+ checkSufferingFromBeingMissing(select);
+
+ select.add(new Option("foo", "foo"), null);
+ checkNotSufferingFromBeingMissing(select);
+
+ select.add(new Option(), null);
+ checkNotSufferingFromBeingMissing(select);
+
+ // The placeholder label can only be the first option, so a selected empty second option is valid
+ select.options[1].selected = true;
+ checkNotSufferingFromBeingMissing(select);
+
+ select.selectedIndex = 0;
+ checkNotSufferingFromBeingMissing(select);
+
+ select.add(select.options[0]);
+ select.selectedIndex = 0;
+ checkSufferingFromBeingMissing(select);
+
+ select.remove(0);
+ checkNotSufferingFromBeingMissing(select);
+
+ select.options[0].disabled = true;
+ // TODO: having that working would require us to add a mutation observer on
+ // the select element.
+ checkSufferingFromBeingMissing(select, true);
+
+ select.options[0].disabled = false
+ select.remove(0);
+ checkSufferingFromBeingMissing(select);
+
+ var option = new Option("foo", "foo");
+ option.disabled = true;
+ select.add(option, null);
+ select.add(new Option("bar"), null);
+ option.selected = true;
+ checkNotSufferingFromBeingMissing(select);
+
+ select.remove(0);
+ select.remove(0);
+
+ /**
+ * Non-multiple and size > 1.
+ * Everything should be the same except moving the selection.
+ */
+ select.multiple = false;
+ select.size = 4;
+ checkSufferingFromBeingMissing(select);
+
+ // Setting defaultSelected to true should not make the option selected
+ select.add(new Option("", "", true), null);
+ checkSufferingFromBeingMissing(select);
+ select.remove(0);
+
+ select.add(new Option("", "", true, true), null);
+ checkNotSufferingFromBeingMissing(select);
+
+ select.add(new Option("foo", "foo"), null);
+ select.remove(0);
+ checkSufferingFromBeingMissing(select);
+
+ select.options[0].selected = true;
+ checkNotSufferingFromBeingMissing(select);
+
+ select.remove(0);
+
+ /**
+ * Multiple, any size.
+ * We can select more than one element and at least needs a value.
+ */
+ select.multiple = true;
+ select.size = 4;
+ checkSufferingFromBeingMissing(select);
+
+ select.add(new Option("", "", true), null);
+ checkSufferingFromBeingMissing(select);
+
+ select.add(new Option("", "", true), null);
+ checkSufferingFromBeingMissing(select);
+
+ select.add(new Option("foo"), null);
+ checkSufferingFromBeingMissing(select);
+
+ select.options[2].selected = true;
+ checkNotSufferingFromBeingMissing(select);
+}
+
+var select = document.createElement("select");
+var content = document.getElementById('content');
+content.appendChild(select);
+
+checkRequiredAttribute(select);
+checkRequiredAndOptionalSelectors(select);
+checkInvalidWhenValueMissing(select);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug598643.html b/dom/html/test/test_bug598643.html
new file mode 100644
index 0000000000..12f91fdec4
--- /dev/null
+++ b/dom/html/test/test_bug598643.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=598643
+-->
+<head>
+ <title>Test for Bug 598643</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=598643">Mozilla Bug 598643</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 598643 **/
+
+function createFileWithData(fileName, fileData)
+{
+ return new File([new Blob([fileData], { type: "text/plain" })], fileName);
+}
+
+function testFileControl(aElement)
+{
+ aElement.type = 'file';
+
+ var file = createFileWithData("file_bug598643", "file content");
+ SpecialPowers.wrap(aElement).mozSetFileArray([file]);
+
+ ok(aElement.validity.valid, "the file control should be valid");
+ ok(!aElement.validity.tooLong,
+ "the file control shouldn't suffer from being too long");
+}
+
+var types = [
+ // These types can be too long.
+ [ "text", "email", "password", "url", "search", "tel" ],
+ // These types can't be too long.
+ [ "radio", "checkbox", "submit", "button", "reset", "image", "hidden",
+ 'number', 'range', 'date', 'time', 'color', 'month', 'week',
+ 'datetime-local' ],
+];
+
+var input = document.createElement("input");
+input.maxLength = 1;
+input.value = "foo";
+
+// Too long types.
+for (type of types[0]) {
+ input.type = type
+ if (type == 'email') {
+ input.value = "foo@bar.com";
+ } else if (type == 'url') {
+ input.value = 'http://foo.org';
+ }
+
+ todo(!input.validity.valid, "the element should be invalid [type=" + type + "]");
+ todo(input.validity.tooLong,
+ "the element should suffer from being too long [type=" + type + "]");
+
+ if (type == 'email' || type == 'url') {
+ input.value = 'foo';
+ }
+}
+
+// Not too long types.
+for (type of types[1]) {
+ input.type = type
+ ok(input.validity.valid, "the element should be valid [type=" + type + "]");
+ ok(!input.validity.tooLong,
+ "the element shouldn't suffer from being too long [type=" + type + "]");
+}
+
+testFileControl(input);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug598833-1.html b/dom/html/test/test_bug598833-1.html
new file mode 100644
index 0000000000..5fbbe221e5
--- /dev/null
+++ b/dom/html/test/test_bug598833-1.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=598833
+-->
+<head>
+ <title>Test for Bug 598833</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=598833">Mozilla Bug 598833</a>
+<p id="display">
+ <fieldset disabled>
+ <select id="s" multiple required>
+ <option>one</option>
+ </select>
+ </fieldset>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 598833 **/
+var s = $("s");
+is(s.matches(":invalid"), false, "Disabled select should not be invalid");
+is(s.matches(":valid"), false, "Disabled select should not be valid");
+var p = s.parentNode;
+p.removeChild(s);
+is(s.matches(":invalid"), true,
+ "Required valueless select not in tree should be invalid");
+is(s.matches(":valid"), false,
+ "Required valueless select not in tree should not be valid");
+p.appendChild(s);
+p.disabled = false;
+is(s.matches(":invalid"), true,
+ "Required valueless select should be invalid");
+is(s.matches(":valid"), false,
+ "Required valueless select should not be valid");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug600155.html b/dom/html/test/test_bug600155.html
new file mode 100644
index 0000000000..893dfe31f1
--- /dev/null
+++ b/dom/html/test/test_bug600155.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=600155
+-->
+<head>
+ <title>Test for Bug 600155</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=600155">Mozilla Bug 600155</a>
+<p id="display"></p>
+<div id='content' style='display:none;'>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 600155 **/
+
+var subjectForConstraintValidation = [ "input", "select", "textarea" ];
+var content = document.getElementById('content');
+
+for (var eName of subjectForConstraintValidation) {
+ var e = document.createElement(eName);
+ content.appendChild(e);
+ e.setCustomValidity("foo");
+ if ("required" in e) {
+ e.required = true;
+ } else {
+ e.setCustomValidity("bar");
+ }
+
+ // At this point, the element is invalid.
+ is(e.validationMessage, "foo",
+ "the validation message should be the author one");
+
+ content.removeChild(e);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug601030.html b/dom/html/test/test_bug601030.html
new file mode 100644
index 0000000000..f9a277f471
--- /dev/null
+++ b/dom/html/test/test_bug601030.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=601030
+-->
+<head>
+ <title>Test for Bug 601030</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=601030">Mozilla Bug 601030</a>
+<p id="display"></p>
+<div id="content">
+ <iframe src="data:text/html,<input autofocus>"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 601030 **/
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var f = document.createElement("iframe");
+ var content = document.getElementById('content');
+
+ f.addEventListener("load", function() {
+ SimpleTest.executeSoon(function() {
+ isnot(document.activeElement, f,
+ "autofocus should not work when another frame is inserted in the document");
+
+ content.removeChild(f);
+ content.removeChild(document.getElementsByTagName('iframe')[0]);
+ f = document.createElement('iframe');
+ f.addEventListener("load", function() {
+ isnot(document.activeElement, f,
+ "autofocus should not work in a frame if the top document is already loaded");
+ SimpleTest.finish();
+ }, {once: true});
+ f.src = "data:text/html,<input autofocus>";
+ content.appendChild(f);
+ });
+ }, {once: true});
+
+ f.src = "data:text/html,<input autofocus>";
+ content.appendChild(f);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug605124-1.html b/dom/html/test/test_bug605124-1.html
new file mode 100644
index 0000000000..d252987ee9
--- /dev/null
+++ b/dom/html/test/test_bug605124-1.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=605124
+-->
+<head>
+ <title>Test for Bug 605124</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=605124">Mozilla Bug 605124</a>
+<p id="display"></p>
+<div id="content">
+ <form>
+ <textarea required></textarea>
+ <input required>
+ <select required></select>
+ <button type='submit'></button>
+ </form>
+
+ <table>
+ <form>
+ <tr>
+ <textarea required></textarea>
+ <input required>
+ <select required></select>
+ <button type='submit'></button>
+ </tr>
+ </form>
+ </table>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 605124 **/
+
+function checkPseudoClass(aElement, aExpected)
+{
+ is(aElement.matches(":user-invalid"), aExpected,
+ "matches(':user-invalid') should return " + aExpected + " for " + aElement);
+}
+
+var content = document.getElementById('content');
+var textarea = document.getElementsByTagName('textarea')[0];
+var input = document.getElementsByTagName('input')[0];
+var select = document.getElementsByTagName('select')[0];
+var button = document.getElementsByTagName('button')[0];
+var form = document.forms[0];
+
+checkPseudoClass(textarea, false);
+checkPseudoClass(input, false);
+checkPseudoClass(select, false);
+
+// Try to submit.
+button.click();
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+
+// No longer in the form.
+content.appendChild(textarea);
+content.appendChild(input);
+content.appendChild(select);
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+
+// Back in the form.
+form.appendChild(textarea);
+form.appendChild(input);
+form.appendChild(select);
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+
+/* Case when elements get orphaned. */
+var textarea = document.getElementsByTagName('textarea')[1];
+var input = document.getElementsByTagName('input')[1];
+var select = document.getElementsByTagName('select')[1];
+var button = document.getElementsByTagName('button')[1];
+var form = document.forms[1];
+
+// Try to submit.
+button.click();
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+
+// Remove the form.
+document.getElementsByTagName('table')[0].removeChild(form);
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug605124-2.html b/dom/html/test/test_bug605124-2.html
new file mode 100644
index 0000000000..07e3e5afcd
--- /dev/null
+++ b/dom/html/test/test_bug605124-2.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=605124
+-->
+<head>
+ <title>Test for Bug 605124</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=605124">Mozilla Bug 605124</a>
+<p id="display"></p>
+<div id="content">
+ <input required>
+ <textarea required></textarea>
+ <select required>
+ <option value="">foo</option>
+ <option>bar</option>
+ </select>
+ <select multiple required>
+ <option value="">foo</option>
+ <option>bar</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 605124 **/
+
+function checkPseudoClass(aElement, aExpected)
+{
+ is(aElement.matches(":-moz-ui-invalid"), aExpected,
+ "matches(':-moz-ui-invalid') should return " + aExpected + " for " + aElement);
+}
+
+function checkElement(aElement)
+{
+ checkPseudoClass(aElement, false);
+
+ // Focusing while :-moz-ui-invalid doesn't apply,
+ // the pseudo-class should not apply while typing.
+ aElement.focus();
+ checkPseudoClass(aElement, false);
+ // with keys
+ sendString("f");
+ checkPseudoClass(aElement, false);
+ synthesizeKey("KEY_Backspace");
+ checkPseudoClass(aElement, false);
+ // with .value
+ aElement.value = 'f';
+ checkPseudoClass(aElement, false);
+ aElement.value = '';
+ checkPseudoClass(aElement, false);
+
+ aElement.blur();
+ checkPseudoClass(aElement, true);
+
+ // Focusing while :-moz-ui-invalid applies,
+ // the pseudo-class should apply while typing if appropriate.
+ aElement.focus();
+ checkPseudoClass(aElement, true);
+ // with keys
+ sendString("f");
+ checkPseudoClass(aElement, false);
+ synthesizeKey("KEY_Backspace");
+ checkPseudoClass(aElement, true);
+ // with .value
+ aElement.value = 'f';
+ checkPseudoClass(aElement, false);
+ aElement.value = '';
+ checkPseudoClass(aElement, true);
+}
+
+function checkSelectElement(aElement)
+{
+ checkPseudoClass(aElement, false);
+
+ // Focusing while :-moz-ui-invalid doesn't apply,
+ // the pseudo-class should not apply while changing selection.
+ aElement.focus();
+ checkPseudoClass(aElement, false);
+
+ aElement.selectedIndex = 1;
+ checkPseudoClass(aElement, false);
+ aElement.selectedIndex = 0;
+ checkPseudoClass(aElement, false);
+
+ aElement.blur();
+ checkPseudoClass(aElement, false);
+
+ // Focusing while :-moz-ui-invalid applies,
+ // the pseudo-class should apply while changing selection if appropriate.
+ aElement.focus();
+ checkPseudoClass(aElement, false);
+
+ aElement.selectedIndex = 1;
+ checkPseudoClass(aElement, false);
+ aElement.selectedIndex = 0;
+ checkPseudoClass(aElement, false);
+ aElement.selectedIndex = 1;
+ checkPseudoClass(aElement, false);
+
+ aElement.blur();
+ checkPseudoClass(aElement, false);
+}
+
+checkElement(document.getElementsByTagName('input')[0]);
+checkElement(document.getElementsByTagName('textarea')[0]);
+checkSelectElement(document.getElementsByTagName('select')[0]);
+checkSelectElement(document.getElementsByTagName('select')[1]);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug605125-1.html b/dom/html/test/test_bug605125-1.html
new file mode 100644
index 0000000000..8b49b9bbc1
--- /dev/null
+++ b/dom/html/test/test_bug605125-1.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=605125
+-->
+<head>
+ <title>Test for Bug 605125</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=605125">Mozilla Bug 605125</a>
+<p id="display"></p>
+<div id="content">
+ <form id='f1'>
+ <textarea></textarea>
+ <input>
+ <button type='submit'></button>
+ <select></select>
+ </form>
+
+ <table>
+ <form id='f2'>
+ <tr>
+ <textarea></textarea>
+ <input>
+ <button type='submit'></button>
+ <select></select>
+ </tr>
+ </form>
+ </table>
+ <input form='f1' required>
+ <input form='f2' required>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 605125 **/
+
+/**
+ * NOTE: this test is very similar to 605124-1.html.
+ */
+
+function checkPseudoClass(aElement, aExpected)
+{
+ is(aElement.matches(":-moz-ui-valid"), aExpected,
+ "matches(':-moz-ui-valid') should return " + aExpected + " for " + aElement);
+}
+
+var content = document.getElementById('content');
+var textarea = document.getElementsByTagName('textarea')[0];
+var input = document.getElementsByTagName('input')[0];
+var button = document.getElementsByTagName('button')[0];
+var select = document.getElementsByTagName('select')[0];
+var form = document.forms[0];
+
+checkPseudoClass(textarea, false);
+checkPseudoClass(input, false);
+checkPseudoClass(select, false);
+
+// Try to submit.
+button.click();
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+
+// No longer in the form.
+content.appendChild(textarea);
+content.appendChild(input);
+content.appendChild(select);
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+
+// Back in the form.
+form.appendChild(textarea);
+form.appendChild(input);
+form.appendChild(select);
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+
+/* Case when elements get orphaned. */
+var textarea = document.getElementsByTagName('textarea')[1];
+var input = document.getElementsByTagName('input')[1];
+var button = document.getElementsByTagName('button')[1];
+var select = document.getElementsByTagName('select')[1];
+var form = document.forms[1];
+
+// Try to submit.
+button.click();
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+
+// Remove the form.
+document.getElementsByTagName('table')[0].removeChild(form);
+checkPseudoClass(textarea, true);
+checkPseudoClass(input, true);
+checkPseudoClass(select, true);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug605125-2.html b/dom/html/test/test_bug605125-2.html
new file mode 100644
index 0000000000..ea1195e189
--- /dev/null
+++ b/dom/html/test/test_bug605125-2.html
@@ -0,0 +1,149 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=605125
+-->
+<head>
+ <title>Test for Bug 605125</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=605125">Mozilla Bug 605125</a>
+<p id="display"></p>
+<div id="content">
+ <input>
+ <textarea></textarea>
+ <select>
+ <option value="">foo</option>
+ <option>bar</option>
+ </select>
+ <select multiple>
+ <option value="">foo</option>
+ <option>bar</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 605125 **/
+
+function checkPseudoClass(aElement, aExpected)
+{
+ is(aElement.matches(":user-valid"), aExpected,
+ "matches(':user-valid') should return " + aExpected + " for " + aElement.outerHTML);
+}
+
+function checkElement(aElement)
+{
+ checkPseudoClass(aElement, false);
+
+ // Focusing while :user-valid doesn't apply,
+ // the pseudo-class should not apply while typing.
+ aElement.focus();
+ checkPseudoClass(aElement, false);
+ // with keys
+ sendString("f");
+ checkPseudoClass(aElement, false);
+ synthesizeKey("KEY_Backspace");
+ checkPseudoClass(aElement, false);
+ // with .value
+ aElement.value = 'f';
+ checkPseudoClass(aElement, false);
+ aElement.value = '';
+ checkPseudoClass(aElement, false);
+
+ aElement.blur();
+ checkPseudoClass(aElement, true);
+
+ // Focusing while :user-valid applies,
+ // the pseudo-class should apply while typing if appropriate.
+ aElement.focus();
+ checkPseudoClass(aElement, true);
+ // with keys
+ sendString("f");
+ checkPseudoClass(aElement, true);
+ synthesizeKey("KEY_Backspace");
+ checkPseudoClass(aElement, true);
+ // with .value
+ aElement.value = 'f';
+ checkPseudoClass(aElement, true);
+ aElement.value = '';
+ checkPseudoClass(aElement, true);
+
+ aElement.blur();
+ aElement.required = true;
+ checkPseudoClass(aElement, false);
+
+ // Focusing while :user-invalid applies,
+ // the pseudo-class should apply while typing if appropriate.
+ aElement.focus();
+ checkPseudoClass(aElement, false);
+ // with keys
+ sendString("f");
+ checkPseudoClass(aElement, true);
+ synthesizeKey("KEY_Backspace");
+ checkPseudoClass(aElement, false);
+ // with .value
+ aElement.value = 'f';
+ checkPseudoClass(aElement, true);
+ aElement.value = '';
+ checkPseudoClass(aElement, false);
+}
+
+function checkSelectElement(aElement)
+{
+ checkPseudoClass(aElement, false);
+
+ if (!aElement.multiple && navigator.platform.startsWith("Mac")) {
+ // Arrow key on macOS opens the popup.
+ return;
+ }
+
+ // Focusing while :user-valid doesn't apply,
+ // the pseudo-class should not apply while changing selection.
+ aElement.focus();
+ checkPseudoClass(aElement, false);
+
+ synthesizeKey("KEY_ArrowDown");
+ checkPseudoClass(aElement, true);
+
+ // Focusing while :user-valid applies,
+ // the pseudo-class should apply while changing selection if appropriate.
+ aElement.focus();
+ checkPseudoClass(aElement, true);
+
+ aElement.selectedIndex = 1;
+ checkPseudoClass(aElement, true);
+ aElement.selectedIndex = 0;
+ checkPseudoClass(aElement, true);
+
+ aElement.blur();
+ aElement.required = true;
+ // select set with multiple is only invalid if no option is selected
+ if (aElement.multiple) {
+ aElement.selectedIndex = -1;
+ }
+ checkPseudoClass(aElement, false);
+
+ // Focusing while :user-invalid applies,
+ // the pseudo-class should apply while changing selection if appropriate.
+ aElement.focus();
+ checkPseudoClass(aElement, false);
+
+ synthesizeKey("KEY_ArrowDown");
+ checkPseudoClass(aElement, true);
+ aElement.selectedIndex = 0;
+ checkPseudoClass(aElement, aElement.multiple);
+}
+
+checkElement(document.getElementsByTagName('input')[0]);
+checkElement(document.getElementsByTagName('textarea')[0]);
+checkSelectElement(document.getElementsByTagName('select')[0]);
+checkSelectElement(document.getElementsByTagName('select')[1]);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug606817.html b/dom/html/test/test_bug606817.html
new file mode 100644
index 0000000000..4564753a93
--- /dev/null
+++ b/dom/html/test/test_bug606817.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=606817
+-->
+<head>
+ <title>Test for Bug 606817</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=606817">Mozilla Bug 606817</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 606817 **/
+
+var messageMaxLength = 256;
+
+function checkMessage(aInput, aMsg, aWithTerminalPeriod)
+{
+ ok(aInput.validationMessage != aMsg,
+ "Original content-defined message should have been truncate");
+ is(aInput.validationMessage.length - aInput.validationMessage.indexOf("_42_"),
+ aWithTerminalPeriod ? messageMaxLength+1 : messageMaxLength,
+ "validation message should be 256 characters length");
+}
+
+var input = document.createElement("input");
+
+var msg = "";
+for (var i=0; i<75; ++i) {
+ msg += "_42_";
+}
+// msg is now 300 chars long
+
+// Testing with setCustomValidity().
+input.setCustomValidity(msg);
+checkMessage(input, msg, false);
+
+// Cleaning.
+input.setCustomValidity("");
+
+// Testing with pattern and titl.
+input.pattern = "[0-9]*";
+input.value = "foo";
+input.title = msg;
+checkMessage(input, msg, true);
+
+// Cleaning.
+input.removeAttribute("pattern");
+input.removeAttribute("title");
+input.value = "";
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug607145.html b/dom/html/test/test_bug607145.html
new file mode 100644
index 0000000000..28ab3fe8ba
--- /dev/null
+++ b/dom/html/test/test_bug607145.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=607145
+-->
+<head>
+ <title>Test for Bug 607145</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=607145">Mozilla Bug 607145</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+var xoriginParams;
+/** Test for Bug 607145 **/
+
+/**
+ * This is not really reflecting an URL as the HTML5 specs want to.
+ * It's how .action is reflected in Gecko (might change later).
+ *
+ * If this changes, add reflectURL for "formAction" in
+ * dom/html/test/forms/test_input_attributes_reflection.html and
+ * "action" in
+ * dom/html/test/forms/test_form_attributes_reflection.html
+ */
+function reflectURL(aElement, aAttr)
+{
+ var idl = aAttr;
+ var attr = aAttr.toLowerCase();
+ var elmtName = aElement.tagName.toLowerCase();
+
+ var url = location.href.replace(/\?.*/, "");
+ var dir = url.replace(/test_bug607145.html[^\/]*$/, "");
+ var parentDir = dir.replace(/test\/$/, "");
+ ok(idl in aElement, idl + " should be available in " + elmtName);
+
+ // Default values.
+ is(aElement[idl].split("?")[0], url, "." + idl + " default value should be the document's URL");
+ is(aElement.getAttribute(attr), null,
+ "@" + attr + " default value should be null");
+
+ var values = [
+ /* value to set, resolved value */
+ [ "foo.html", dir + "foo.html" ],
+ [ "data:text/html,<html></html>", "data:text/html,<html></html>" ],
+ [ "http://example.org/", "http://example.org/" ],
+ [ "//example.org/", "http://example.org/" ],
+ [ "?foo=bar", url + "?foo=bar" ],
+ [ "#foo", url + "#foo" ],
+ [ "", url ],
+ [ " ", url ],
+ [ "../", parentDir ],
+ [ "...", dir + "..." ],
+ // invalid URL
+ [ "http://a b/", "http://a b/" ], // TODO: doesn't follow the specs, should be "".
+ ];
+
+ for (var value of values) {
+ aElement[idl] = value[0];
+ is(aElement[idl].replace(xoriginParams, ""), value[1], "." + idl + " value should be " + value[1]);
+ is(aElement.getAttribute(attr), value[0],
+ "@" + attr + " value should be " + value[0]);
+ }
+
+ for (var value of values) {
+ aElement.setAttribute(attr, value[0]);
+ is(aElement[idl].replace(xoriginParams, ""), value[1], "." + idl + " value should be " + value[1]);
+ is(aElement.getAttribute(attr), value[0],
+ "@" + attr + " value should be " + value[0]);
+ }
+}
+
+
+xoriginParams = window.location.search;
+
+reflectURL(document.createElement("form"), "action");
+reflectURL(document.createElement("input"), "formAction");
+reflectURL(document.createElement("button"), "formAction");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug610212.html b/dom/html/test/test_bug610212.html
new file mode 100644
index 0000000000..69838f7e4d
--- /dev/null
+++ b/dom/html/test/test_bug610212.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=610212
+-->
+<head>
+ <title>Test for Bug 610212</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=610212">Mozilla Bug 610212</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 610212 **/
+
+var canvas = document.createElement('canvas');
+
+reflectUnsignedInt({
+ element: canvas,
+ attribute: "width",
+ nonZero: false,
+ defaultValue: 300,
+});
+
+reflectUnsignedInt({
+ element: canvas,
+ attribute: "height",
+ nonZero: false,
+ defaultValue: 150,
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug610687.html b/dom/html/test/test_bug610687.html
new file mode 100644
index 0000000000..fd6950cce4
--- /dev/null
+++ b/dom/html/test/test_bug610687.html
@@ -0,0 +1,195 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=610687
+-->
+<head>
+ <title>Test for Bug 610687</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=610687">Mozilla Bug 610687</a>
+<p id="display"></p>
+<div id="content">
+ <form>
+ <input type='radio' name='a'>
+ <input type='radio' name='a'>
+ <input type='radio' name='b'>
+ </form>
+ <input type='radio' name='a'>
+ <input type='radio' name='a'>
+ <input type='radio' name='b'>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 610687 **/
+
+function checkPseudoClasses(aElement, aValid, aValidUI, aInvalidUI)
+{
+ if (aValid) {
+ ok(aElement.matches(":valid"), ":valid should apply");
+ } else {
+ ok(aElement.matches(":invalid"), ":invalid should apply");
+ }
+
+ is(aElement.matches(":user-valid"), aValidUI,
+ aValid ? ":user-valid should apply" : ":user-valid should not apply");
+
+ is(aElement.matches(":user-invalid"), aInvalidUI,
+ aInvalidUI ? ":user-invalid should apply" : ":user-invalid should not apply");
+
+ if (aInvalidUI && (aValid || aValidUI)) {
+ ok(false, ":invalid can't apply with :valid or :user-valid");
+ }
+}
+
+/**
+ * r1 and r2 should be in the same group.
+ * r3 should be in another group.
+ * form can be null.
+ */
+function checkRadios(r1, r2, r3, form)
+{
+ // Default state.
+ checkPseudoClasses(r1, true, false, false);
+ checkPseudoClasses(r2, true, false, false);
+ checkPseudoClasses(r3, true, false, false);
+
+ // Suffering from being missing (without ui-invalid).
+ r1.required = true;
+ checkPseudoClasses(r1, false, false, false);
+ checkPseudoClasses(r2, false, false, false);
+ checkPseudoClasses(r3, true, false, false);
+
+ // Do not suffer from being missing (with ui-valid).
+ r1.click();
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, true, false, false);
+ checkPseudoClasses(r3, true, false, false);
+
+ // Do not suffer from being missing (with ui-valid).
+ r1.checked = false;
+ r1.required = false;
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, true, false, false);
+ checkPseudoClasses(r3, true, false, false);
+
+ // Suffering from being missing (with ui-invalid) with required set on one radio
+ // and the checked state changed on another.
+ r1.required = true;
+ r2.checked = false;
+ checkPseudoClasses(r1, false, false, true);
+ checkPseudoClasses(r2, false, false, false);
+ checkPseudoClasses(r3, true, false, false);
+
+ // Do not suffer from being missing (with ui-valid) by checking the radio which
+ // hasn't the required attribute.
+ r2.checked = true;
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, true, false, false);
+ checkPseudoClasses(r3, true, false, false);
+
+ // .setCustomValidity() should not affect the entire group.
+ r1.checked = false; r2.checked = false; r3.checked = false;
+ r1.required = false;
+ r1.setCustomValidity('foo');
+ checkPseudoClasses(r1, false, false, true);
+ checkPseudoClasses(r2, true, false, false);
+ checkPseudoClasses(r3, true, false, false);
+
+ r1.setCustomValidity('');
+ r2.setCustomValidity('foo');
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, false, false, false);
+ checkPseudoClasses(r3, true, false, false);
+
+ r2.setCustomValidity('');
+ r3.setCustomValidity('foo');
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, true, false, false);
+ checkPseudoClasses(r3, false, false, false);
+
+ // Removing the radio with the required attribute should make the group valid.
+ r1.setCustomValidity('');
+ r2.setCustomValidity('');
+ r1.required = false;
+ r2.required = true;
+ r1.checked = r2.checked = false;
+ checkPseudoClasses(r1, false, false, true);
+ checkPseudoClasses(r2, false, false, false);
+
+ var p = r2.parentNode;
+ p.removeChild(r2);
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, false, false, false);
+
+ p.appendChild(r2);
+ checkPseudoClasses(r1, false, false, true);
+ checkPseudoClasses(r2, false, false, false);
+
+ // Adding a radio element to an invalid group should make it invalid.
+ p.removeChild(r1);
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, false, false, false);
+
+ p.appendChild(r1);
+ checkPseudoClasses(r1, false, false, true);
+ checkPseudoClasses(r2, false, false, false);
+
+ // Adding a checked radio element to an invalid group should make it valid.
+ p.removeChild(r1);
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, false, false, false);
+
+ r1.checked = true;
+ p.appendChild(r1);
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, true, false, false);
+ r1.checked = false;
+
+ // Adding an invalid radio element by changing the name attribute.
+ r2.name = 'c';
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, false, false, false);
+
+ r2.name = 'a';
+ checkPseudoClasses(r1, false, false, true);
+ checkPseudoClasses(r2, false, false, false);
+
+ // Adding an element to an invalid radio group by changing the name attribute.
+ r1.name = 'c';
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, false, false, false);
+
+ r1.name = 'a';
+ checkPseudoClasses(r1, false, false, true);
+ checkPseudoClasses(r2, false, false, false);
+
+ // Adding a checked element to an invalid radio group with the name attribute.
+ r1.name = 'c';
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, false, false, false);
+
+ r1.checked = true;
+ r1.name = 'a';
+ checkPseudoClasses(r1, true, true, false);
+ checkPseudoClasses(r2, true, false, false);
+ r1.checked = false;
+}
+
+var r1 = document.getElementsByTagName('input')[0];
+var r2 = document.getElementsByTagName('input')[1];
+var r3 = document.getElementsByTagName('input')[2];
+checkRadios(r1, r2, r3);
+
+r1 = document.getElementsByTagName('input')[3];
+r2 = document.getElementsByTagName('input')[4];
+r3 = document.getElementsByTagName('input')[5];
+checkRadios(r1, r2, r3);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug611189.html b/dom/html/test/test_bug611189.html
new file mode 100644
index 0000000000..d798fd4393
--- /dev/null
+++ b/dom/html/test/test_bug611189.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=611189
+-->
+<head>
+ <title>Test for Bug 611189</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=611189">Mozilla Bug 611189</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 611189 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(async function() {
+ var i = document.createElement("input");
+ var b = document.getElementById("content");
+ b.appendChild(i);
+ b.clientWidth; // bind to frame
+ i.focus(); // initialize editor
+ var before = await snapshotWindow(window, true);
+ i.value = "L"; // set the value
+ i.style.display = "none";
+ b.clientWidth; // unbind from frame
+ i.value = ""; // set the value without a frame
+ i.style.display = "";
+ b.clientWidth; // rebind to frame
+ is(i.value, "", "Input's value should be correctly updated");
+ var after = await snapshotWindow(window, true);
+ ok(compareSnapshots(before, after, true), "The correct value should be rendered inside the control");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug612730.html b/dom/html/test/test_bug612730.html
new file mode 100644
index 0000000000..ccdfc1241d
--- /dev/null
+++ b/dom/html/test/test_bug612730.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=612730
+-->
+<head>
+ <title>Test for Bug 612730</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612730">Mozilla Bug 612730</a>
+<p id="display"></p>
+<div id="content">
+ <select multiple required>
+ <option value="">foo</option>
+ <option value="">bar</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 612730 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function runTest()
+{
+ var select = document.getElementsByTagName('select')[0];
+
+ select.addEventListener("focus", function() {
+ isnot(select.selectedIndex, -1, "Something should have been selected");
+
+ ok(!select.matches(":-moz-ui-invalid"),
+ ":-moz-ui-invalid should not apply");
+ ok(!select.matches(":-moz-ui-valid"),
+ ":-moz-ui-valid should not apply");
+
+ SimpleTest.finish();
+ }, {once: true});
+
+ synthesizeMouse(select, 5, 5, {});
+}
+
+SimpleTest.waitForFocus(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug613019.html b/dom/html/test/test_bug613019.html
new file mode 100644
index 0000000000..3f96ce2542
--- /dev/null
+++ b/dom/html/test/test_bug613019.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=613019
+-->
+<head>
+ <title>Test for Bug 613019</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=613019">Mozilla Bug 613019</a>
+<div id="content">
+ <input type="text" maxlength="2" style="width:200px" value="Test">
+ <textarea maxlength="2" style="width:200px">Test</textarea>
+ <input type="text" minlength="6" style="width:200px" value="Test">
+ <textarea minlength="6" style="width:200px">Test</textarea>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 613019 **/
+
+function testInteractivityOfMaxLength(elem) {
+ // verify that user interactivity is necessary for validity state to apply.
+ is(elem.value, "Test", "Element has incorrect starting value.");
+ is(elem.validity.tooLong, false, "Element should not be tooLong.");
+
+ elem.setSelectionRange(elem.value.length, elem.value.length)
+ elem.focus();
+
+ synthesizeKey("KEY_Backspace");
+ is(elem.value, "Tes", "Element value was not changed correctly.");
+ is(elem.validity.tooLong, true, "Element should still be tooLong.");
+
+ synthesizeKey("KEY_Backspace");
+ is(elem.value, "Te", "Element value was not changed correctly.");
+ is(elem.validity.tooLong, false, "Element should no longer be tooLong.");
+
+ elem.value = "Test";
+ is(elem.validity.tooLong, false,
+ "Element should not be tooLong after non-interactive value change.");
+}
+
+function testInteractivityOfMinLength(elem) {
+ // verify that user interactivity is necessary for validity state to apply.
+ is(elem.value, "Test", "Element has incorrect starting value.");
+ is(elem.validity.tooLong, false, "Element should not be tooShort.");
+
+ elem.setSelectionRange(elem.value.length, elem.value.length)
+ elem.focus();
+
+ sendString("e");
+ is(elem.value, "Teste", "Element value was not changed correctly.");
+ is(elem.validity.tooShort, true, "Element should still be tooShort.");
+
+ sendString("d");
+ is(elem.value, "Tested", "Element value was not changed correctly.");
+ is(elem.validity.tooShort, false, "Element should no longer be tooShort.");
+
+ elem.value = "Test";
+ is(elem.validity.tooShort, false,
+ "Element should not be tooShort after non-interactive value change.");
+}
+
+function test() {
+ window.getSelection().removeAllRanges();
+ testInteractivityOfMaxLength(document.querySelector("input[type=text][maxlength]"));
+ testInteractivityOfMaxLength(document.querySelector("textarea[maxlength]"));
+ testInteractivityOfMinLength(document.querySelector("input[type=text][minlength]"));
+ testInteractivityOfMinLength(document.querySelector("textarea[minlength]"));
+ SimpleTest.finish();
+}
+
+window.onload = function() {
+ SimpleTest.waitForExplicitFinish();
+ setTimeout(test, 0);
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug613113.html b/dom/html/test/test_bug613113.html
new file mode 100644
index 0000000000..3308246af0
--- /dev/null
+++ b/dom/html/test/test_bug613113.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=613113
+-->
+<head>
+ <title>Test for Bug 613113</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=613113">Mozilla Bug 613113</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <iframe name='f'></iframe>
+ <form target='f' action="data:text/html,">
+ <output></output>
+ <button></button>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 613113 **/
+
+SimpleTest.waitForExplicitFinish();
+
+var invalidEvent = false;
+
+var form = document.forms[0];
+var button = document.getElementsByTagName('button')[0];
+var output = document.getElementsByTagName('output')[0];
+
+output.addEventListener("invalid", function() {
+ ok(false, "invalid event should have been send");
+});
+
+form.addEventListener("submit", function() {
+ ok(true, "submit has been caught");
+ setTimeout(function() {
+ SimpleTest.finish();
+ }, 0);
+});
+
+output.setCustomValidity("foo");
+
+button.click();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug613722.html b/dom/html/test/test_bug613722.html
new file mode 100644
index 0000000000..4fbead4507
--- /dev/null
+++ b/dom/html/test/test_bug613722.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=613722
+-->
+<head>
+ <title>Test for Bug 613722</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=613722">Mozilla Bug 613722</a>
+<p id="display"></p>
+<div id="content">
+ <embed src="test_plugin.tst" hidden>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 613722 **/
+
+var rect = document.getElementsByTagName('embed')[0].getBoundingClientRect();
+
+var hasFrame = rect.left != 0 || rect.right != 0 || rect.top != 0 ||
+ rect.bottom != 0;
+
+ok(hasFrame, "embed should have a frame with hidden set");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug613979.html b/dom/html/test/test_bug613979.html
new file mode 100644
index 0000000000..40921d8064
--- /dev/null
+++ b/dom/html/test/test_bug613979.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=613979
+-->
+<head>
+ <title>Test for Bug 613979</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=613979">Mozilla Bug 613979</a>
+<p id="display"></p>
+<div id="content">
+ <input required>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 613979 **/
+
+var testNum = 0;
+var input = document.getElementsByTagName('input')[0];
+
+input.addEventListener("input", function() {
+ if (testNum == 0) {
+ ok(input.validity.valid, "input should be valid");
+ testNum++;
+ SimpleTest.executeSoon(function() {
+ synthesizeKey("KEY_Backspace");
+ });
+ } else if (testNum == 1) {
+ ok(!input.validity.valid, "input should not be valid");
+ input.removeEventListener("input", arguments.callee);
+ SimpleTest.finish();
+ }
+});
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+ input.focus();
+ sendString("a");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug615595.html b/dom/html/test/test_bug615595.html
new file mode 100644
index 0000000000..4e3d002498
--- /dev/null
+++ b/dom/html/test/test_bug615595.html
Binary files differ
diff --git a/dom/html/test/test_bug615833.html b/dom/html/test/test_bug615833.html
new file mode 100644
index 0000000000..530603daee
--- /dev/null
+++ b/dom/html/test/test_bug615833.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=615697
+-->
+<head>
+ <title>Test for Bug 615697</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=615697">Mozilla Bug 615697</a>
+<p id="display"></p>
+<div id="content">
+ <input>
+ <textarea></textarea>
+ <input type='radio'>
+ <input type='checkbox'>
+ <select>
+ <option>foo</option>
+ <option>bar</option>
+ </select>
+ <select multiple size='1'>
+ <option>tulip</option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 615697 **/
+
+/**
+ * This test is making all elements trigger 'change' event.
+ * You should read the test from bottom to top:
+ * events are registered from the last one to the first one.
+ *
+ * Sometimes, elements are focused before a click. This might sound useless
+ * but it guarantees to have the element visible before simulating the click.
+ */
+
+var input = document.getElementsByTagName('input')[0];
+var textarea = document.getElementsByTagName('textarea')[0];
+var radio = document.getElementsByTagName('input')[1];
+var checkbox= document.getElementsByTagName('input')[2];
+var select = document.getElementsByTagName('select')[0];
+var selectMultiple = document.getElementsByTagName('select')[1];
+
+function checkChangeEvent(aEvent)
+{
+ ok(aEvent.bubbles, "change event should bubble");
+ ok(!aEvent.cancelable, "change event shouldn't be cancelable");
+}
+
+selectMultiple.addEventListener("change", function(aEvent) {
+ checkChangeEvent(aEvent);
+ SimpleTest.finish();
+}, {once: true});
+
+selectMultiple.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function () {
+ synthesizeMouseAtCenter(selectMultiple, {});
+ });
+}, {once: true});
+
+select.addEventListener("change", function(aEvent) {
+ checkChangeEvent(aEvent);
+ selectMultiple.focus();
+}, {once: true});
+
+select.addEventListener("keyup", function() {
+ select.blur();
+}, {once: true});
+
+select.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function () {
+ synthesizeKey("KEY_ArrowDown");
+ });
+}, {once: true});
+
+checkbox.addEventListener("change", function(aEvent) {
+ checkChangeEvent(aEvent);
+ select.focus();
+}, {once: true});
+
+checkbox.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function () {
+ synthesizeMouseAtCenter(checkbox, {});
+ });
+}, {once: true});
+
+radio.addEventListener("change", function(aEvent) {
+ checkChangeEvent(aEvent);
+ checkbox.focus();
+}, {once: true});
+
+radio.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function () {
+ synthesizeMouseAtCenter(radio, {});
+ });
+}, {once: true});
+
+textarea.addEventListener("change", function(aEvent) {
+ checkChangeEvent(aEvent);
+ radio.focus();
+}, {once: true});
+
+textarea.addEventListener("input", function() {
+ textarea.blur();
+}, {once: true});
+
+textarea.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function () {
+ sendString("f");
+ });
+}, {once: true});
+
+input.addEventListener("change", function(aEvent) {
+ checkChangeEvent(aEvent);
+ textarea.focus();
+}, {once: true});
+
+input.addEventListener("input", function() {
+ input.blur();
+}, {once: true});
+
+input.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function () {
+ sendString("f");
+ });
+}, {once: true});
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ input.focus();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug618948.html b/dom/html/test/test_bug618948.html
new file mode 100644
index 0000000000..04a9347261
--- /dev/null
+++ b/dom/html/test/test_bug618948.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=618948
+-->
+<head>
+ <title>Test for Bug 618948</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=618948">Mozilla Bug 618948</a>
+<p id="display"></p>
+<div id="content">
+ <form>
+ <input type='email' id='i'>
+ <button>submit</button>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 618948 **/
+
+var events = ["focus", "input", "change", "invalid" ];
+
+var handled = ({});
+
+function eventHandler(event)
+{
+ dump("\n" + event.type + "\n");
+ handled[event.type] = true;
+}
+
+function beginTest()
+{
+ for (var e of events) {
+ handled[e] = false;
+ }
+
+ i.focus();
+}
+
+function endTest()
+{
+ for (var e of events) {
+ ok(handled[e], "on" + e + " should have been called");
+ }
+
+ SimpleTest.finish();
+}
+
+var i = document.getElementsByTagName('input')[0];
+var b = document.getElementsByTagName('button')[0];
+
+i.onfocus = function(event) {
+ eventHandler(event);
+ sendString("f");
+ i.onfocus = null;
+};
+
+i.oninput = function(event) {
+ eventHandler(event);
+ b.focus();
+ i.oninput = null;
+};
+
+i.onchange = function(event) {
+ eventHandler(event);
+ i.onchange = null;
+ synthesizeMouseAtCenter(b, {});
+};
+
+i.oninvalid = function(event) {
+ eventHandler(event);
+ i.oninvalid = null;
+ endTest();
+};
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(beginTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug619278.html b/dom/html/test/test_bug619278.html
new file mode 100644
index 0000000000..56f704c037
--- /dev/null
+++ b/dom/html/test/test_bug619278.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=619278
+-->
+<head>
+ <title>Test for Bug 619278</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=619278">Mozilla Bug 619278</a>
+<p id="display"></p>
+<div id="content">
+ <form>
+ <input required><button>submit</button>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 619278 **/
+
+function doElementMatchesSelector(aElement, aSelector)
+{
+ ok(aElement.matches(aSelector),
+ aSelector + " should match for " + aElement);
+}
+
+var e = document.forms[0].elements[0];
+
+e.addEventListener("invalid", function(event) {
+ e.addEventListener("invalid", arguments.callee);
+
+ SimpleTest.executeSoon(function() {
+ doElementMatchesSelector(e, ":-moz-ui-invalid");
+ SimpleTest.finish();
+ });
+});
+
+e.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function() {
+ synthesizeKey("KEY_Enter");
+ });
+}, {once: true});
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ e.focus();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug622597.html b/dom/html/test/test_bug622597.html
new file mode 100644
index 0000000000..ead4887a1d
--- /dev/null
+++ b/dom/html/test/test_bug622597.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=622597
+-->
+<head>
+ <title>Test for Bug 622597</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622597">Mozilla Bug 622597</a>
+<p id="display"></p>
+<div id="content">
+ <form>
+ <input required>
+ <textarea required></textarea>
+ <select required><option value="">foo</option><option selected>bar</option></select>
+ <button>submit</button>
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 622597 **/
+
+var form = document.forms[0];
+var input = form.elements[0];
+var textarea = form.elements[1];
+var select = form.elements[2];
+var button = form.elements[3];
+
+function checkPseudoClasses(aElement, aValid, aInvalid)
+{
+ is(aElement.matches(":-moz-ui-valid"), aValid,
+ aValid ? aElement + " should match :-moz-ui-valid"
+ : aElement + " should not match :-moz-ui-valid");
+ is(aElement.matches(":-moz-ui-invalid"), aInvalid,
+ aInvalid ? aElement + " should match :-moz-ui-invalid"
+ : aElement + " should not match :-moz-ui-invalid");
+ if (aValid && aInvalid) {
+ ok(false,
+ aElement + " should not match :-moz-ui-valid AND :-moz-ui-invalid");
+ }
+}
+
+select.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function() {
+ form.noValidate = false;
+ SimpleTest.executeSoon(function() {
+ checkPseudoClasses(select, false, true);
+ SimpleTest.finish();
+ });
+ });
+}, {once: true});
+
+textarea.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function() {
+ form.noValidate = false;
+ SimpleTest.executeSoon(function() {
+ checkPseudoClasses(textarea, false, true);
+ form.noValidate = true;
+ select.selectedIndex = 0;
+ select.focus();
+ });
+ });
+}, {once: true});
+
+input.addEventListener("invalid", function() {
+ input.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function() {
+ form.noValidate = false;
+ SimpleTest.executeSoon(function() {
+ checkPseudoClasses(input, false, true);
+ form.noValidate = true;
+ textarea.value = '';
+ textarea.focus();
+ });
+ });
+ }, {once: true});
+
+ SimpleTest.executeSoon(function() {
+ form.noValidate = true;
+ input.blur();
+ input.value = '';
+ input.focus();
+ });
+}, {once: true});
+
+button.addEventListener("focus", function() {
+ SimpleTest.executeSoon(function() {
+ synthesizeKey("KEY_Enter");
+ });
+}, {once: true});
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ button.focus();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug623291.html b/dom/html/test/test_bug623291.html
new file mode 100644
index 0000000000..c7b7ab7ea4
--- /dev/null
+++ b/dom/html/test/test_bug623291.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=623291
+-->
+<head>
+ <title>Test for Bug 623291</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=623291">Mozilla Bug 623291</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<input id="textField" onfocus="next()" onblur="done();">
+<button id="b">a button</button>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 623291 **/
+
+function runTest() {
+ document.getElementById("textField").focus();
+}
+
+function next() {
+ synthesizeMouseAtCenter(document.getElementById('b'), {}, window);
+}
+
+function done() {
+ isnot(document.activeElement, document.getElementById("textField"),
+ "TextField should not be active anymore!");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug6296.html b/dom/html/test/test_bug6296.html
new file mode 100644
index 0000000000..6e74ce8ec2
--- /dev/null
+++ b/dom/html/test/test_bug6296.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=6296
+-->
+<head>
+ <title>Test for Bug 6296</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=6296">Mozilla Bug 6296</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <A HREF="../testdata/test.gif" id="foo" NAME="anchor1" ALT="this is a test of the image
+ attribute">Hi</A>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 6296 **/
+is($("foo").name, "anchor1", "accessing an anchor name should work, and not crash either!")
+
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug629801.html b/dom/html/test/test_bug629801.html
new file mode 100644
index 0000000000..073979a5fe
--- /dev/null
+++ b/dom/html/test/test_bug629801.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=629801
+-->
+<head>
+ <title>Test for Bug 629801</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=629801">Mozilla Bug 629801</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+<div itemscope>
+ This tests itemValue on time elements, first with no datetime attribute, then with no text content,
+ then with both.
+ <time id="t1" itemprop="a">May 10th 2009</time>
+ <time id="t2" itemprop="b" datetime="2009-05-10"></time>
+ <time id="t3" itemprop="c" datetime="2009-05-10">May 10th 2009</time>
+</div>
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 629801 **/
+
+var t1 = document.getElementById("t1"),
+ t2 = document.getElementById("t2"),
+ t3 = document.getElementById("t3"),
+ t4 = document.createElement("time");
+
+// .dateTime IDL
+is(t1.dateTime, "", "dateTime is properly set to empty string if datetime attributeis absent");
+is(t2.dateTime, "2009-05-10", "dateTime is properly set to datetime attribute with datetime and no text content");
+is(t3.dateTime, "2009-05-10", "dateTime is properly set to datetime attribute with datetime and text content");
+
+// dateTime reflects datetime attribute
+reflectString({
+ element: t4,
+ attribute: "dateTime"
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug633058.html b/dom/html/test/test_bug633058.html
new file mode 100644
index 0000000000..a92f1f9369
--- /dev/null
+++ b/dom/html/test/test_bug633058.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=633058
+-->
+<head>
+ <title>Test for Bug 633058</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=633058">Mozilla Bug 633058</a>
+<p id="display"></p>
+<div id="content">
+ <input>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 633058 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(startTest);
+
+function startTest() {
+ var nbExpectedKeyDown = 8;
+ var nbExpectedKeyPress = 1;
+ var inputGotKeyPress = 0;
+ var inputGotKeyDown = 0;
+ var divGotKeyPress = 0;
+ var divGotKeyDown = 0;
+
+ var input = document.getElementsByTagName('input')[0];
+ var content = document.getElementById('content');
+
+ content.addEventListener("keydown", () => { divGotKeyDown++; });
+ content.addEventListener("keypress", () => { divGotKeyPress++; });
+ input.addEventListener("keydown", () => { inputGotKeyDown++; });
+ input.addEventListener("keypress", () => { inputGotKeyPress++; });
+
+ input.addEventListener('focus', function() {
+ SimpleTest.executeSoon(() => {
+ synthesizeKey('KEY_ArrowUp');
+ synthesizeKey('KEY_ArrowLeft');
+ synthesizeKey('KEY_ArrowRight');
+ synthesizeKey('KEY_ArrowDown');
+ synthesizeKey('KEY_Backspace');
+ synthesizeKey('KEY_Delete');
+ synthesizeKey('KEY_Escape');
+ synthesizeKey('KEY_Enter'); // Will dispatch keypress event even in strict behavior.
+
+ is(inputGotKeyDown, nbExpectedKeyDown, "input got all keydown events");
+ is(inputGotKeyPress, nbExpectedKeyPress, "input got all keypress events");
+ is(divGotKeyDown, nbExpectedKeyDown, "div got all keydown events");
+ is(divGotKeyPress, nbExpectedKeyPress, "div got all keypress events");
+ SimpleTest.finish();
+ });
+ }, {once: true});
+ input.focus();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug636336.html b/dom/html/test/test_bug636336.html
new file mode 100644
index 0000000000..314e941f84
--- /dev/null
+++ b/dom/html/test/test_bug636336.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=636336
+-->
+<head>
+ <title>Test for Bug 636336</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=636336">Mozilla Bug 636336</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 636336 **/
+function testIt(tag) {
+ var elem = document.createElement(tag);
+ elem.setAttribute("src", " ");
+ is(elem.getAttribute("src"), " ",
+ tag + " src attribute setter should not strip whitespace");
+ elem.setAttribute("src", " test ");
+ is(elem.getAttribute("src"), " test ",
+ tag + " src attribute setter should not strip whitespace around non-whitespace");
+ is(elem.src, window.location.href.replace(/\?.*/, "")
+ .replace(/test_bug636336\.html$/, "test"),
+ tag + ".src should strip whitespace as needed");
+}
+
+testIt("img");
+testIt("source");
+testIt("audio");
+testIt("video");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug641219.html b/dom/html/test/test_bug641219.html
new file mode 100644
index 0000000000..44dc15cf41
--- /dev/null
+++ b/dom/html/test/test_bug641219.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=641219
+-->
+<head>
+ <title>Test for Bug 641219</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=641219">Mozilla Bug 641219</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<div id="div">
+<font></font>
+<svg><font/></svg>
+</div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 641219 **/
+var HTML = "http://www.w3.org/1999/xhtml",
+ SVG = "http://www.w3.org/2000/svg";
+var wrapper = document.getElementById("div");
+is(wrapper.getElementsByTagName("FONT").length, 1);
+is(wrapper.getElementsByTagName("FONT")[0].namespaceURI, HTML);
+is(wrapper.getElementsByTagName("font").length, 2);
+is(wrapper.getElementsByTagName("font")[0].namespaceURI, HTML);
+is(wrapper.getElementsByTagName("font")[1].namespaceURI, SVG);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug643051.html b/dom/html/test/test_bug643051.html
new file mode 100644
index 0000000000..5719d1d2db
--- /dev/null
+++ b/dom/html/test/test_bug643051.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=643051
+-->
+<head>
+ <title>Test for Bug 643051</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=643051">Mozilla Bug 643051</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({
+ "set": [
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ]
+}, () => {
+ /** Test for Bug 643051 **/
+ document.cookie = "a=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; // clear cookie
+ document.cookie = "a2=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; // clear cookie
+ document.cookie = "a3=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; // clear cookie
+
+ // single cookie, should work
+ document.cookie = "a=bar";
+ is(document.cookie, "a=bar", "Can't read stored cookie!");
+
+ document.cookie = "a2=bar\na3=bar";
+ is(document.cookie, "a=bar; a2=bar", "Wrong cookie value");
+
+ document.cookie = "a2=baz; a3=bar";
+ is(document.cookie, "a=bar; a2=baz", "Wrong cookie value");
+
+ // clear cookies again to avoid affecting other tests
+ document.cookie = "a=; expires=Thu, 01-Jan-1970 00:00:01 GMT";
+ document.cookie = "a2=; expires=Thu, 01-Jan-1970 00:00:01 GMT";
+ document.cookie = "a3=; expires=Thu, 01-Jan-1970 00:00:01 GMT";
+
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug646157.html b/dom/html/test/test_bug646157.html
new file mode 100644
index 0000000000..ea0cbedaf0
--- /dev/null
+++ b/dom/html/test/test_bug646157.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=646157
+-->
+<head>
+ <title>Test for Bug 646157</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=646157">Mozilla Bug 646157</a>
+<p id="display"></p>
+<div id="content">
+ <label id="l1"/><input id="c1" type='checkbox'>
+ <label id="l2"/><input id="c2" type='checkbox'>
+ <label id="l3"/><input id="c3" type='checkbox'>
+ <label id="l4"/><input id="c4" type='checkbox'>
+ <label id="l5"/><input id="c5" type='checkbox'>
+ <label id="l6"/><input id="c6" type='checkbox'>
+ <label id="l7"/><input id="c7" type='checkbox'>
+ <label id="l8"/><input id="c8" type='checkbox'>
+ <label id="l9"/><input id="c9" type='checkbox'>
+ <label id="l10"/><input id="c10" type='checkbox'>
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 646157 **/
+
+var expectedClicks = {
+ // [ Direct clicks, bubbled clicks, synthetic clicks]
+ l1: [0, 2, 1],
+ l2: [0, 2, 1],
+ l3: [0, 2, 1],
+ l4: [0, 2, 1],
+ l5: [0, 2, 1],
+ l6: [0, 2, 1],
+ l7: [0, 2, 1],
+ l8: [0, 2, 1],
+ l9: [0, 2, 1],
+ l10:[1, 2, 1],
+ c1: [0, 0, 0],
+ c2: [0, 0, 0],
+ c3: [0, 0, 0],
+ c4: [0, 0, 0],
+ c5: [0, 0, 0],
+ c6: [0, 0, 0],
+ c7: [0, 0, 0],
+ c8: [0, 0, 0],
+ c9: [0, 0, 0],
+ c10:[1, 1, 1]
+};
+
+function clickhandler(e) {
+ if (!e.currentTarget.clickCount)
+ e.currentTarget.clickCount = 1;
+ else
+ e.currentTarget.clickCount++;
+
+ if (e.currentTarget === e.target)
+ e.currentTarget.directClickCount = 1;
+
+ if (e.target != document.getElementById("l10")) {
+ if (!e.currentTarget.synthClickCount)
+ e.currentTarget.synthClickCount = 1;
+ else
+ e.currentTarget.synthClickCount++;
+ }
+}
+
+for (var i = 1; i <= 10; i++) {
+ document.getElementById("l" + i).addEventListener('click', clickhandler);
+ document.getElementById("c" + i).addEventListener('click', clickhandler);
+}
+
+document.getElementById("l10").click();
+
+function check(thing) {
+ var expected = expectedClicks[thing.id];
+ is(thing.directClickCount || 0, expected[0], "Wrong number of direct clicks");
+ is(thing.clickCount || 0, expected[1], "Wrong number of clicks");
+ is(thing.synthClickCount || 0, expected[2], "Wrong number of synthetic clicks");
+}
+
+// Compare them all
+for (var i = 1; i <= 10; i++) {
+ check(document.getElementById("l" + i));
+ check(document.getElementById("c" + i));
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug649134.html b/dom/html/test/test_bug649134.html
new file mode 100644
index 0000000000..52d644fb14
--- /dev/null
+++ b/dom/html/test/test_bug649134.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=649134
+-->
+<head>
+ <title>Test for Bug 649134</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=649134">Mozilla Bug 649134</a>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 649134 **/
+SimpleTest.waitForExplicitFinish();
+
+var calls = 0;
+function finish() {
+ if (++calls == 4)
+ SimpleTest.finish();
+}
+function verifyNoLoad(iframe) {
+ ok(iframe.contentDocument.body.offsetHeight > 0,
+ "HTTP Link stylesheet was ignored " + iframe.src);
+ finish();
+}
+var verifyLoadCalls = 0;
+function verifyLoad(iframe) {
+ if (++verifyLoadCalls == 2) {
+ ok(indexContent == iframe.contentDocument.body.innerHTML,
+ "bug649134/ loads bug649134/index.html " + iframe.src);
+ }
+ finish();
+}
+function indexLoad(iframe) {
+ indexContent = iframe.contentDocument.body.innerHTML;
+ verifyLoad(iframe);
+}
+
+</script>
+</pre>
+<p id="display">
+<!-- Note: the extra sub-directory is needed for the test, see bug 649134 comment 14 -->
+<iframe onload="verifyNoLoad(this);" src="bug649134/file_bug649134-1.sjs"></iframe>
+<iframe onload="verifyNoLoad(this);" src="bug649134/file_bug649134-2.sjs"></iframe>
+<iframe onload="verifyLoad(this);" src="bug649134/"></iframe> <!-- verify that mochitest server loads index.html -->
+<iframe onload="indexLoad(this);" src="bug649134/index.html"></iframe>
+</p>
+</body>
+</html>
diff --git a/dom/html/test/test_bug651956.html b/dom/html/test/test_bug651956.html
new file mode 100644
index 0000000000..bd059bab94
--- /dev/null
+++ b/dom/html/test/test_bug651956.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=651956
+-->
+<head>
+ <title>Test for Bug 651956</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=651956">Mozilla Bug 651956</a>
+<p id="display"></p>
+<div id="content">
+ <input>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 651956 **/
+
+var input = document.getElementsByTagName('input')[0];
+
+var gotInputEvent = false;
+
+input.addEventListener("input", function() {
+ gotInputEvent = true;
+}, {once: true});
+
+input.addEventListener("focus", function() {
+ synthesizeKey("KEY_Escape");
+
+ setTimeout(function() {
+ ok(!gotInputEvent, "No input event should have been sent.");
+ SimpleTest.finish();
+ });
+}, {once: true});
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ input.focus();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug658746.html b/dom/html/test/test_bug658746.html
new file mode 100644
index 0000000000..260b345300
--- /dev/null
+++ b/dom/html/test/test_bug658746.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=658746
+-->
+<head>
+ <title>Test for Bug 658746</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=658746">Mozilla Bug 658746</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 658746 **/
+
+/**
+ * Sets property, gets property and deletes property.
+ */
+function SetGetDelete(prop)
+{
+ var el = document.createElement('div');
+
+ el.dataset[prop] = 'aaaaaa';
+ is(el.dataset[prop], 'aaaaaa', 'Dataset property "' + prop + '" should have been set.');
+
+ delete el.dataset[prop];
+ is(el.dataset[prop], undefined, 'Dataset property"' + prop + '" should have been deleted.');
+}
+
+/**
+ * Gets, deletes and sets property. Expects exception while trying to set property.
+ */
+function SetExpectException(prop)
+{
+ var el = document.createElement('div');
+
+ is(el.dataset[prop], undefined, 'Dataset property "' + prop + '" should be undefined.');
+ delete el.dataset[prop];
+
+ try {
+ el.dataset[prop] = "xxxxxx";
+ ok(false, 'Exception should have been thrown when setting "' + prop + '".');
+ } catch (ex) {
+ ok(true, 'Exception should have been thrown.');
+ }
+}
+
+// Numbers as properties.
+SetGetDelete(-12345678901234567000);
+SetGetDelete(-1);
+SetGetDelete(0);
+SetGetDelete(1);
+SetGetDelete(12345678901234567000);
+
+// Floating point numbers as properties.
+SetGetDelete(-1.1);
+SetGetDelete(0.0);
+SetGetDelete(1.1);
+
+// Hexadecimal numbers as properties.
+SetGetDelete(0x3);
+SetGetDelete(0xa);
+
+// Octal numbers as properties.
+SetGetDelete(0o3);
+SetGetDelete(0o7);
+
+// String numbers as properties.
+SetGetDelete('0');
+SetGetDelete('01');
+SetGetDelete('0x1');
+
+// Undefined as property.
+SetGetDelete(undefined);
+
+// Empty arrays as properties.
+SetGetDelete(new Array());
+SetGetDelete([]);
+
+// Non-empty array and object as properties.
+SetExpectException(['a', 'b']);
+SetExpectException({'a':'b'});
+
+// Objects as properties.
+SetExpectException(new Object());
+SetExpectException(document);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug659596.html b/dom/html/test/test_bug659596.html
new file mode 100644
index 0000000000..79a55a7608
--- /dev/null
+++ b/dom/html/test/test_bug659596.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=659596
+-->
+<head>
+ <title>Test for Bug 659596</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=659596">Mozilla Bug 659596</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 659596 **/
+
+function checkReflection(option, attribute) {
+ /**
+ * Getting.
+ */
+
+ // When attribute isn't present.
+ var tests = [ "", "foo" ];
+ for (var test of tests) {
+ option.removeAttribute(attribute);
+ option.textContent = test;
+ is(option.getAttribute(attribute), null,
+ "option " + attribute + "'s value should be null");
+ is(option[attribute], option.textContent,
+ "option." + attribute + " should reflect the text content when the attribute isn't set");
+ }
+
+ // When attribute is present.
+ tests = [
+ [ "", "" ],
+ [ "", "foo" ],
+ [ "foo", "bar" ],
+ [ "foo", "" ],
+ ];
+ for (var test of tests) {
+ option.setAttribute(attribute, test[0]);
+ option.textContent = test[1];
+ is(option[attribute], option.getAttribute(attribute),
+ "option." + attribute + " should reflect the content attribute when it is set");
+ }
+
+ /**
+ * Setting.
+ */
+
+ // When attribute isn't present.
+ tests = [
+ [ "", "new" ],
+ [ "foo", "new" ],
+ ];
+ for (var test of tests) {
+ option.removeAttribute(attribute);
+ option.textContent = test[0];
+ option[attribute] = test[1]
+
+ is(option.getAttribute(attribute), test[1],
+ "when setting, the content attribute should change");
+ is(option.textContent, test[0],
+ "when setting, the text content should not change");
+ }
+
+ // When attribute is present.
+ tests = [
+ [ "", "", "new" ],
+ [ "", "foo", "new" ],
+ [ "foo", "bar", "new" ],
+ [ "foo", "", "new" ],
+ ];
+ for (var test of tests) {
+ option.setAttribute(attribute, test[0]);
+ option.textContent = test[1];
+ option[attribute] = test[2];
+
+ is(option.getAttribute(attribute), test[2],
+ "when setting, the content attribute should change");
+ is(option.textContent, test[1],
+ "when setting, the text content should not change");
+ }
+}
+
+var option = document.createElement("option");
+
+checkReflection(option, "value");
+checkReflection(option, "label");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug659743.xml b/dom/html/test/test_bug659743.xml
new file mode 100644
index 0000000000..12236bdc02
--- /dev/null
+++ b/dom/html/test/test_bug659743.xml
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=659743
+-->
+<head>
+ <title>Test for Bug 659743</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=659743">Mozilla Bug 659743</a>
+<p id="display">
+<map name="a">
+<area shape="rect" coords="25,25,75,75" href="#x"/>
+</map>
+<map id="b">
+<area shape="rect" coords="25,25,75,75" href="#y"/>
+</map>
+<map name="a">
+<area shape="rect" coords="25,25,75,75" href="#FAIL"/>
+</map>
+<map id="b">
+<area shape="rect" coords="25,25,75,75" href="#FAIL"/>
+</map>
+
+<img usemap="#a" src="image.png"/>
+<img usemap="#b" src="image.png"/>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 659743 **/
+SimpleTest.waitForExplicitFinish();
+var images = document.getElementsByTagName("img");
+var second = false;
+onhashchange = function() {
+ if (!second) {
+ second = true;
+ is(location.hash, "#x", "First map");
+ SimpleTest.waitForFocus(() => synthesizeMouse(images[1], 50, 50, {}));
+ } else {
+ is(location.hash, "#y", "Second map");
+ SimpleTest.finish();
+ }
+};
+SimpleTest.waitForFocus(() => synthesizeMouse(images[0], 50, 50, {}));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug660663.html b/dom/html/test/test_bug660663.html
new file mode 100644
index 0000000000..dcc208dfe5
--- /dev/null
+++ b/dom/html/test/test_bug660663.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=660663
+-->
+<head>
+ <title>Test for Bug 660663</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660663">Mozilla Bug 660663</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 660663 **/
+reflectLimitedEnumerated({
+ element: document.createElement("div"),
+ attribute: "dir",
+ validValues: ["ltr", "rtl", "auto"],
+ invalidValues: ["cheesecake", ""]
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug660959-1.html b/dom/html/test/test_bug660959-1.html
new file mode 100644
index 0000000000..930b31b4d3
--- /dev/null
+++ b/dom/html/test/test_bug660959-1.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=660959
+-->
+<head>
+ <title>Test for Bug 660959</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660959">Mozilla Bug 660959</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <a href="#" id="testa"></a>
+</div>
+<pre id="test">
+<script>
+ is($("content").querySelector(":link, :visited"), $("testa"),
+ "Should find a link even in a display:none subtree");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug660959-2.html b/dom/html/test/test_bug660959-2.html
new file mode 100644
index 0000000000..a7dfb2e3d5
--- /dev/null
+++ b/dom/html/test/test_bug660959-2.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=660959
+-->
+<head>
+ <title>Test for Bug 660959</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ :link, :visited {
+ color: red;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660959">Mozilla Bug 660959</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <a href="#" id="a"></a>
+</div>
+<pre id="test">
+<script type="application/javascript">
+ var a = document.getElementById("a");
+ is(window.getComputedStyle(a).color, "rgb(255, 0, 0)", "Link is not right color?");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug660959-3.html b/dom/html/test/test_bug660959-3.html
new file mode 100644
index 0000000000..cc39c1eb98
--- /dev/null
+++ b/dom/html/test/test_bug660959-3.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=660959
+-->
+<head>
+ <title>Test for Bug 660959</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660959">Mozilla Bug 660959</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <a href="http://www.example.com"></a>
+ <div id="foo">
+ <span id="test"></span>
+ </div>
+</div>
+<pre id="test">
+<script>
+ is($("foo").querySelector(":link + * span, :visited + * span"), $("test"),
+ "Should be able to find link siblings even in a display:none subtree");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug666200.html b/dom/html/test/test_bug666200.html
new file mode 100644
index 0000000000..d68966f787
--- /dev/null
+++ b/dom/html/test/test_bug666200.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=666200
+-->
+<head>
+ <title>Test for Bug 666200</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=666200">Mozilla Bug 666200</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/** Test for Bug 666200 **/
+var sel = document.createElement("select");
+var opt1 = new Option();
+var opt2 = new Option();
+var opt3 = new Option();
+var opt4 = new Option();
+var opt5 = new Option();
+opt1.value = 1;
+opt2.value = 2;
+opt3.value = 3;
+opt4.value = 4;
+opt5.value = 5;
+sel.add(opt1);
+sel.add(opt2, 0);
+sel.add(opt3, 1000);
+sel.options.add(opt4, opt3);
+sel.add(opt5, undefined);
+is(sel[0], opt2, "1st item should be 2");
+is(sel[1], opt1, "2nd item should be 1");
+is(sel[2], opt4, "3rd item should be 4");
+is(sel[3], opt3, "4th item should be 3");
+is(sel[4], opt5, "5th item should be 5");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug666666.html b/dom/html/test/test_bug666666.html
new file mode 100644
index 0000000000..a3c22d4e0f
--- /dev/null
+++ b/dom/html/test/test_bug666666.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=666666
+-->
+<head>
+ <title>Test for Bug 666666</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=666666">Mozilla Bug 666666</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 666666 **/
+["audio", "video"].forEach(function(element) {
+ reflectLimitedEnumerated({
+ element: document.createElement(element),
+ attribute: "preload",
+ validValues: ["none", "metadata", "auto"],
+ invalidValues: ["cheesecake", ""]
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug669012.html b/dom/html/test/test_bug669012.html
new file mode 100644
index 0000000000..330286f33d
--- /dev/null
+++ b/dom/html/test/test_bug669012.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=669012
+-->
+<head>
+ <title>Test for Bug 669012</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=669012">Mozilla Bug 669012</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<script>
+var run = 0;
+</script>
+<svg>
+<script>
+run++;
+ok(true, "Should run SVG script without attributes")
+</script>
+<script for=window event=onload>
+run++;
+ok(true, "Should run SVG script with for=window event=onload")
+</script>
+<script for=window event=foo>
+run++;
+ok(true, "Should run SVG script with for=window event=foo")
+</script>
+<script for=foo event=onload>
+run++;
+ok(true, "Should run SVG script with for=foo event=onload")
+</script>
+</svg>
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 669012 **/
+is(run, 4, "Should have run all tests")
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug674558.html b/dom/html/test/test_bug674558.html
new file mode 100644
index 0000000000..ab9713bf35
--- /dev/null
+++ b/dom/html/test/test_bug674558.html
@@ -0,0 +1,287 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674558
+-->
+<head>
+ <title>Test for Bug 674558</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674558">Mozilla Bug 674558</a>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 674558 **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(startTest);
+
+function startTest() {
+ function textAreaCtor() {
+ return document.createElement("textarea");
+ }
+ var ctors = [textAreaCtor];
+ ["text", "password", "search"].forEach(function(type) {
+ ctors.push(function inputCtor() {
+ var input = document.createElement("input");
+ input.type = type;
+ return input;
+ });
+ });
+
+ for (var ctor in ctors) {
+ test(ctors[ctor]);
+ }
+
+ SimpleTest.finish();
+}
+
+function test(ctor) {
+ var elem = ctor();
+ ok(true, "Testing " + name(elem));
+
+ ok("selectionDirection" in elem, "elem should have the selectionDirection property");
+
+ is(elem.selectionStart, elem.value.length, "Default value");
+ is(elem.selectionEnd, elem.value.length, "Default value");
+ is(elem.selectionDirection, "forward", "Default value");
+
+ var content = document.getElementById("content");
+ content.appendChild(elem);
+
+ function flush() { document.body.clientWidth; }
+ function hide() {
+ content.style.display = "none";
+ flush();
+ }
+ function show() {
+ content.style.display = "";
+ flush();
+ }
+
+ elem.value = "foobar";
+
+ is(elem.selectionStart, elem.value.length, "Default value");
+ is(elem.selectionEnd, elem.value.length, "Default value");
+ is(elem.selectionDirection, "forward", "Default value");
+
+ elem.setSelectionRange(1, 3);
+ is(elem.selectionStart, 1, "Correct value");
+ is(elem.selectionEnd, 3, "Correct value");
+ is(elem.selectionDirection, "forward", "If not set, should default to forward");
+
+ hide();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 3, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 3, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ // extend to right
+ elem.focus();
+ synthesizeKey("VK_RIGHT", {shiftKey: true});
+
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Correct value");
+ is(elem.selectionDirection, "forward", "Still forward");
+
+ hide();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ // change the direction
+ elem.selectionDirection = "backward";
+
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Correct value");
+
+ hide();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ // extend to right again
+ synthesizeKey("VK_RIGHT", {shiftKey: true});
+
+ is(elem.selectionStart, 2, "Correct value");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Still backward");
+
+ hide();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ elem.selectionEnd = 5;
+
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Correct value");
+ is(elem.selectionDirection, "backward", "Still backward");
+
+ hide();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ elem.selectionDirection = "none";
+
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "forward", "none not supported");
+
+ hide();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ elem.selectionDirection = "backward";
+
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Correct Value");
+
+ hide();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ elem.selectionDirection = "invalid";
+
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Treated as none");
+
+ hide();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ elem.selectionDirection = "backward";
+
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Correct Value");
+
+ hide();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ elem.setSelectionRange(1, 4);
+
+ is(elem.selectionStart, 1, "Correct value");
+ is(elem.selectionEnd, 4, "Correct value");
+ is(elem.selectionDirection, "forward", "Correct value");
+
+ hide();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ elem.setSelectionRange(1, 1);
+ synthesizeKey("VK_RIGHT", {shiftKey: true});
+ synthesizeKey("VK_RIGHT", {shiftKey: true});
+ synthesizeKey("VK_RIGHT", {shiftKey: true});
+
+ is(elem.selectionStart, 1, "Correct value");
+ is(elem.selectionEnd, 4, "Correct value");
+ is(elem.selectionDirection, "forward", "Correct value");
+
+ hide();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 1, "Value unchanged");
+ is(elem.selectionEnd, 4, "Value unchanged");
+ is(elem.selectionDirection, "forward", "Value unchanged");
+
+ elem.setSelectionRange(5, 5);
+ synthesizeKey("VK_LEFT", {shiftKey: true});
+ synthesizeKey("VK_LEFT", {shiftKey: true});
+ synthesizeKey("VK_LEFT", {shiftKey: true});
+
+ is(elem.selectionStart, 2, "Correct value");
+ is(elem.selectionEnd, 5, "Correct value");
+ is(elem.selectionDirection, "backward", "Correct value");
+
+ hide();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+
+ show();
+ is(elem.selectionStart, 2, "Value unchanged");
+ is(elem.selectionEnd, 5, "Value unchanged");
+ is(elem.selectionDirection, "backward", "Value unchanged");
+}
+
+function name(elem) {
+ var tag = elem.localName;
+ if (tag == "input") {
+ tag += "[type=" + elem.type + "]";
+ }
+ return tag;
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug674927.html b/dom/html/test/test_bug674927.html
new file mode 100644
index 0000000000..92af594530
--- /dev/null
+++ b/dom/html/test/test_bug674927.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=674927
+-->
+<title>Test for Bug 674927</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<p><span>Hello</span></p>
+<div contenteditable>Contenteditable <i>is</i> splelchecked by default</div>
+<textarea>Textareas are spellchekced by default</textarea>
+<input value="Inputs are not spellcheckde by default">
+<script>
+// Test the effect of setting spellcheck on various elements
+[
+ "html",
+ "body",
+ "p",
+ "span",
+ "div",
+ "i",
+ "textarea",
+ "input",
+].forEach(function(query) {
+ var element = document.querySelector(query);
+
+ // First check what happens if no attributes are set
+ var defaultSpellcheck;
+ if (element.isContentEditable || element.tagName == "TEXTAREA") {
+ defaultSpellcheck = true;
+ } else {
+ defaultSpellcheck = false;
+ }
+ is(element.spellcheck, defaultSpellcheck,
+ "Default spellcheck for <" + element.tagName.toLowerCase() + ">");
+
+ // Now try setting spellcheck on ancestors
+ var ancestor = element;
+ do {
+ testSpellcheck(ancestor, element);
+ ancestor = ancestor.parentNode;
+ } while (ancestor.nodeType == Node.ELEMENT_NODE);
+});
+
+function testSpellcheck(ancestor, element) {
+ ancestor.spellcheck = true;
+ is(element.spellcheck, true,
+ ".spellcheck on <" + element.tagName.toLowerCase() + "> with " +
+ "spellcheck=true on <" + ancestor.tagName.toLowerCase() + ">");
+ ancestor.spellcheck = false;
+ is(element.spellcheck, false,
+ ".spellcheck on <" + element.tagName.toLowerCase() + "> with " +
+ "spellcheck=false on <" + ancestor.tagName.toLowerCase() + ">");
+ ancestor.removeAttribute("spellcheck");
+}
+</script>
diff --git a/dom/html/test/test_bug677495-1.html b/dom/html/test/test_bug677495-1.html
new file mode 100644
index 0000000000..be11d20fd6
--- /dev/null
+++ b/dom/html/test/test_bug677495-1.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=677495
+
+As mandated by the spec, the body of a media document must only contain one child.
+-->
+<head>
+ <title>Test for Bug 571981</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ function frameLoaded() {
+ var testframe = document.getElementById('testframe');
+ var testframeChildren = testframe.contentDocument.body.childNodes;
+ is(testframeChildren.length, 1, "Body of video document has 1 child");
+ is(testframeChildren[0].nodeName, "VIDEO", "Only child of body must be a <video> element");
+
+ SimpleTest.finish();
+ }
+</script>
+
+</head>
+<body>
+ <p id="display"></p>
+
+ <iframe id="testframe" name="testframe" onload="frameLoaded()"
+ src="file.webm"></iframe>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug677495.html b/dom/html/test/test_bug677495.html
new file mode 100644
index 0000000000..2145d5899c
--- /dev/null
+++ b/dom/html/test/test_bug677495.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=677495
+
+As mandated by the spec, the body of a media document must only contain one child.
+-->
+<head>
+ <title>Test for Bug 571981</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ function frameLoaded() {
+ var testframe = document.getElementById('testframe');
+ var testframeChildren = testframe.contentDocument.body.childNodes;
+ is(testframeChildren.length, 1, "Body of image document has 1 child");
+ is(testframeChildren[0].nodeName, "IMG", "Only child of body must be an <img> element");
+
+ SimpleTest.finish();
+ }
+</script>
+
+</head>
+<body>
+ <p id="display"></p>
+
+ <iframe id="testframe" name="testframe" onload="frameLoaded()"
+ src="image.png"></iframe>
+
+</body>
+</html>
diff --git a/dom/html/test/test_bug677658.html b/dom/html/test/test_bug677658.html
new file mode 100644
index 0000000000..79a4088b73
--- /dev/null
+++ b/dom/html/test/test_bug677658.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=677658
+-->
+<head>
+ <title>Test for Bug 677658</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="test()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=677658">Mozilla Bug 677658</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript"><!--
+
+/** Test for Bug 677658 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function testDone() {
+ ok(window.testPassed, "Script shouldn't have run!");
+ SimpleTest.finish();
+}
+
+function test() {
+ window.testPassed = true;
+ document.getElementById("testtarget").innerHTML =
+ "<script async src='data:text/plain, window.testPassed = false;'></script>";
+ SimpleTest.executeSoon(testDone);
+}
+
+// -->
+</script>
+</pre>
+<div id="testtarget"></div>
+</body>
+</html>
diff --git a/dom/html/test/test_bug682886.html b/dom/html/test/test_bug682886.html
new file mode 100644
index 0000000000..cb032738c9
--- /dev/null
+++ b/dom/html/test/test_bug682886.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=682886
+-->
+<head>
+ <title>Test for Bug 682886</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=682886">Mozilla Bug 682886</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 682886 **/
+
+
+ var m = document.createElement("menu");
+ var s = "<menuitem>foo</menuitem>";
+ m.innerHTML = s;
+ is(m.innerHTML, s, "Wrong menuitem serialization!");
+
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug691.html b/dom/html/test/test_bug691.html
new file mode 100644
index 0000000000..f88df20a54
--- /dev/null
+++ b/dom/html/test/test_bug691.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=691
+-->
+<head>
+ <title>Test for Bug 691</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script type="text/javascript">
+
+function show(what) {
+ var stage = document.getElementById("stage");
+ if (what == "modularity") {
+ var spaghetti = document.createElement("IMG",null);
+ spaghetti.setAttribute("SRC","nnc_lockup.gif");
+ spaghetti.setAttribute("id","foo");
+ stage.insertBefore(spaghetti,stage.firstChild);
+ }
+}
+
+function remove() {
+ var stage = document.getElementById("stage");
+ var body = document.getElementsByTagName("BODY")[0];
+ while (stage.firstChild) {
+ stage.firstChild.remove();
+ }
+}
+
+</script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=691">Mozilla Bug 691</a>
+<p id="display"></p>
+<div id="content" >
+<ul>
+<li >foo</li>
+</ul>
+<div id="stage">
+</div>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 691 **/
+
+show("modularity");
+remove();
+show("modularity");
+remove();
+show("modularity");
+remove();
+show("modularity");
+
+ok($("foo"), "basic DOM manipulation doesn't crash");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug694.html b/dom/html/test/test_bug694.html
new file mode 100644
index 0000000000..78eb054cfc
--- /dev/null
+++ b/dom/html/test/test_bug694.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=694
+-->
+<head>
+ <title>Test for Bug 694</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=694">Mozilla Bug 694</a>
+<p id="display"></p>
+<div id="content" >
+<img src="/missing_on_purpose" width=123 height=25 alt="Hello, &quot;Quotes&quot; how are you?" id="testimg">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 694 **/
+
+is($("testimg").getAttribute("alt"), "Hello, \"Quotes\" how are you?", "entities in alt attribute works");
+
+
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug694503.html b/dom/html/test/test_bug694503.html
new file mode 100644
index 0000000000..4ff10feffc
--- /dev/null
+++ b/dom/html/test/test_bug694503.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=694503
+-->
+<head>
+ <title>Test for Bug 694503</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=694503">Mozilla Bug 694503</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<div>
+<map name="map1">
+ <area onclick="++mapClickCount; event.preventDefault();"
+ coords="0,0,50,50" shape="rect">
+</map>
+</div>
+
+<img id="img"
+ usemap="#map1" alt="Foo bar" src="about:logo">
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 694503 **/
+
+var mapClickCount = 0;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var m = document.getElementsByTagName("map")[0];
+ var img = document.getElementById('img');
+ var origName = m.name;
+
+ synthesizeMouse(img, 25, 25, {});
+ is(mapClickCount, 1, "Wrong click count (1)");
+
+ m.name = "foo"
+ synthesizeMouse(img, 25, 25, {});
+ is(mapClickCount, 1, "Wrong click count (2)");
+
+ m.removeAttribute("name");
+ m.id = origName;
+ synthesizeMouse(img, 25, 25, {});
+ is(mapClickCount, 2, "Wrong click count (3)");
+
+ // Back to original state
+ m.removeAttribute("id");
+ m.name = origName;
+ synthesizeMouse(img, 25, 25, {});
+ is(mapClickCount, 3, "Wrong click count (4)");
+
+ var p = m.parentNode;
+ p.removeChild(m);
+ synthesizeMouse(img, 25, 25, {});
+ is(mapClickCount, 3, "Wrong click count (5)");
+
+ // Back to original state
+ p.appendChild(m);
+ synthesizeMouse(img, 25, 25, {});
+ is(mapClickCount, 4, "Wrong click count (6)");
+
+ SimpleTest.finish();
+});
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug696.html b/dom/html/test/test_bug696.html
new file mode 100644
index 0000000000..6b3c5d9561
--- /dev/null
+++ b/dom/html/test/test_bug696.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=696
+-->
+<head>
+ <title>Test for Bug 696</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=696">Mozilla Bug 696</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <table><tr id="mytr"><td>Foo</td><td>Bar</td></tr></table>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 696 **/
+var mytr = $("content").getElementsByTagName("TR")[0];
+is(mytr.getAttribute("ID"),"mytr","TR tags expose their ID attribute");
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug717819.html b/dom/html/test/test_bug717819.html
new file mode 100644
index 0000000000..b2e04f17ba
--- /dev/null
+++ b/dom/html/test/test_bug717819.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=717819
+-->
+<head>
+ <title>Test for Bug 717819</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=717819">Mozilla Bug 717819</a>
+<p id="display"></p>
+<div id="content">
+ <table style="position: relative; top: 100px;">
+ <tr>
+ <td>
+ <div id="test" style="position: absolute; top: 50px;"></div>
+ </td>
+ </tr>
+ </table>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 717819 **/
+var div = document.getElementById("test");
+is(div.offsetTop, 50, "The offsetTop must be calculated correctly");
+is(div.offsetParent, document.querySelector("table"),
+ "The offset should be calculated off of the correct parent");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug741266.html b/dom/html/test/test_bug741266.html
new file mode 100644
index 0000000000..d61e5b6ab0
--- /dev/null
+++ b/dom/html/test/test_bug741266.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=741266
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 741266</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=741266">Mozilla Bug 741266</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 741266 **/
+SimpleTest.waitForExplicitFinish();
+
+var url = URL.createObjectURL(new Blob([""], { type: "text/html" }));
+var w = window.open(url, "", "width=100,height=100");
+w.onload = function() {
+ is(w.innerHeight, 100, "Popup height should be 100 when opened with window.open");
+ // XXXbz On at least some platforms, the innerWidth is off by the scrollbar
+ // width for some reason. So just make sure it's the same for both popups.
+ var width = w.innerWidth;
+ w.close();
+
+ w = document.open(url, "", "width=100,height=100");
+ w.onload = function() {
+ is(w.innerHeight, 100, "Popup height should be 100 when opened with document.open");
+ is(w.innerWidth, width, "Popup width should be the same when opened with document.open");
+ w.close();
+ SimpleTest.finish();
+ };
+};
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug742030.html b/dom/html/test/test_bug742030.html
new file mode 100644
index 0000000000..28185e60db
--- /dev/null
+++ b/dom/html/test/test_bug742030.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=742030
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 742030</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=742030">Mozilla Bug 742030</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 742030 **/
+const str = " color: #ff0000 ";
+var span = document.createElement("span");
+span.setAttribute("style", str);
+is(span.getAttribute("style"), str, "Should have set properly");
+var span2 = span.cloneNode(false);
+is(span2.getAttribute("style"), str, "Should have cloned properly");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug742549.html b/dom/html/test/test_bug742549.html
new file mode 100644
index 0000000000..553493858e
--- /dev/null
+++ b/dom/html/test/test_bug742549.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=742549
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 742549</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=742549">Mozilla Bug 742549</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 742549 **/
+var els = [ document.createElement("script"),
+ document.createElementNS("http://www.w3.org/2000/svg", "script") ]
+
+for (var i = 0; i < els.length; ++i) {
+ reflectLimitedEnumerated({
+ element: els[i],
+ attribute: { content: "crossorigin", idl: "crossOrigin" },
+ // "" is a valid value per spec, but gets mapped to the "anonymous" state,
+ // just like invalid values, so just list it under invalidValues
+ validValues: [ "anonymous", "use-credentials" ],
+ invalidValues: [
+ "", " aNOnYmous ", " UsE-CreDEntIALS ", "foobar", "FOOBAR", " fOoBaR "
+ ],
+ defaultValue: { invalid: "anonymous", missing: null },
+ nullable: true,
+ })
+}
+
+
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug745685.html b/dom/html/test/test_bug745685.html
new file mode 100644
index 0000000000..c4544441e7
--- /dev/null
+++ b/dom/html/test/test_bug745685.html
@@ -0,0 +1,105 @@
+<!doctype html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=745685
+-->
+<title>Test for Bug 745685</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=745685">Mozilla Bug 745685</a>
+<font>Test text</font>
+<font size=1>1</font>
+<font size=2>2</font>
+<font size=3>3</font>
+<font size=4>4</font>
+<font size=5>5</font>
+<font size=6>6</font>
+<font size=7>7</font>
+<script>
+/** Test for Bug 745685 **/
+
+var referenceSizes = {};
+for (var i = 1; i <= 7; i++) {
+ referenceSizes[i] =
+ getComputedStyle(document.querySelector('[size="' + i + '"]'))
+ .fontSize;
+ if (i > 1) {
+ isnot(referenceSizes[i], referenceSizes[i - 1],
+ "Sanity check: different <font size>s give different .fontSize");
+ }
+}
+
+function testFontSize(input, expected) {
+ var font = document.querySelector("font");
+ font.setAttribute("size", input);
+ is(font.getAttribute("size"), input,
+ "Setting doesn't round-trip (.getAttribute)");
+ is(font.size, input,
+ "Setting doesn't round-trip (.size)");
+ is(getComputedStyle(font).fontSize, referenceSizes[expected],
+ 'Incorrect size for "' + input + '" : expected the same as ' + expected);
+}
+
+function testFontSizes(input, expected) {
+ testFontSize(input, expected);
+ // Leading whitespace
+ testFontSize(" " + input, expected);
+ testFontSize("\t" + input, expected);
+ testFontSize("\n" + input, expected);
+ testFontSize("\f" + input, expected);
+ testFontSize("\r" + input, expected);
+ // Trailing garbage
+ testFontSize(input + "abcd", expected);
+ testFontSize(input + ".5", expected);
+ testFontSize(input + "e2", expected);
+}
+
+// Parse error
+testFontSizes("", 3);
+
+// No sign
+testFontSizes("0", 1);
+testFontSizes("1", 1);
+testFontSizes("2", 2);
+testFontSizes("3", 3);
+testFontSizes("4", 4);
+testFontSizes("5", 5);
+testFontSizes("6", 6);
+testFontSizes("7", 7);
+testFontSizes("8", 7);
+testFontSizes("9", 7);
+testFontSizes("10", 7);
+testFontSizes("10000000000000000000000", 7);
+
+// Minus sign
+testFontSizes("-0", 3);
+testFontSizes("-1", 2);
+testFontSizes("-2", 1);
+testFontSizes("-3", 1);
+testFontSizes("-4", 1);
+testFontSizes("-5", 1);
+testFontSizes("-6", 1);
+testFontSizes("-7", 1);
+testFontSizes("-8", 1);
+testFontSizes("-9", 1);
+testFontSizes("-10", 1);
+testFontSizes("-10000000000000000000000", 1);
+
+// Plus sign
+testFontSizes("+0", 3);
+testFontSizes("+1", 4);
+testFontSizes("+2", 5);
+testFontSizes("+3", 6);
+testFontSizes("+4", 7);
+testFontSizes("+5", 7);
+testFontSizes("+6", 7);
+testFontSizes("+7", 7);
+testFontSizes("+8", 7);
+testFontSizes("+9", 7);
+testFontSizes("+10", 7);
+testFontSizes("+10000000000000000000000", 7);
+
+// Non-HTML5 whitespace
+testFontSize("\b1", 3);
+testFontSize("\v1", 3);
+testFontSize("\0u00a01", 3);
+</script>
diff --git a/dom/html/test/test_bug763626.html b/dom/html/test/test_bug763626.html
new file mode 100644
index 0000000000..11da9d1ad2
--- /dev/null
+++ b/dom/html/test/test_bug763626.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=763626
+-->
+<head>
+<title>Test for Bug 763626</title>
+
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function boom()
+{
+ var r = document.createElement("iframe").sandbox;
+ SpecialPowers.DOMWindowUtils.garbageCollect();
+ is("" + r, "", "ToString should return empty string when element is gone");
+ SimpleTest.finish();
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
+
diff --git a/dom/html/test/test_bug765780.html b/dom/html/test/test_bug765780.html
new file mode 100644
index 0000000000..9aee15ea6b
--- /dev/null
+++ b/dom/html/test/test_bug765780.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=765780
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 765780</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ /** Test for Bug 765780 **/
+ SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ var f = $("f");
+ var doc = f.contentDocument;
+ doc.designMode = "on";
+ var s = doc.createElement("script");
+ s.textContent = "parent.called = true;";
+
+ window.called = false;
+ doc.body.appendChild(s);
+ ok(called, "Script in designMode iframe should have run");
+
+ doc = doc.querySelector("iframe").contentDocument;
+ var s = doc.createElement("script");
+ s.textContent = "parent.parent.called = true;";
+
+ window.called = false;
+ doc.body.appendChild(s);
+ ok(called, "Script in designMode iframe's child should have run");
+
+ SimpleTest.finish();
+ }
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=765780">Mozilla Bug 765780</a>
+<!-- Important: iframe needs to not be display: none -->
+<p id="display"><iframe id="f" srcdoc="<iframe></iframe>"></iframe> </p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug780993.html b/dom/html/test/test_bug780993.html
new file mode 100644
index 0000000000..14324e8e43
--- /dev/null
+++ b/dom/html/test/test_bug780993.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test for bug 780993</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ var select = document.createElement("select");
+ var option = document.createElement("option");
+ select.appendChild(option);
+ assert_equals(select[0], option);
+ select[0] = null;
+ assert_equals(option.parentNode, null);
+ assert_equals(select[0], undefined);
+}, "Should be able to set select[n] to null.");
+test(function() {
+ var select = document.createElement("select");
+ var option = document.createElement("option");
+ var option2 = document.createElement("option");
+ select.appendChild(option);
+ assert_equals(select[0], option);
+ select[0] = option2;
+ assert_equals(option.parentNode, null);
+ assert_equals(option2.parentNode, select);
+ assert_equals(select[0], option2);
+}, "Should be able to set select[n] to an option element");
+test(function() {
+ var select = document.createElement("select");
+ var option = document.createElement("option");
+ select.appendChild(option);
+ assert_equals(select[0], option);
+ assert_throws(null, function() {
+ select[0] = 42;
+ });
+ assert_equals(option.parentNode, select);
+ assert_equals(select[0], option);
+}, "Should not be able to set select[n] to a primitive.");
+</script>
diff --git a/dom/html/test/test_bug787134.html b/dom/html/test/test_bug787134.html
new file mode 100644
index 0000000000..59aee4e463
--- /dev/null
+++ b/dom/html/test/test_bug787134.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=787134
+-->
+<head>
+ <title>Test for Bug 787134</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=787134">Mozilla Bug 787134</a>
+<p id="display"></p>
+<p><a id="link-test1" href="example link">example link</a></p>
+<pre id="test">
+<script>
+ var div = document.createElement('div');
+ div.innerHTML = '<a href=#></a>';
+ var a = div.firstChild;
+ ok(a.matches(':link'), "Should match a link not in a document");
+ is(div.querySelector(':link'), a, "Should find a link not in a document");
+ a = document.querySelector('#link-test1');
+ ok(a.matches(':link'), "Should match a link in a document with an invalid URL");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug797113.html b/dom/html/test/test_bug797113.html
new file mode 100644
index 0000000000..6c246eb3c3
--- /dev/null
+++ b/dom/html/test/test_bug797113.html
@@ -0,0 +1,39 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test for bug 780993</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+test(function() {
+ var select = document.createElement("select");
+ var option = document.createElement("option");
+ select.appendChild(option);
+ assert_equals(select.options[0], option);
+ select.options[0] = null;
+ assert_equals(option.parentNode, null);
+ assert_equals(select.options[0], undefined);
+}, "Should be able to set select.options[n] to null.");
+test(function() {
+ var select = document.createElement("select");
+ var option = document.createElement("option");
+ var option2 = document.createElement("option");
+ select.appendChild(option);
+ assert_equals(select.options[0], option);
+ select.options[0] = option2;
+ assert_equals(option.parentNode, null);
+ assert_equals(option2.parentNode, select);
+ assert_equals(select.options[0], option2);
+}, "Should be able to set select.options[n] to an option element");
+test(function() {
+ var select = document.createElement("select");
+ var option = document.createElement("option");
+ select.appendChild(option);
+ assert_equals(select.options[0], option);
+ assert_throws(null, function() {
+ select.options[0] = 42;
+ });
+ assert_equals(option.parentNode, select);
+ assert_equals(select.options[0], option);
+}, "Should not be able to set select.options[n] to a primitive.");
+</script>
diff --git a/dom/html/test/test_bug803677.html b/dom/html/test/test_bug803677.html
new file mode 100644
index 0000000000..640f747528
--- /dev/null
+++ b/dom/html/test/test_bug803677.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=803677
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 803677</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<style>
+ .base { border:1px solid gray; }
+ .bad-table { display:table-cell; border:1px solid red; }
+</style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=803677">Mozilla Bug 803677</a>
+<p id="display"></p>
+<div id="content">
+ <p class="base">1</p>
+ <p class="base">2</p>
+ <p class="base">3</p>
+ <p class="base bad-table">4</p>
+ <p class="base">7</p>
+ <p class="base">8</p>
+ <p class="base">9</p>
+</div>
+<pre id="test">
+<script type="application/javascript">
+ var p = document.querySelectorAll(".base");
+ var parent = document.querySelector("body");
+ var prevOffset = 0;
+ for (var i = 0; i < p.length; i++) {
+ var t = 0, e = p[i];
+ is(e.offsetParent, parent, "Offset parent of all paragraphs should be the body.");
+ while (e) {
+ t += e.offsetTop;
+ e = e.offsetParent;
+ }
+ p[i].innerHTML = t;
+
+ ok(t > prevOffset, "Offset should increase down the page");
+ prevOffset = t;
+ }
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug821307.html b/dom/html/test/test_bug821307.html
new file mode 100644
index 0000000000..591018da17
--- /dev/null
+++ b/dom/html/test/test_bug821307.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=821307
+-->
+<head>
+ <title>Test for Bug 821307</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=821307">Mozilla Bug 821307</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<input id='dummy'></input>
+<input type="password" id='input' value='11111111111111111' style="width:40em; font-size:40px;"></input>
+
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ var dummy = document.getElementById('dummy');
+ dummy.focus();
+ is(document.activeElement, dummy, "Check dummy element is now focused");
+
+ var input = document.getElementById('input');
+ var rect = input.getBoundingClientRect();
+ synthesizeMouse(input, 100, rect.height/2, {});
+ is(document.activeElement, input, "Check input element is now focused");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug827126.html b/dom/html/test/test_bug827126.html
new file mode 100644
index 0000000000..c4cf28d44c
--- /dev/null
+++ b/dom/html/test/test_bug827126.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=827126
+-->
+<head>
+ <title>Test for Bug 827126</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=827126">Mozilla Bug 827126</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test to ensure we reflect <img align> correctly **/
+reflectString({
+ element: new Image(),
+ attribute: "align",
+ otherValues: [ "left", "right", "middle", "justify" ]
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug838582.html b/dom/html/test/test_bug838582.html
new file mode 100644
index 0000000000..2d412a041b
--- /dev/null
+++ b/dom/html/test/test_bug838582.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=838582
+-->
+<head>
+ <title>Test for Bug 838582</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=838582">Mozilla Bug 838582</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<textarea id="t">abc</textarea>
+<script type="application/javascript">
+
+/** Test for Bug 838582 **/
+
+var textarea = document.getElementById("t");
+
+is(t.textLength, 3, "Correct textLength for defaultValue");
+t.value = "abcdef";
+is(t.textLength, 6, "Correct textLength for value");
+ok(!("controllers" in t), "Don't have web-visible controllers property");
+ok("controllers" in SpecialPowers.wrap(t), "Have chrome-visible controllers property");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug839371.html b/dom/html/test/test_bug839371.html
new file mode 100644
index 0000000000..5d434a1803
--- /dev/null
+++ b/dom/html/test/test_bug839371.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=839371
+-->
+<head>
+ <title>Test for Bug 839371</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=839371">Mozilla Bug 839371</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+<div itemscope>
+ <data id="d1" itemprop="product-id" value="9678AOU879">The Instigator 2000</data>
+</div>
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 839371 **/
+
+var d1 = document.getElementById("d1"),
+ d2 = document.createElement("data");
+
+// .value IDL
+is(d1.value, "9678AOU879", "value property reflects content attribute");
+d1.value = "123";
+is(d1.value, "123", "value property can be set via setter");
+
+// .value reflects value attribute
+reflectString({
+ element: d2,
+ attribute: "value"
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug839913.html b/dom/html/test/test_bug839913.html
new file mode 100644
index 0000000000..7397fa3b6b
--- /dev/null
+++ b/dom/html/test/test_bug839913.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for HTMLAreaElement's stringifier</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(function() {
+ var area = document.createElement("area");
+ area.href = "http://example.org/";
+ assert_equals(area.href, "http://example.org/");
+ assert_equals(String(area), "http://example.org/");
+}, "Area elements should stringify to the href attribute");
+</script>
diff --git a/dom/html/test/test_bug841466.html b/dom/html/test/test_bug841466.html
new file mode 100644
index 0000000000..98eb9a305e
--- /dev/null
+++ b/dom/html/test/test_bug841466.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=841466
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 841466</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+ <script>
+ /** Test for Bug 841466 **/
+var els = ['button', 'fieldset', 'input', 'object', 'output', 'select', 'textarea'];
+var code = "try { is(foo, 'bar', 'expected value bar from expando on element ' + localName); } catch (e) { ok(false, String(e)); }";
+els.forEach(function(el) {
+ var f = document.createElement("form");
+ f.foo = "bar";
+ f.innerHTML = '<' + el + ' onclick="' + code + '">';
+ var e = f.firstChild
+ e.dispatchEvent(new Event("click"));
+})
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=841466">Mozilla Bug 841466</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug845057.html b/dom/html/test/test_bug845057.html
new file mode 100644
index 0000000000..ef0d45d9ed
--- /dev/null
+++ b/dom/html/test/test_bug845057.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=845057
+-->
+<head>
+ <title>Test for Bug 845057</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=845057">Mozilla Bug 845057</a>
+<p id="display"></p>
+<div id="content">
+ <iframe id="iframe" sandbox="allow-scripts"></iframe>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ var iframe = document.getElementById("iframe"),
+ attr = iframe.sandbox;
+ // Security enforcement tests for iframe sandbox are in test_iframe_*
+
+ function eq(a, b) {
+ // check if two attributes are qual modulo permutation
+ return ((a+'').split(" ").sort()+'') == ((b+'').split(" ").sort()+'');
+ }
+
+ ok(attr instanceof DOMTokenList,
+ "Iframe sandbox attribute is instace of DOMTokenList");
+ ok(eq(attr, "allow-scripts") &&
+ eq(iframe.getAttribute("sandbox"), "allow-scripts"),
+ "Stringyfied sandbox attribute is same as that of the DOM element");
+
+ ok(attr.contains("allow-scripts") && !attr.contains("allow-same-origin"),
+ "Set membership of attribute elements is ok");
+
+ attr.add("allow-same-origin");
+
+ ok(attr.contains("allow-scripts") && attr.contains("allow-same-origin"),
+ "Attribute contains added atom");
+ ok(eq(attr, "allow-scripts allow-same-origin") &&
+ eq(iframe.getAttribute("sandbox"), "allow-scripts allow-same-origin"),
+ "Stringyfied attribute with new atom is correct");
+
+ attr.add("allow-forms");
+ attr.remove("allow-scripts");
+
+ ok(!attr.contains("allow-scripts") && attr.contains("allow-forms") &&
+ attr.contains("allow-same-origin"),
+ "Attribute does not contain removed atom");
+ ok(eq(attr, "allow-forms allow-same-origin") &&
+ eq(iframe.getAttribute("sandbox"), "allow-forms allow-same-origin"),
+ "Stringyfied attribute with removed atom is correct");
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_bug869040.html b/dom/html/test/test_bug869040.html
new file mode 100644
index 0000000000..c7edcd89d9
--- /dev/null
+++ b/dom/html/test/test_bug869040.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=869040
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 869040</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=869040">Mozilla Bug 869040</a>
+<p id="display"></p>
+<div id="content" style="display: none" data-foo="present1" data-bar="present2">
+
+</div>
+<pre id="test">
+</pre>
+ <script type="application/javascript">
+
+ /** Test for Bug 869040 **/
+ var foo = "default1";
+ var dataset = $("content").dataset;
+ for (var i = 0; i < 100000; ++i)
+ foo = dataset.foo;
+
+ var bar = "default2";
+ for (var j = 0; j < 100; ++j)
+ bar = dataset.bar;
+
+ is(foo, "present1", "Our IC should work");
+ is(bar, "present2", "Our non-IC case should work");
+ </script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug870787.html b/dom/html/test/test_bug870787.html
new file mode 100644
index 0000000000..d6f66dda32
--- /dev/null
+++ b/dom/html/test/test_bug870787.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=870787
+-->
+<head>
+ <title>Test for Bug 870787</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=870787">Mozilla Bug 870787</a>
+
+<p id="msg"></p>
+
+<form id="form0"></form>
+<img name="img0" id="img0id">
+
+<img name="img1" id="img1id" />
+<form id="form1">
+ <img name="img2" id="img2id" />
+</form>
+<img name="img3" id="img3id" />
+
+<table>
+ <form id="form2">
+ <tr><td>
+ <button name="input1" id="input1id" />
+ <input name="input2" id="input2id" />
+ </form>
+</table>
+
+<table>
+ <form id="form3">
+ <tr><td>
+ <img name="img4" id="img4id" />
+ <img name="img5" id="img5id" />
+ </form>
+</table>
+
+<form id="form4"><img id="img6"></form>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 870787 **/
+
+var form0 = document.getElementById("form0");
+ok(form0, "Form0 exists");
+ok(!form0.img0, "Form0.img0 doesn't exist");
+ok(!form0.img0id, "Form0.img0id doesn't exist");
+
+var form1 = document.getElementById("form1");
+ok(form1, "Form1 exists");
+ok(!form1.img1, "Form1.img1 doesn't exist");
+ok(!form1.img1id, "Form1.img1id doesn't exist");
+is(form1.img2, document.getElementById("img2id"), "Form1.img2 exists");
+is(form1.img2id, document.getElementById("img2id"), "Form1.img2id exists");
+ok(!form1.img3, "Form1.img3 doesn't exist");
+ok(!form1.img3id, "Form1.img3id doesn't exist");
+
+var form2 = document.getElementById("form2");
+ok(form2, "Form2 exists");
+is(form2.input1, document.getElementById("input1id"), "Form2.input1 exists");
+is(form2.input1id, document.getElementById("input1id"), "Form2.input1id exists");
+is(form2.input2, document.getElementById("input2id"), "Form2.input2 exists");
+is(form2.input2id, document.getElementById("input2id"), "Form2.input2id exists");
+
+var form3 = document.getElementById("form3");
+ok(form3, "Form3 exists");
+is(form3.img4, document.getElementById("img4id"), "Form3.img4 doesn't exists");
+is(form3.img4id, document.getElementById("img4id"), "Form3.img4id doesn't exists");
+is(form3.img5, document.getElementById("img5id"), "Form3.img5 doesn't exists");
+is(form3.img5id, document.getElementById("img5id"), "Form3.img5id doesn't exists");
+
+var form4 = document.getElementById("form4");
+ok(form4, "Form4 exists");
+is(Object.getOwnPropertyNames(form4.elements).indexOf("img6"), -1, "Form4.elements should not contain img6");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug871161.html b/dom/html/test/test_bug871161.html
new file mode 100644
index 0000000000..c4512621b6
--- /dev/null
+++ b/dom/html/test/test_bug871161.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=871161
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 871161</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 871161 **/
+ SimpleTest.waitForExplicitFinish();
+
+ window.onmessage = function(e) {
+ is(e.data, "windows-1252", "Wrong charset");
+ e.source.close();
+ SimpleTest.finish();
+ }
+
+ function run() {
+ window.open("file_bug871161-1.html");
+ }
+
+ </script>
+</head>
+<body onload="run();">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=871161">Mozilla Bug 871161</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug874758.html b/dom/html/test/test_bug874758.html
new file mode 100644
index 0000000000..fa77225ba6
--- /dev/null
+++ b/dom/html/test/test_bug874758.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html data-expando-prop="xyz">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=874758
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 874758</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 874758 **/
+ Object.prototype.expandoProp = 5;
+ is({}.expandoProp, 5, "Should see this on random objects");
+
+ is(document.head.dataset.expandoProp, 5, "Should see this on dataset too");
+ is(document.documentElement.dataset.expandoProp, "xyz",
+ "But if the dataset has it, we should get it from there");
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=874758">Mozilla Bug 874758</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug879319.html b/dom/html/test/test_bug879319.html
new file mode 100644
index 0000000000..692f880449
--- /dev/null
+++ b/dom/html/test/test_bug879319.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=879319
+-->
+<head>
+ <title>Test for Bug 879319</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=879319">Mozilla Bug 879319</a>
+
+<p id="msg"></p>
+
+<form id="form">
+ <img id="img0" name="bar0" />
+</form>
+<input id="input0" name="foo0" form="form" />
+<input id="input1" name="foo1" form="form" />
+<input id="input2" name="foo2" form="form" />
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 879319 **/
+
+var input0 = document.getElementById("input0");
+ok(input0, "input0 exists");
+
+var form = document.getElementById("form");
+ok(form, "form exists");
+is(form.foo0, input0, "Form.foo0 should exist");
+
+ok("foo0" in form.elements, "foo0 in form.elements");
+is(input0.form, form, "input0.form is form");
+
+input0.setAttribute("name", "tmp0");
+ok("tmp0" in form.elements, "tmp0 is in form.elements");
+ok(!("foo0" in form.elements), "foo0 is not in form.elements");
+is(form.tmp0, input0, "Form.tmp0 == input0");
+is(form.foo0, input0, "Form.foo0 is still here");
+
+input0.setAttribute("name", "tmp1");
+ok("tmp1" in form.elements, "tmp1 is in form.elements");
+ok(!("tmp0" in form.elements), "tmp0 is not in form.elements");
+ok(!("foo0" in form.elements), "foo0 is not in form.elements");
+is(form.tmp0, input0, "Form.tmp0 == input0");
+is(form.tmp1, input0, "Form.tmp1 == input0");
+is(form.foo0, input0, "Form.foo0 is still here");
+
+input0.setAttribute("form", "");
+ok(!("foo0" in form.elements), "foo0 is not in form.elements");
+is(form.foo0, undefined, "Form.foo0 should not still be here");
+is(form.tmp0, undefined, "Form.tmp0 should not still be here");
+is(form.tmp1, undefined, "Form.tmp1 should not still be here");
+
+var input1 = document.getElementById("input1");
+ok(input1, "input1 exists");
+is(form.foo1, input1, "Form.foo1 should exist");
+
+ok("foo1" in form.elements, "foo1 in form.elements");
+is(input1.form, form, "input1.form is form");
+
+input1.setAttribute("name", "foo0");
+ok("foo0" in form.elements, "foo0 is in form.elements");
+is(form.foo0, input1, "Form.foo0 should be input1");
+is(form.foo1, input1, "Form.foo1 should be input1");
+
+var input2 = document.getElementById("input2");
+ok(input2, "input2 exists");
+is(form.foo2, input2, "Form.foo2 should exist");
+input2.remove();
+ok(!("foo2" in form.elements), "foo2 is not in form.elements");
+is(form.foo2, undefined, "Form.foo2 should not longer be there");
+
+var img0 = document.getElementById("img0");
+ok(img0, "img0 exists");
+is(form.bar0, img0, "Form.bar0 should exist");
+
+img0.setAttribute("name", "old_bar0");
+is(form.old_bar0, img0, "Form.bar0 is still here");
+is(form.bar0, img0, "Form.bar0 is still here");
+
+img0.remove();
+is(form.bar0, undefined, "Form.bar0 should not be here");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug885024.html b/dom/html/test/test_bug885024.html
new file mode 100644
index 0000000000..96f1783910
--- /dev/null
+++ b/dom/html/test/test_bug885024.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html data-expando-prop="xyz">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=885024
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 885024</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885024">Mozilla Bug 885024</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<img form="t">
+
+<form id="form">
+ <div id="div"></div>
+</form>
+
+<pre id="test">
+ <script type="application/javascript">
+ var img = document.createElement('img');
+ img.setAttribute('id', 'img');
+
+ var div = document.getElementById('div');
+ div.appendChild(img);
+
+ var form = document.getElementById('form');
+ ok(form, "form exists");
+ ok(form.img, "form.img exists");
+
+ var img2 = document.createElement('img');
+ img2.setAttribute('id', 'img2');
+ img2.setAttribute('form', 'blabla');
+ ok(form, "form exists2");
+ div.appendChild(img2);
+ ok(form.img2, "form.img2 exists");
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug893537.html b/dom/html/test/test_bug893537.html
new file mode 100644
index 0000000000..5935529d87
--- /dev/null
+++ b/dom/html/test/test_bug893537.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=893537
+-->
+ <head>
+<title>Test for crash caused by unloading and reloading srcdoc iframes</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=893537">Mozilla Bug 893537</a>
+
+<iframe id="pframe" src="file_bug893537.html"></iframe>
+
+<pre id="test">
+<script>
+ <!-- Bug 895303 -->
+ SimpleTest.expectAssertions(0, 1);
+
+ SimpleTest.waitForExplicitFinish();
+ var pframe = $("pframe");
+
+ var loadState = 1;
+ pframe.contentWindow.addEventListener("load", function () {
+
+ if (loadState == 1) {
+ var iframe = pframe.contentDocument.getElementById("iframe");
+ iframe.removeAttribute("srcdoc");
+ loadState = 2;
+ }
+ if (loadState == 2) {
+ SimpleTest.executeSoon(function () { pframe.contentWindow.location.reload() });
+ loadState = 3;
+ }
+ if (loadState == 3) {
+ ok(true, "This is a mochitest implementation of a crashtest. To finish is to pass");
+ SimpleTest.finish();
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug95530.html b/dom/html/test/test_bug95530.html
new file mode 100644
index 0000000000..c4a3078d3e
--- /dev/null
+++ b/dom/html/test/test_bug95530.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=95530
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 95530</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 95530 **/
+ function run() {
+ is(document.compatMode, "CSS1Compat", "Ensure we are in standards mode, not quirks mode.");
+
+ var body = document.getElementsByTagName("body");
+
+ is(computedStyle(body[0],"margin-top"), "100px", "Ensure margin-top matches topmargin");
+ is(computedStyle(body[0],"margin-bottom"), "150px", "Ensure margin-bottom matches bottommargin");
+ is(computedStyle(body[0],"margin-left"), "23px", "Ensure margin-left matches leftmargin");
+ is(computedStyle(body[0],"margin-right"), "64px", "Ensure margin-right matches rightmargin");
+ SimpleTest.finish();
+ }
+ SimpleTest.waitForExplicitFinish();
+ window.addEventListener("load", run);
+ </script>
+</head>
+<body topmargin="100" bottommargin="150" leftmargin="23" rightmargin="64">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=95530">Mozilla Bug 95530</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug969346.html b/dom/html/test/test_bug969346.html
new file mode 100644
index 0000000000..5be76c46ec
--- /dev/null
+++ b/dom/html/test/test_bug969346.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=969346
+-->
+<head>
+<title>Nesting of srcdoc iframes is permitted</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=969349">Mozilla Bug 969346</a>
+
+<iframe id="pframe" srcdoc="<iframe id='iframe' srcdoc='I am nested'></iframe"></iframe>
+
+<pre id="test">
+<script>
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(function () {
+ var pframe = $("pframe");
+ var pframeDoc = pframe.contentDocument;
+ var iframe = pframeDoc.getElementById("iframe");
+ var innerDoc = iframe.contentDocument;
+
+ is(innerDoc.body.innerHTML, "I am nested", "Nesting not working?");
+ SimpleTest.finish();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug982039.html b/dom/html/test/test_bug982039.html
new file mode 100644
index 0000000000..6b158413bc
--- /dev/null
+++ b/dom/html/test/test_bug982039.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=982039
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 982039</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 982039 **/
+ SimpleTest.waitForExplicitFinish();
+ function test() {
+ var f = document.getElementById("testform");
+ f.elements[0].disabled = true;
+ is(f.checkValidity(), false,
+ "Setting a radiobutton to disabled shouldn't make form valid.");
+
+ f.elements[1].checked = true;
+ ok(f.checkValidity(), "Form should be now valid.");
+
+ f.elements[0].required = false;
+ f.elements[1].required = false;
+ f.elements[2].required = false;
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body onload="test()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=982039">Mozilla Bug 982039</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<form action="#" id="testform">
+ <input type="radio" name="radio" value="1" required>
+ <input type="radio" name="radio" value="2" required>
+ <input type="radio" name="radio" value="3" required>
+</form>
+</body>
+</html>
diff --git a/dom/html/test/test_change_crossorigin.html b/dom/html/test/test_change_crossorigin.html
new file mode 100644
index 0000000000..303aac7bea
--- /dev/null
+++ b/dom/html/test/test_change_crossorigin.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=696451
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 696451</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 696451 **/
+
+ SimpleTest.waitForExplicitFinish();
+
+ var img = new Image,
+ canvas = document.createElement("canvas"),
+ ctx = canvas.getContext("2d"),
+ src = "http://example.com/tests/dom/html/test/image-allow-credentials.png",
+ imgDone = false,
+ imgNotAllowedToLoadDone = false;
+
+ img.src = src;
+ img.crossOrigin = "Anonymous";
+
+ img.addEventListener("load", function() {
+ canvas.width = img.width;
+ canvas.height = img.height;
+ ctx.drawImage( img, 0, 0 );
+ try {
+ canvas.toDataURL("image/png");
+ ok(true, "Image was refetched with setting crossOrigin.");
+ } catch (e) {
+ ok(false, "Image was not refetched after setting crossOrigin.");
+ }
+
+ imgDone = true;
+ if (imgDone && imgNotAllowedToLoadDone) {
+ SimpleTest.finish();
+ }
+ });
+
+ img.addEventListener("error", function (event) {
+ ok(false, "Should be able to load cross origin image with proper headers.");
+
+ imgDone = true;
+ if (imgDone && imgNotAllowedToLoadDone) {
+ SimpleTest.finish();
+ }
+ });
+
+ var imgNotAllowedToLoad = new Image;
+
+ imgNotAllowedToLoad.src = "http://example.com/tests/dom/html/test/image.png";
+
+ imgNotAllowedToLoad.crossOrigin = "Anonymous";
+
+ imgNotAllowedToLoad.addEventListener("load", function() {
+ ok(false, "Image should not be allowed to load without " +
+ "allow-cross-origin-access headers.");
+
+ imgNotAllowedToLoadDone = true;
+ if (imgDone && imgNotAllowedToLoadDone) {
+ SimpleTest.finish();
+ }
+ });
+
+ imgNotAllowedToLoad.addEventListener("error", function() {
+ ok(true, "Image should not be allowed to load without " +
+ "allow-cross-origin-access headers.");
+ imgNotAllowedToLoadDone = true;
+ if (imgDone && imgNotAllowedToLoadDone) {
+ SimpleTest.finish();
+ }
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=696451">Mozilla Bug 696451</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_checked.html b/dom/html/test/test_checked.html
new file mode 100644
index 0000000000..d69dcf2a28
--- /dev/null
+++ b/dom/html/test/test_checked.html
@@ -0,0 +1,347 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=418756
+-->
+<head>
+ <title>Test for Bug 418756</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Mozilla bug
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418756">418756</a>
+<p id="display"></p>
+<div id="content">
+ <form id="f1">
+ </form>
+ <form id="f2">
+ </form>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 418756 **/
+var group1;
+var group2;
+var group3;
+
+function bounce(node) {
+ let n = node.nextSibling;
+ let p = node.parentNode;
+ p.removeChild(node);
+ p.insertBefore(node, n);
+}
+
+var createdNodes = [];
+
+function cleanup() {
+ for (let node of createdNodes) {
+ if (node.parentNode) {
+ node.remove();
+ }
+ }
+
+ createdNodes = [];
+}
+
+var typeMapper = {
+ 'c': 'checkbox',
+ 'r': 'radio'
+};
+
+var id = 0;
+
+// type can be 'c' for 'checkbox' and 'r' for 'radio'
+function createNode(type, name, checked) {
+ let node = document.createElement("input");
+ node.setAttribute("type", typeMapper[type]);
+ if (checked) {
+ node.setAttribute("checked", "checked");
+ }
+ node.setAttribute("id", type + (++id));
+ node.setAttribute("name", name);
+ createdNodes.push(node);
+ return node;
+}
+
+var types = ['c', 'r'];
+
+// First make sure that setting .checked makes .defaultChecked changes no
+// longer affect .checked.
+for (let type of types) {
+ let n = createNode(type, '', false);
+ is(n.defaultChecked, false, "Bogus defaultChecked on " + typeMapper[type]);
+ is(n.checked, false, "Bogus checked on " + typeMapper[type]);
+ n.defaultChecked = true;
+ is(n.defaultChecked, true, "Bogus defaultChecked on " + typeMapper[type] +
+ "after mutation");
+ is(n.checked, true, "Bogus checked on " + typeMapper[type] +
+ "after mutation");
+ n.checked = false;
+ is(n.defaultChecked, true, "Bogus defaultChecked on " + typeMapper[type] +
+ "after second mutation");
+ is(n.checked, false, "Bogus checked on " + typeMapper[type] +
+ "after second mutation");
+ n.defaultChecked = false;
+ is(n.defaultChecked, false, "Bogus defaultChecked on " + typeMapper[type] +
+ "after third mutation");
+ is(n.checked, false, "Bogus checked on " + typeMapper[type] +
+ "after third mutation");
+ n.defaultChecked = true;
+ is(n.defaultChecked, true, "Bogus defaultChecked on " + typeMapper[type] +
+ "after fourth mutation");
+ is(n.checked, false, "Bogus checked on " + typeMapper[type] +
+ "after fourth mutation");
+}
+
+cleanup();
+
+// Now check that bouncing a control that's the only one of its kind has no
+// effect
+for (let type of types) {
+ let n = createNode(type, 'test1', true);
+ $("f1").appendChild(n);
+ n.checked = false;
+ n.defaultChecked = false;
+ bounce(n);
+ n.defaultChecked = true;
+ is(n.checked, false, "We set .checked on this " + typeMapper[type]);
+}
+
+cleanup();
+
+// Now check that playing with a single radio in a group affects all
+// other radios in the group (but not radios not in that group)
+group1 = [ createNode('r', 'g1', false),
+ createNode('r', 'g1', false),
+ createNode('r', 'g1', false) ];
+group2 = [ createNode('r', 'g2', false),
+ createNode('r', 'g2', false),
+ createNode('r', 'g2', false) ];
+group3 = [ createNode('r', 'g1', false),
+ createNode('r', 'g1', false),
+ createNode('r', 'g1', false) ];
+for (let g of group1) {
+ $("f1").appendChild(g);
+}
+for (let g of group2) {
+ $("f1").appendChild(g);
+}
+for (let g of group3) {
+ $("f2").appendChild(g);
+}
+
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, false,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 1");
+ is(g.checked, false,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checkedhecked wrong pass 1");
+ }
+}
+
+group1[1].defaultChecked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1 && group1.indexOf(g) == 1,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 2");
+ is(g.checked, n == 1 && group1.indexOf(g) == 1,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 2");
+ }
+}
+
+group1[0].defaultChecked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 1 ||
+ group1.indexOf(g) == 0),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 3");
+ is(g.checked, n == 1 && group1.indexOf(g) == 0,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 3");
+ }
+}
+
+group1[2].defaultChecked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 4");
+ is(g.checked, n == 1 && group1.indexOf(g) == 2,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 4");
+ }
+}
+
+var next = group1[1].nextSibling;
+var p = group1[1].parentNode;
+p.removeChild(group1[1]);
+group1[1].defaultChecked = false;
+group1[1].defaultChecked = true;
+p.insertBefore(group1[1], next);
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 5");
+ is(g.checked, n == 1 && group1.indexOf(g) == 1,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 5");
+ }
+}
+
+for (let g of group1) {
+ g.defaultChecked = false;
+}
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, false,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 6");
+ is(g.checked, false,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checkedhecked wrong pass 6");
+ }
+}
+
+group1[1].checked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, false,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 7");
+ is(g.checked, n == 1 && group1.indexOf(g) == 1,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 7");
+ }
+}
+
+group1[0].defaultChecked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1 && group1.indexOf(g) == 0,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 8");
+ is(g.checked, n == 1 && group1.indexOf(g) == 1,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 8");
+ }
+}
+
+group1[2].defaultChecked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 ||
+ group1.indexOf(g) == 2),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 9");
+ is(g.checked, n == 1 && group1.indexOf(g) == 1,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 9");
+ }
+}
+group1[1].remove();
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 ||
+ group1.indexOf(g) == 2),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 10");
+ is(g.checked, n == 1 && group1.indexOf(g) == 1,
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 10");
+ }
+}
+
+group1[2].checked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 ||
+ group1.indexOf(g) == 2),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 11");
+ is(g.checked, n == 1 && (group1.indexOf(g) == 1 ||
+ group1.indexOf(g) == 2),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 11");
+ }
+}
+
+group1[0].checked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 ||
+ group1.indexOf(g) == 2),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 12");
+ is(g.checked, n == 1 && (group1.indexOf(g) == 1 ||
+ group1.indexOf(g) == 0),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 12");
+ }
+}
+
+next = group2[1].nextSibling;
+p = group2[1].parentNode;
+p.removeChild(group2[1]);
+p.insertBefore(group2[1], next);
+group2[0].checked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 ||
+ group1.indexOf(g) == 2),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 13");
+ is(g.checked, (n == 1 && (group1.indexOf(g) == 1 ||
+ group1.indexOf(g) == 0)) ||
+ (n == 2 && group2.indexOf(g) == 0),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 13");
+ }
+}
+
+p.insertBefore(group2[1], next);
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 ||
+ group1.indexOf(g) == 2),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 14");
+ is(g.checked, (n == 1 && (group1.indexOf(g) == 1 ||
+ group1.indexOf(g) == 0)) ||
+ (n == 2 && group2.indexOf(g) == 0),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 14");
+ }
+}
+
+group2[1].defaultChecked = true;
+for (let n of [1, 2, 3]) {
+ for (let g of window["group"+n]) {
+ is(g.defaultChecked, (n == 1 && (group1.indexOf(g) == 0 ||
+ group1.indexOf(g) == 2)) ||
+ (n == 2 && group2.indexOf(g) == 1),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] defaultChecked wrong pass 15");
+ is(g.checked, (n == 1 && (group1.indexOf(g) == 1 ||
+ group1.indexOf(g) == 0)) ||
+ (n == 2 && group2.indexOf(g) == 0),
+ "group" + n + "[" + window["group"+n].indexOf(g) +
+ "] checked wrong pass 15");
+ }
+}
+
+cleanup();
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_dir_attributes_reflection.html b/dom/html/test/test_dir_attributes_reflection.html
new file mode 100644
index 0000000000..3aefaef9a5
--- /dev/null
+++ b/dom/html/test/test_dir_attributes_reflection.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLDirectoryElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLDirectoryElement attributes reflection **/
+
+// .name
+reflectBoolean({
+ element: document.createElement("dir"),
+ attribute: "compact",
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_dl_attributes_reflection.html b/dom/html/test/test_dl_attributes_reflection.html
new file mode 100644
index 0000000000..100b28e9fb
--- /dev/null
+++ b/dom/html/test/test_dl_attributes_reflection.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLDListElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLDListElement attributes reflection **/
+
+// .compact
+reflectBoolean({
+ element: document.createElement("dl"),
+ attribute: "compact"
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_document-element-inserted.html b/dom/html/test/test_document-element-inserted.html
new file mode 100644
index 0000000000..6d7e8695ce
--- /dev/null
+++ b/dom/html/test/test_document-element-inserted.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: document-element-inserted</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id = 'media'>
+</iframe>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+if (navigator.platform.startsWith("Win")) {
+ SimpleTest.expectAssertions(0, 4);
+}
+
+SimpleTest.waitForExplicitFinish();
+var loc;
+
+var observe = function(doc){
+ if (doc == media.contentDocument) {
+ ok(media.contentDocument.location.toString().includes(loc),
+ "The loaded media should be " + loc);
+ next();
+ }
+}
+
+var media = document.getElementById('media');
+var tests = [
+ "../../../media/test/short-video.ogv",
+ "../../../media/test/sound.ogg",
+ "../../content/test/image.png"
+]
+
+function next() {
+ if (tests.length) {
+ var t = tests.shift();
+ loc = t.substring(t.indexOf("test"));
+ media.setAttribute("src",t);
+ }
+ else {
+ SpecialPowers.removeObserver(observe, "document-element-inserted");
+ SimpleTest.finish();
+ }
+}
+
+SpecialPowers.addObserver(observe, "document-element-inserted")
+next();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_documentAll.html b/dom/html/test/test_documentAll.html
new file mode 100644
index 0000000000..f3bb7e7df6
--- /dev/null
+++ b/dom/html/test/test_documentAll.html
@@ -0,0 +1,167 @@
+<html>
+<!--
+Tests for document.all
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <title>Tests for document.all</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=259332">Mozilla Bug 259332</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=393629">Mozilla Bug 393629</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=448904">Mozilla Bug 448904</a>
+<p id="display">
+</p>
+<div id="content" style="display: none">
+ <a id="id1">A</a>
+ <a id="id2">B</a>
+ <a id="id2">C</a>
+ <a id="id3">D</a>
+ <a id="id3">E</a>
+ <a id="id3">F</a>
+</div>
+<iframe id="subframe" srcdoc="<span id='x'></span>"
+ style="display: none"></iframe>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+p = document.getElementById("content");
+
+// Test that several elements with the same id or name behave correctly
+function testNumSame() {
+ is(document.all.id0, undefined, "no ids");
+ is(document.all.namedItem("id0"), null, "no ids");
+ is(document.all.id1, p.children[0], "one id");
+ is(document.all.id2[0], p.children[1], "two ids");
+ is(document.all.id2[1], p.children[2], "two ids");
+ is(document.all.id2.length, 2, "two length");
+ is(document.all.id3[0], p.children[3], "three ids");
+ is(document.all.id3[1], p.children[4], "three ids");
+ is(document.all.id3[2], p.children[5], "three ids");
+ is(document.all.id3.length, 3, "three length");
+}
+testNumSame();
+p.innerHTML = p.innerHTML.replace(/id=/g, "name=");
+testNumSame();
+
+
+// Test that dynamic changes behave properly
+
+// Add two elements and check that they are added to the correct lists
+child = Array.prototype.slice.call(p.children);
+child[6] = document.createElement("a");
+child[6].id = "id0";
+p.appendChild(child[6]);
+child[7] = document.createElement("a");
+child[7].id = "id1";
+p.appendChild(child[7]);
+is(document.all.id0, child[6], "now one id");
+is(document.all.id1[0], child[0], "now two ids");
+is(document.all.id1[1], child[7], "now two ids");
+is(document.all.id1.length, 2, "now two length");
+
+// Remove and element and check that the list shrinks
+rC(child[1]);
+is(document.all.id2, child[2], "now just one id");
+
+// Change an id and check that its removed and added to the correct lists
+child[4].name = "id1";
+is(document.all.id1[0], child[0], "now three ids");
+is(document.all.id1[1], child[4], "now three ids");
+is(document.all.id1[2], child[7], "now three ids");
+is(document.all.id1.length, 3, "now three length");
+is(document.all.id3[1], child[5], "now just two ids");
+is(document.all.id3.length, 2, "now two length");
+
+// Remove all elements from a list and check that it goes empty
+id3list = document.all.id3;
+rC(child[3]);
+is(id3list.length, 1, "now one length");
+rC(child[5]);
+is(document.all.id3, undefined, "now none");
+is(document.all.namedItem("id3"), null, "now none (namedItem)");
+is(id3list.length, 0, "now none length");
+
+// Give an element both a name and id and check that it appears in two lists
+p.insertBefore(child[1], child[2]); // restore previously removed
+id1list = document.all.id1;
+id2list = document.all.id2;
+child[1].id = "id1";
+is(id1list[0], child[0], "now four ids");
+is(id1list[1], child[1], "now four ids");
+is(id1list[2], child[4], "now four ids");
+is(id1list[3], child[7], "now four ids");
+is(id1list.length, 4, "now four length");
+is(id2list[0], child[1], "still two ids");
+is(id2list[1], child[2], "still two ids");
+is(id2list.length, 2, "still two length");
+
+
+// Check that document.all behaves list a list of all elements
+allElems = document.getElementsByTagName("*");
+ok(testArraysSame(document.all, allElems), "arrays same");
+length = document.all.length;
+expectedLength = length + p.getElementsByTagName("*").length + 1;
+p.appendChild(p.cloneNode(true));
+ok(testArraysSame(document.all, allElems), "arrays still same");
+is(document.all.length, expectedLength, "grew correctly");
+
+// Check which elements the 'name' attribute works on
+var elementNames =
+ ['abbr','acronym','address','area','a','b','base',
+ 'bgsound','big','blockquote','br','canvas','center','cite','code',
+ 'col','colgroup','dd','del','dfn','dir','div','dir','dl','dt','em','embed',
+ 'fieldset','font','form','frame','frameset','head','i','iframe','img',
+ 'input','ins','isindex','kbd','keygen','label','li','legend','link','menu',
+ 'multicol','noscript','noframes','object','spacer','table','td','td','th',
+ 'thead','tfoot','tr','textarea','select','option','spacer','param',
+ 'marquee','hr','title','hx','tt','u','ul','var','wbr','sub','sup','cite',
+ 'code','q','nobr','ol','p','pre','s','samp','small','body','html','map',
+ 'bdo','legend','listing','style','script','tbody','caption','meta',
+ 'optgroup','button','span','strike','strong','td'].sort();
+var hasName =
+ ['a','embed','form','iframe','img','input','object','textarea',
+ 'select','map','meta','button','frame','frameset'].sort();
+
+elementNames.forEach(function (name) {
+ nameval = 'namefor' + name;
+
+ e = document.createElement(name);
+ p.appendChild(e);
+ e.setAttribute('name', nameval);
+
+ if (name == hasName[0]) {
+ is(document.all[nameval], e, "should have name");
+ hasName.shift();
+ }
+ else {
+ is(document.all[nameval], undefined, "shouldn't have name");
+ is(document.all.namedItem(nameval), null, "shouldn't have name (namedItem)");
+ }
+});
+is(hasName.length, 0, "found all names");
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var subdoc = $("subframe").contentDocument;
+ is(subdoc.all.x, subdoc.body.firstChild,
+ "document.all should work in a subdocument");
+ SimpleTest.finish();
+});
+
+// Utility functions
+function rC(node) {
+ node.remove();
+}
+function testArraysSame(a1, a2) {
+ return Array.prototype.every.call(a1, function(e, index) {
+ return a2[index] === e;
+ }) && a1.length == a2.length;
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_element_prototype.html b/dom/html/test/test_element_prototype.html
new file mode 100644
index 0000000000..0cb8d9745f
--- /dev/null
+++ b/dom/html/test/test_element_prototype.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=844127
+-->
+<head>
+ <title>Test for Bug 844127</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=844127">Mozilla Bug 844127</a>
+
+<script type="text/javascript">
+
+/** Test for Bug 844127 **/
+
+var a1 = document.createElement('bgsound');
+var a2 = document.createElement('image');
+var a3 = document.createElement('multicol');
+var a4 = document.createElement('spacer');
+var a5 = document.createElement('isindex');
+
+is(Object.getPrototypeOf(a1), HTMLUnknownElement.prototype, "Prototype for bgsound should be correct");
+is(Object.getPrototypeOf(a2), HTMLElement.prototype, "Prototype for image should be correct");
+is(Object.getPrototypeOf(a3), HTMLUnknownElement.prototype, "Prototype for multicol should be correct");
+is(Object.getPrototypeOf(a4), HTMLUnknownElement.prototype, "Prototype for spacer should be correct");
+is(Object.getPrototypeOf(a5), HTMLUnknownElement.prototype, "Prototype for isindex should be correct");
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_embed_attributes_reflection.html b/dom/html/test/test_embed_attributes_reflection.html
new file mode 100644
index 0000000000..44338113a7
--- /dev/null
+++ b/dom/html/test/test_embed_attributes_reflection.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLEmbedElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLEmbedElement attributes reflection **/
+
+// .src (URL)
+reflectURL({
+ element: document.createElement("embed"),
+ attribute: "src",
+});
+
+// .type (String)
+reflectString({
+ element: document.createElement("embed"),
+ attribute: "type",
+});
+
+// .width (String)
+reflectString({
+ element: document.createElement("embed"),
+ attribute: "width",
+});
+
+// .height (String)
+reflectString({
+ element: document.createElement("embed"),
+ attribute: "height",
+});
+
+// .align (String)
+reflectString({
+ element: document.createElement("embed"),
+ attribute: "align",
+});
+
+// .name (String)
+reflectString({
+ element: document.createElement("embed"),
+ attribute: "name",
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_external_protocol_iframe.html b/dom/html/test/test_external_protocol_iframe.html
new file mode 100644
index 0000000000..cb99a19e1a
--- /dev/null
+++ b/dom/html/test/test_external_protocol_iframe.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for external protocol URLs blocked for iframes</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <div id='foo'><a href='#'>Click here to test this issue</a></div>
+ <script>
+
+function test_initialize() {
+ ChromeUtils.resetLastExternalProtocolIframeAllowed();
+ next();
+}
+
+function test_noUserInteraction() {
+ ok(!SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, "No user interaction yet");
+ is(ChromeUtils.lastExternalProtocolIframeAllowed(), 0, "No iframe loaded before this test!");
+
+ for (let i = 0; i < 10; ++i) {
+ let ifr = document.createElement('iframe');
+ ifr.src = "foo+bar:all_good";
+ document.body.appendChild(ifr);
+
+ is(ChromeUtils.getPopupControlState(), "openAbused", "No user-interaction means: abuse state");
+ ok(ChromeUtils.lastExternalProtocolIframeAllowed() != 0, "We have 1 iframe loaded");
+ }
+
+ next();
+}
+
+function test_userInteraction() {
+ let foo = document.getElementById('foo');
+ foo.addEventListener('click', _ => {
+ ok(SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, "User should've interacted");
+
+ for (let i = 0; i < 10; ++i) {
+ let ifr = document.createElement('iframe');
+ ifr.src = "foo+bar:all_good";
+ document.body.appendChild(ifr);
+
+ ok(!SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, "User interaction should've been consumed");
+ }
+
+ next();
+
+ }, {once: true});
+
+ setTimeout(_ => {
+ synthesizeMouseAtCenter(foo, {});
+ }, 0);
+}
+
+let tests = [
+ test_initialize,
+ test_noUserInteraction,
+ test_userInteraction,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ let test = tests.shift();
+ SimpleTest.executeSoon(test);
+}
+
+SpecialPowers.pushPrefEnv({'set': [
+ ['dom.block_external_protocol_in_iframes', true],
+ ['dom.delay.block_external_protocol_in_iframes.enabled', true],
+]}, next);
+
+SimpleTest.waitForExplicitFinish();
+ </script>
+</body>
+</html>
diff --git a/dom/html/test/test_fakepath.html b/dom/html/test/test_fakepath.html
new file mode 100644
index 0000000000..f9819e732f
--- /dev/null
+++ b/dom/html/test/test_fakepath.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Fakepath in HTMLInputElement</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<input id="file" type="file"></input>
+<input id="file_wd" type="file" webkitdirectory></input>
+<script type="application/javascript">
+
+var url = SimpleTest.getTestFileURL("script_fakepath.js");
+script = SpecialPowers.loadChromeScript(url);
+
+function onOpened(message) {
+ var e = document.getElementById("file");
+ SpecialPowers.wrap(e).mozSetDndFilesAndDirectories(message.data);
+ ok(e.value, "C:\\fakepath\\prefs.js");
+
+ e = document.getElementById("file_wd");
+ SpecialPowers.wrap(e).mozSetDndFilesAndDirectories(message.data);
+ ok(e.value, "C:\\fakepath\\prefs.js");
+
+ SimpleTest.finish();
+}
+
+function run() {
+ script.addMessageListener("file.opened", onOpened);
+ script.sendAsyncMessage("file.open");
+}
+
+SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true]]}, run);
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_filepicker_default_directory.html b/dom/html/test/test_filepicker_default_directory.html
new file mode 100644
index 0000000000..2be811655a
--- /dev/null
+++ b/dom/html/test/test_filepicker_default_directory.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1194893
+-->
+<head>
+ <title>Test for filepicker default directory</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1194893">Mozilla Bug 1194893</a>
+<div id="content">
+ <input type="file" id="f">
+</div>
+<pre id="text">
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+const { Cc: Cc, Ci: Ci } = SpecialPowers;
+
+// Platform-independent directory names are #define'd in xpcom/io/nsDirectoryServiceDefs.h
+
+// When we want to test an upload directory other than the default, we need to
+// get a valid directory in a platform-independent way. Since NS_OS_DESKTOP_DIR
+// may fallback to NS_OS_HOME_DIR, let's use NS_OS_TMP_DIR.
+var customUploadDirectory = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIDirectoryService)
+ .QueryInterface(Ci.nsIProperties)
+ .get("TmpD", Ci.nsIFile);
+
+// Useful for debugging
+//info("customUploadDirectory" + customUploadDirectory.path);
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+// need to show the MockFilePicker so .displayDirectory gets set
+var f = document.getElementById("f");
+f.focus();
+
+var testIndex = 0;
+var tests = [
+ ["", null, "Desk"],
+ [customUploadDirectory.path, customUploadDirectory.path, ""]
+]
+
+MockFilePicker.showCallback = function(filepicker) {
+ if (tests[testIndex][1] === null) {
+ is(SpecialPowers.wrap(MockFilePicker).displayDirectory, null, "DisplayDirectory is null");
+ } else {
+ is(SpecialPowers.wrap(MockFilePicker).displayDirectory.path, tests[testIndex][1], "DisplayDirectory matches the path");
+ }
+
+ is(SpecialPowers.wrap(MockFilePicker).displaySpecialDirectory, tests[testIndex][2], "DisplaySpecialDirectory matches the path");
+
+ if (++testIndex == tests.length) {
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+ } else {
+ launchNextTest();
+ }
+}
+
+function launchNextTest() {
+ SpecialPowers.pushPrefEnv(
+ { 'set': [
+ ['dom.input.fallbackUploadDir', tests[testIndex][0]],
+ ]},
+ function () {
+ f.click();
+ });
+}
+
+launchNextTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_focusshift_button.html b/dom/html/test/test_focusshift_button.html
new file mode 100644
index 0000000000..90fadd4827
--- /dev/null
+++ b/dom/html/test/test_focusshift_button.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for shifting focus while mouse clicking on button</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+
+<script class="testbody" type="application/javascript">
+
+var result = "";
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+ synthesizeMouseAtCenter(document.getElementById("button"), { });
+ is(result, "(focus button)(blur button)(focus input)", "Focus button then input");
+ SimpleTest.finish();
+});
+</script>
+
+
+<button id="button" onfocus="result += '(focus button)'; document.getElementById('input').focus()"
+ onblur="result += '(blur button)'">Focus</button>
+<input id="input" value="Test" onfocus="result += '(focus input)'"
+ onblur="result += '(blur input)'">
+
+</body>
+</html>
diff --git a/dom/html/test/test_form-parsing.html b/dom/html/test/test_form-parsing.html
new file mode 100644
index 0000000000..7c4acca756
--- /dev/null
+++ b/dom/html/test/test_form-parsing.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Form parsing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="content" style="display: none">
+ <div>
+ <form name="test" action="test" id="test">
+ <input type="text" name="text1" id="text1" />
+ <input type="text" name="text2" id="text2" />
+ </div>
+ <input type="text" name="text3" id="text3" />
+ <input type="text" name="text4" id="text4" />
+ <input type="text" name="text5" id="text5" />
+ <input type="text" name="text6" id="text6" />
+ </form>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ var form1 = document.getElementById("test");
+ var elem1 = form1.getElementsByTagName("*");
+ var elem1Length = elem1.length;
+ var form1ElementsLength = form1.elements.length;
+
+ is(form1ElementsLength, 6, "form.elements must include mis-nested elements");
+ is(elem1Length, 2, "form must not include mis-nested elements");
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_formData.html b/dom/html/test/test_formData.html
new file mode 100644
index 0000000000..4518f37cf5
--- /dev/null
+++ b/dom/html/test/test_formData.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=690659
+-->
+<head>
+ <title>Test for Bug 690659 and 739173</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=690659">Mozilla Bug 690659 & 739173</a>
+<script type="text/javascript" src="./formData_test.js"></script>
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function runMainThreadAndWorker() {
+ var mt = new Promise(function(resolve) {
+ runTest(resolve);
+ });
+
+ var worker;
+ var w = new Promise(function(resolve) {
+ worker = new Worker("formData_worker.js");
+ worker.onmessage = function(event) {
+ if (event.data.type == 'finish') {
+ resolve();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ } else if (event.data.type == 'todo') {
+ todo(event.data.status, event.data.msg);
+ }
+ }
+
+ worker.onerror = function(event) {
+ ok(false, "Worker had an error: " + event.message + " at " + event.lineno);
+ resolve();
+ };
+
+ worker.postMessage(true);
+ });
+
+ return Promise.all([mt, w]);
+}
+
+runMainThreadAndWorker().then(SimpleTest.finish);
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_formSubmission.html b/dom/html/test/test_formSubmission.html
new file mode 100644
index 0000000000..952f65e6dd
--- /dev/null
+++ b/dom/html/test/test_formSubmission.html
@@ -0,0 +1,910 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=523771
+-->
+<head>
+ <title>Test for Bug 523771</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
+</head>
+<body onload="bodyLoaded()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=523771">Mozilla Bug 523771</a>
+<p id="display"></p>
+<iframe name="target_iframe" id="target_iframe"></iframe>
+<form action="form_submit_server.sjs" target="target_iframe" id="form"
+ method="POST" enctype="multipart/form-data">
+ <table>
+ <tr>
+ <td>Control type</td>
+ <td>Name and value</td>
+ <td>Name, empty value</td>
+ <td>Name, no value</td>
+ <td>Empty name, with value</td>
+ <td>No name, with value</td>
+ <td>No name or value</td>
+ <td>Strange name/value</td>
+ </tr>
+ <tr>
+ <td>Default input</td>
+ <td><input name="n1_1" value="v1_1"></td>
+ <td><input name="n1_2" value=""></td>
+ <td><input name="n1_3"></td>
+ <td><input name="" value="v1_4"></td>
+ <td><input value="v1_5"></td>
+ <td><input></td>
+ <td><input name="n1_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v1_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Text input</td>
+ <td><input type=text name="n2_1" value="v2_1"></td>
+ <td><input type=text name="n2_2" value=""></td>
+ <td><input type=text name="n2_3"></td>
+ <td><input type=text name="" value="v2_4"></td>
+ <td><input type=text value="v2_5"></td>
+ <td><input type=text></td>
+ <td><input type=text name="n2_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v2_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Checkbox unchecked</td>
+ <td><input type=checkbox name="n3_1" value="v3_1"></td>
+ <td><input type=checkbox name="n3_2" value=""></td>
+ <td><input type=checkbox name="n3_3"></td>
+ <td><input type=checkbox name="" value="v3_4"></td>
+ <td><input type=checkbox value="v3_5"></td>
+ <td><input type=checkbox></td>
+ <td><input type=checkbox name="n3_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v3_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Checkbox checked</td>
+ <td><input checked type=checkbox name="n4_1" value="v4_1"></td>
+ <td><input checked type=checkbox name="n4_2" value=""></td>
+ <td><input checked type=checkbox name="n4_3"></td>
+ <td><input checked type=checkbox name="" value="v4_4"></td>
+ <td><input checked type=checkbox value="v4_5"></td>
+ <td><input checked type=checkbox></td>
+ <td><input checked type=checkbox
+ name="n4_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v4_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Radio unchecked</td>
+ <td><input type=radio name="n5_1" value="v5_1"></td>
+ <td><input type=radio name="n5_2" value=""></td>
+ <td><input type=radio name="n5_3"></td>
+ <td><input type=radio name="" value="v5_4"></td>
+ <td><input type=radio value="v5_5"></td>
+ <td><input type=radio></td>
+ <td><input type=radio name="n5_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v5_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Radio checked</td>
+ <td><input checked type=radio name="n6_1" value="v6_1"></td>
+ <td><input checked type=radio name="n6_2" value=""></td>
+ <td><input checked type=radio name="n6_3"></td>
+ <td><input checked type=radio name="" value="v6_4"></td>
+ <td><input checked type=radio value="v6_5"></td>
+ <td><input checked type=radio></td>
+ <td><input checked type=radio
+ name="n6_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v6_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Hidden input</td>
+ <td><input type=hidden name="n7_1" value="v7_1"></td>
+ <td><input type=hidden name="n7_2" value=""></td>
+ <td><input type=hidden name="n7_3"></td>
+ <td><input type=hidden nane="" value="v7_4"></td>
+ <td><input type=hidden value="v7_5"></td>
+ <td><input type=hidden></td>
+ <td><input type=hidden name="n7_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v7_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Password input</td>
+ <td><input type=password name="n8_1" value="v8_1"></td>
+ <td><input type=password name="n8_2" value=""></td>
+ <td><input type=password name="n8_3"></td>
+ <td><input type=password name="" value="v8_4"></td>
+ <td><input type=password value="v8_5"></td>
+ <td><input type=password></td>
+ <td><input type=password name="n8_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v8_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Submit input</td>
+ <td><input type=submit name="n9_1" value="v9_1"></td>
+ <td><input type=submit name="n9_2" value=""></td>
+ <td><input type=submit name="n9_3"></td>
+ <td><input type=submit name="" value="v9_4"></td>
+ <td><input type=submit value="v9_5"></td>
+ <td><input type=submit></td>
+ <td><input type=submit name="n9_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v9_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Button input</td>
+ <td><input type=button name="n10_1" value="v10_1"></td>
+ <td><input type=button name="n10_2" value=""></td>
+ <td><input type=button name="n10_3"></td>
+ <td><input type=button name="" value="v10_4"></td>
+ <td><input type=button value="v10_5"></td>
+ <td><input type=button></td>
+ <td><input type=button name="n10_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v10_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Image input</td>
+ <td><input type=image src="file_formSubmission_img.jpg" name="n11_1" value="v11_1"></td>
+ <td><input type=image src="file_formSubmission_img.jpg" name="n11_2" value=""></td>
+ <td><input type=image src="file_formSubmission_img.jpg" name="n11_3"></td>
+ <td><input type=image src="file_formSubmission_img.jpg" name="" value="v11_4"></td>
+ <td><input type=image src="file_formSubmission_img.jpg" value="v11_5"></td>
+ <td><input type=image src="file_formSubmission_img.jpg"></td>
+ <td><input type=image src="file_formSubmission_img.jpg"
+ name="n11_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v11_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Reset input</td>
+ <td><input type=reset name="n12_1" value="v12_1"></td>
+ <td><input type=reset name="n12_2" value=""></td>
+ <td><input type=reset name="n12_3"></td>
+ <td><input type=reset name="" value="v12_4"></td>
+ <td><input type=reset value="v12_5"></td>
+ <td><input type=reset></td>
+ <td><input type=reset name="n12_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v12_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Unknown input</td>
+ <td><input type=foobar name="n13_1" value="v13_1"></td>
+ <td><input type=foobar name="n13_2" value=""></td>
+ <td><input type=foobar name="n13_3"></td>
+ <td><input type=foobar name="" value="v13_4"></td>
+ <td><input type=foobar value="v13_5"></td>
+ <td><input type=foobar></td>
+ <td><input type=foobar name="n13_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v13_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></td>
+ </tr>
+ <tr>
+ <td>Default button</td>
+ <td><button name="n14_1" value="v14_1"></button></td>
+ <td><button name="n14_2" value=""></button></td>
+ <td><button name="n14_3"></button></td>
+ <td><button name="" value="v14_4"></button></td>
+ <td><button value="v14_5"></button></td>
+ <td><button></button></td>
+ <td><button name="n14_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v14_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></button></td>
+ </tr>
+ <tr>
+ <td>Submit button</td>
+ <td><button type=submit name="n15_1" value="v15_1"></button></td>
+ <td><button type=submit name="n15_2" value=""></button></td>
+ <td><button type=submit name="n15_3"></button></td>
+ <td><button type=submit name="" value="v15_4"></button></td>
+ <td><button type=submit value="v15_5"></button></td>
+ <td><button type=submit></button></td>
+ <td><button type=submit name="n15_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v15_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></button></td>
+ </tr>
+ <tr>
+ <td>Button button</td>
+ <td><button type=button name="n16_1" value="v16_1"></button></td>
+ <td><button type=button name="n16_2" value=""></button></td>
+ <td><button type=button name="n16_3"></button></td>
+ <td><button type=button name="" value="v16_4"></button></td>
+ <td><button type=button value="v16_5"></button></td>
+ <td><button type=button></button></td>
+ <td><button type=button name="n16_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v16_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></button></td>
+ </tr>
+ <tr>
+ <td>Reset button</td>
+ <td><button type=reset name="n17_1" value="v17_1"></button></td>
+ <td><button type=reset name="n17_2" value=""></button></td>
+ <td><button type=reset name="n17_3"></button></td>
+ <td><button type=reset name="" value="v17_4"></button></td>
+ <td><button type=reset value="v17_5"></button></td>
+ <td><button type=reset></button></td>
+ <td><button type=reset name="n17_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v17_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></button></td>
+ </tr>
+ <tr>
+ <td>Unknown button</td>
+ <td><button type=foobar name="n18_1" value="v18_1"></button></td>
+ <td><button type=foobar name="n18_2" value=""></button></td>
+ <td><button type=foobar name="n18_3"></button></td>
+ <td><button type=foobar name="" value="v18_4"></button></td>
+ <td><button type=foobar value="v18_5"></button></td>
+ <td><button type=foobar ></button></td>
+ <td><button type=foobar name="n18_7_&#13;_&#10;_&#13;&#10;_ _&quot;"
+ value="v18_7_&#13;_&#10;_&#13;&#10;_ _&quot;"></button></td>
+ </tr>
+ <tr>
+ <td>&lt;input type='url'&gt;</td>
+ <td><input type=url name="n19_1" value="http://v19_1.org"></td>
+ <td><input type=url name="n19_2" value=""></td>
+ <td><input type=url name="n19_3"></td>
+ <td><input type=url name="" value="http://v19_4.org"></td>
+ <td><input type=url value="http://v19_5.org"></td>
+ <td><input type=url ></td>
+ <td><input type=url name="n19_7_&#13;_&#10;_&#13;&#10;__&quot;"
+ value="http://v19_7_&#13;_&#10;_&#13;&#10;__&quot;">
+ <!-- Put UTF-8 value in the "strange" column. -->
+ <input type=url name="n19_8" value="http://m&#xf3;zill&auml;.&#xf3;rg"></td>
+ </tr>
+ <tr>
+ <td>&lt;input type='email'&gt;</td>
+ <td><input type=email name="n20_1" value="v20_1@bar"></td>
+ <td><input type=email name="n20_2" value=""></td>
+ <td><input type=email name="n20_3"></td>
+ <td><input type=email name="" value="v20_4@bar"></td>
+ <td><input type=email value="v20_5@bar"></td>
+ <td><input type=email ></td>
+ <td><input type=email name="n20_7_&#13;_&#10;_&#13;&#10;__&quot;"
+ value="v20_7_&#13;_&#10;_&#13;&#10;__&quot;@bar">
+ <!-- Put UTF-8 value is the "strange" column. -->
+ <input type=email name="n20_8" value="foo@mózillä.órg"></td>
+ </tr>
+ </table>
+
+ <p>
+ File input:
+ <input type=file name="file_1" class="setfile">
+ <input type=file name="file_2">
+ <input type=file name="" class="setfile">
+ <input type=file name="">
+ <input type=file class="setfile">
+ <input type=file>
+ </p>
+ <p>
+ Multifile input:
+ <input multiple type=file name="file_3" class="setfile">
+ <input multiple type=file name="file_4" class="setfile multi">
+ <input multiple type=file name="file_5">
+ <input multiple type=file name="" class="setfile">
+ <input multiple type=file name="" class="setfile multi">
+ <input multiple type=file name="">
+ <input multiple type=file class="setfile">
+ <input multiple type=file class="setfile multi">
+ <input multiple type=file>
+ </p>
+
+ <p>
+ Textarea:
+ <textarea name="t1">t_1_v</textarea>
+ <textarea name="t2"></textarea>
+ <textarea name="">t_3_v</textarea>
+ <textarea>t_4_v</textarea>
+ <textarea></textarea>
+ <textarea name="t6">
+t_6_v</textarea>
+ <textarea name="t7">t_7_v
+</textarea>
+ <textarea name="t8">
+
+ t_8_v&#0032;
+</textarea>
+ <textarea name="t9_&#13;_&#10;_&#13;&#10;_ _&quot;">t_9_&#13;_&#10;_&#13;&#10;_ _&quot;_v</textarea>
+ <textarea name="t10" value="t_10_bogus">t_10_v</textarea>
+ </p>
+
+ <p>
+ Select one:
+
+ <select name="sel_1"></select>
+ <select name="sel_1b"><option></option></select>
+ <select name="sel_1c"><option selected></option></select>
+
+ <select name="sel_2"><option value="sel_2_v"></option></select>
+ <select name="sel_3"><option selected value="sel_3_v"></option></select>
+
+ <select name="sel_4"><option value="sel_4_v1"></option><option value="sel_4_v2"></option></select>
+ <select name="sel_5"><option selected value="sel_5_v1"></option><option value="sel_5_v2"></option></select>
+ <select name="sel_6"><option value="sel_6_v1"></option><option selected value="sel_6_v2"></option></select>
+
+ <select name="sel_7"><option>sel_7_v1</option><option>sel_7_v2</option></select>
+ <select name="sel_8"><option selected>sel_8_v1</option><option>sel_8_v2</option></select>
+ <select name="sel_9"><option>sel_9_v1</option><option selected>sel_9_v2</option></select>
+
+ <select name="sel_10"><option value="sel_10_v1">sel_10_v1_text</option><option value="sel_10_v2">sel_10_v2_text</option></select>
+ <select name="sel_11"><option selected value="sel_11_v1">sel_11_v1_text</option><option value="sel_11_v2">sel_11_v2_text</option></select>
+ <select name="sel_12"><option value="sel_12_v1">sel_12_v1_text</option><option selected value="sel_12_v2">sel_12_v2_text</option></select>
+
+ <select name="sel_13"><option disabled>sel_13_v1</option><option>sel_13_v2</option></select>
+ <select name="sel_14"><option disabled selected>sel_14_v1</option><option>sel_14_v2</option></select>
+ <select name="sel_15"><option disabled>sel_15_v1</option><option selected>sel_15_v2</option></select>
+
+ <select name="sel_16"><option>sel_16_v1</option><option disabled>sel_16_v2</option></select>
+ <select name="sel_17"><option selected>sel_17_v1</option><option disabled>sel_17_v2</option></select>
+ <select name="sel_18"><option>sel_18_v1</option><option disabled selected>sel_18_v2</option></select>
+
+ <select name=""><option selected value="sel_13_v1"></option><option value="sel_13_v2"></option></select>
+ <select name=""><option value="sel_14_v1"></option><option selected value="sel_14_v2"></option></select>
+ <select name=""><option selected>sel_15_v1</option><option>sel_15_v2</option></select>
+ <select name=""><option>sel_16_v1</option><option selected>sel_16_v2</option></select>
+
+ <select><option selected value="sel_17_v1"></option><option value="sel_17_v2"></option></select>
+ <select><option value="sel_18_v1"></option><option selected value="sel_18_v2"></option></select>
+ <select><option selected>sel_19_v1</option><option>sel_19_v2</option></select>
+ <select><option>sel_20_v1</option><option selected>sel_20_v2</option></select>
+ </p>
+
+ <p>
+ Select multiple:
+
+ <select multiple name="msel_1"></select>
+ <select multiple name="msel_1b"><option></option></select>
+ <select multiple name="msel_1c"><option selected></option></select>
+
+ <select multiple name="msel_2"><option value="msel_2_v"></option></select>
+ <select multiple name="msel_3"><option selected value="msel_3_v"></option></select>
+
+ <select multiple name="msel_4"><option value="msel_4_v1"></option><option value="msel_4_v2"></option></select>
+ <select multiple name="msel_5"><option selected value="msel_5_v1"></option><option value="msel_5_v2"></option></select>
+ <select multiple name="msel_6"><option value="msel_6_v1"></option><option selected value="msel_6_v2"></option></select>
+ <select multiple name="msel_7"><option selected value="msel_7_v1"></option><option selected value="msel_7_v2"></option></select>
+
+ <select multiple name="msel_8"><option>msel_8_v1</option><option>msel_8_v2</option></select>
+ <select multiple name="msel_9"><option selected>msel_9_v1</option><option>msel_9_v2</option></select>
+ <select multiple name="msel_10"><option>msel_10_v1</option><option selected>msel_10_v2</option></select>
+ <select multiple name="msel_11"><option selected>msel_11_v1</option><option selected>msel_11_v2</option></select>
+
+ <select multiple name="msel_12"><option value="msel_12_v1">msel_12_v1_text</option><option value="msel_12_v2">msel_12_v2_text</option></select>
+ <select multiple name="msel_13"><option selected value="msel_13_v1">msel_13_v1_text</option><option value="msel_13_v2">msel_13_v2_text</option></select>
+ <select multiple name="msel_14"><option value="msel_14_v1">msel_14_v1_text</option><option selected value="msel_14_v2">msel_14_v2_text</option></select>
+ <select multiple name="msel_15"><option selected value="msel_15_v1">msel_15_v1_text</option><option selected value="msel_15_v2">msel_15_v2_text</option></select>
+
+ <select multiple name="msel_16"><option>msel_16_v1</option><option>msel_16_v2</option><option>msel_16_v3</option></select>
+ <select multiple name="msel_17"><option selected>msel_17_v1</option><option>msel_17_v2</option><option>msel_17_v3</option></select>
+ <select multiple name="msel_18"><option>msel_18_v1</option><option selected>msel_18_v2</option><option>msel_18_v3</option></select>
+ <select multiple name="msel_19"><option selected>msel_19_v1</option><option selected>msel_19_v2</option><option>msel_19_v3</option></select>
+ <select multiple name="msel_20"><option>msel_20_v1</option><option>msel_20_v2</option><option selected>msel_20_v3</option></select>
+ <select multiple name="msel_21"><option selected>msel_21_v1</option><option>msel_21_v2</option><option selected>msel_21_v3</option></select>
+ <select multiple name="msel_22"><option>msel_22_v1</option><option selected>msel_22_v2</option><option selected>msel_22_v3</option></select>
+ <select multiple name="msel_23"><option selected>msel_23_v1</option><option selected>msel_23_v2</option><option selected>msel_23_v3</option></select>
+
+ <select multiple name="msel_24"><option disabled>msel_24_v1</option><option>msel_24_v2</option></select>
+ <select multiple name="msel_25"><option disabled selected>msel_25_v1</option><option>msel_25_v2</option></select>
+ <select multiple name="msel_26"><option disabled>msel_26_v1</option><option selected>msel_26_v2</option></select>
+ <select multiple name="msel_27"><option disabled selected>msel_27_v1</option><option selected>msel_27_v2</option></select>
+
+ <select multiple name="msel_28"><option>msel_28_v1</option><option disabled>msel_28_v2</option></select>
+ <select multiple name="msel_29"><option selected>msel_29_v1</option><option disabled>msel_29_v2</option></select>
+ <select multiple name="msel_30"><option>msel_30_v1</option><option disabled selected>msel_30_v2</option></select>
+ <select multiple name="msel_31"><option selected>msel_31_v1</option><option disabled selected>msel_31_v2</option></select>
+
+ <select multiple name="msel_32"><option disabled selected>msel_32_v1</option><option disabled selected>msel_32_v2</option></select>
+
+ <select multiple name=""><option>msel_33_v1</option><option>msel_33_v2</option></select>
+ <select multiple name=""><option selected>msel_34_v1</option><option>msel_34_v2</option></select>
+ <select multiple name=""><option>msel_35_v1</option><option selected>msel_35_v2</option></select>
+ <select multiple name=""><option selected>msel_36_v1</option><option selected>msel_36_v2</option></select>
+
+ <select multiple><option>msel_37_v1</option><option>msel_37_v2</option></select>
+ <select multiple><option selected>msel_38_v1</option><option>msel_38_v2</option></select>
+ <select multiple><option>msel_39_v1</option><option selected>msel_39_v2</option></select>
+ <select multiple><option selected>msel_40_v1</option><option selected>msel_40_v2</option></select>
+ </p>
+</form>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const placeholder_myFile1 = {};
+const placeholder_myFile2 = {};
+const placeholder_emptyFile = {};
+
+var myFile1, myFile2, emptyFile;
+let openerURL, opener;
+var gen;
+
+function bodyLoaded() {
+ openerURL = SimpleTest.getTestFileURL("formSubmission_chrome.js");
+ opener = SpecialPowers.loadChromeScript(openerURL);
+
+ let xhr = new XMLHttpRequest;
+ xhr.open("GET", "/dynamic/getMyDirectory.sjs", false);
+ xhr.send();
+ let basePath = xhr.responseText;
+
+ opener.addMessageListener("files.opened", onFilesOpened);
+ opener.sendAsyncMessage("files.open", [
+ basePath + "file_formSubmission_text.txt",
+ basePath + "file_formSubmission_img.jpg",
+ ]);
+
+ /*
+ * The below test function uses callbacks that invoke gen.next() rather than
+ * creating and resolving Promises. I'm trying to minimize churn since these
+ * changes want to be uplifted. Some kind soul might want to clean this all up
+ * at some point.
+ */
+
+ $("target_iframe").onload = function() { gen.next(); };
+}
+
+
+function onFilesOpened(files) {
+ let [textFile, imageFile] = files;
+ opener.destroy();
+
+ let singleFile = textFile;
+ let multiFile = [textFile, imageFile];
+
+ var addList = document.getElementsByClassName("setfile");
+ let i = 0;
+ var input;
+ while ((input = addList[i++])) {
+ if (input.classList.contains("multi")) {
+ SpecialPowers.wrap(input).mozSetFileArray(multiFile);
+ } else {
+ SpecialPowers.wrap(input).mozSetFileArray([singleFile]);
+ }
+ }
+
+ input = document.createElement("input");
+ input.type = "file";
+ input.multiple = true;
+ SpecialPowers.wrap(input).mozSetFileArray(multiFile);
+ myFile1 = input.files[0];
+ myFile2 = input.files[1];
+ is(myFile1.size, 20, "File1 size");
+ is(myFile2.size, 2711, "File2 size");
+ emptyFile = { name: "", type: "application/octet-stream" };
+
+ // Now, actually run the tests; see below.
+ runAllTestVariants();
+};
+
+var expectedSub = [
+ // Default input
+ { name: "n1_1", value: "v1_1" },
+ { name: "n1_2", value: "" },
+ { name: "n1_3", value: "" },
+ { name: "n1_7_\r\n_\r\n_\r\n_ _\"", value: "v1_7____ _\"" },
+ // Text input
+ { name: "n2_1", value: "v2_1" },
+ { name: "n2_2", value: "" },
+ { name: "n2_3", value: "" },
+ { name: "n2_7_\r\n_\r\n_\r\n_ _\"", value: "v2_7____ _\"" },
+ // Checkbox unchecked
+ // Checkbox checked
+ { name: "n4_1", value: "v4_1" },
+ { name: "n4_2", value: "" },
+ { name: "n4_3", value: "on" },
+ { name: "n4_7_\r\n_\r\n_\r\n_ _\"", value: "v4_7_\r\n_\r\n_\r\n_ _\"" },
+ // Radio unchecked
+ // Radio checked
+ { name: "n6_1", value: "v6_1" },
+ { name: "n6_2", value: "" },
+ { name: "n6_3", value: "on" },
+ { name: "n6_7_\r\n_\r\n_\r\n_ _\"", value: "v6_7_\r\n_\r\n_\r\n_ _\"" },
+ // Hidden input
+ { name: "n7_1", value: "v7_1" },
+ { name: "n7_2", value: "" },
+ { name: "n7_3", value: "" },
+ { name: "n7_7_\r\n_\r\n_\r\n_ _\"", value: "v7_7_\r\n_\r\n_\r\n_ _\"" },
+ // Password input
+ { name: "n8_1", value: "v8_1" },
+ { name: "n8_2", value: "" },
+ { name: "n8_3", value: "" },
+ { name: "n8_7_\r\n_\r\n_\r\n_ _\"", value: "v8_7____ _\"" },
+ // Submit input
+ // Button input
+ // Image input
+ // Reset input
+ // Unknown input
+ { name: "n13_1", value: "v13_1" },
+ { name: "n13_2", value: "" },
+ { name: "n13_3", value: "" },
+ { name: "n13_7_\r\n_\r\n_\r\n_ _\"", value: "v13_7____ _\"" },
+ // <input type='url'>
+ { name: "n19_1", value: "http://v19_1.org" },
+ { name: "n19_2", value: "" },
+ { name: "n19_3", value: "" },
+ { name: "n19_7_\r\n_\r\n_\r\n__\"", value: "http://v19_7_____\"" },
+ { name: "n19_8", value: "http://m\xf3zill\xe4.\xf3rg" },
+ // <input type='email'>
+ { name: "n20_1", value: "v20_1@bar" },
+ { name: "n20_2", value: "" },
+ { name: "n20_3", value: "" },
+ { name: "n20_7_\r\n_\r\n_\r\n__\"", value: "v20_7_____\"@bar" },
+ { name: "n20_8", value: "foo@mózillä.órg" },
+ // Default button
+ // Submit button
+ // Button button
+ // Reset button
+ // Unknown button
+ // File
+ { name: "file_1", value: placeholder_myFile1 },
+ { name: "file_2", value: placeholder_emptyFile },
+ // Multiple file
+ { name: "file_3", value: placeholder_myFile1 },
+ { name: "file_4", value: placeholder_myFile1 },
+ { name: "file_4", value: placeholder_myFile2 },
+ { name: "file_5", value: placeholder_emptyFile },
+ // Textarea
+ { name: "t1", value: "t_1_v" },
+ { name: "t2", value: "" },
+ { name: "t6", value: "t_6_v" },
+ { name: "t7", value: "t_7_v\r\n" },
+ { name: "t8", value: "\r\n t_8_v \r\n" },
+ { name: "t9_\r\n_\r\n_\r\n_ _\"", value: "t_9_\r\n_\r\n_\r\n_ _\"_v" },
+ { name: "t10", value: "t_10_v" },
+
+ // Select one
+ { name: "sel_1b", value: "" },
+ { name: "sel_1c", value: "" },
+ { name: "sel_2", value: "sel_2_v" },
+ { name: "sel_3", value: "sel_3_v" },
+ { name: "sel_4", value: "sel_4_v1" },
+ { name: "sel_5", value: "sel_5_v1" },
+ { name: "sel_6", value: "sel_6_v2" },
+ { name: "sel_7", value: "sel_7_v1" },
+ { name: "sel_8", value: "sel_8_v1" },
+ { name: "sel_9", value: "sel_9_v2" },
+ { name: "sel_10", value: "sel_10_v1" },
+ { name: "sel_11", value: "sel_11_v1" },
+ { name: "sel_12", value: "sel_12_v2" },
+ { name: "sel_13", value: "sel_13_v2" },
+ { name: "sel_15", value: "sel_15_v2" },
+ { name: "sel_16", value: "sel_16_v1" },
+ { name: "sel_17", value: "sel_17_v1" },
+ // Select three
+ { name: "msel_1c", value: "" },
+ { name: "msel_3", value: "msel_3_v" },
+ { name: "msel_5", value: "msel_5_v1" },
+ { name: "msel_6", value: "msel_6_v2" },
+ { name: "msel_7", value: "msel_7_v1" },
+ { name: "msel_7", value: "msel_7_v2" },
+ { name: "msel_9", value: "msel_9_v1" },
+ { name: "msel_10", value: "msel_10_v2" },
+ { name: "msel_11", value: "msel_11_v1" },
+ { name: "msel_11", value: "msel_11_v2" },
+ { name: "msel_13", value: "msel_13_v1" },
+ { name: "msel_14", value: "msel_14_v2" },
+ { name: "msel_15", value: "msel_15_v1" },
+ { name: "msel_15", value: "msel_15_v2" },
+ { name: "msel_17", value: "msel_17_v1" },
+ { name: "msel_18", value: "msel_18_v2" },
+ { name: "msel_19", value: "msel_19_v1" },
+ { name: "msel_19", value: "msel_19_v2" },
+ { name: "msel_20", value: "msel_20_v3" },
+ { name: "msel_21", value: "msel_21_v1" },
+ { name: "msel_21", value: "msel_21_v3" },
+ { name: "msel_22", value: "msel_22_v2" },
+ { name: "msel_22", value: "msel_22_v3" },
+ { name: "msel_23", value: "msel_23_v1" },
+ { name: "msel_23", value: "msel_23_v2" },
+ { name: "msel_23", value: "msel_23_v3" },
+ { name: "msel_26", value: "msel_26_v2" },
+ { name: "msel_27", value: "msel_27_v2" },
+ { name: "msel_29", value: "msel_29_v1" },
+ { name: "msel_31", value: "msel_31_v1" },
+];
+
+var expectedAugment = [
+ { name: "aName", value: "aValue" },
+ //{ name: "aNameBool", value: "false" },
+ { name: "aNameNum", value: "9.2" },
+ { name: "aNameFile1", value: placeholder_myFile1 },
+ { name: "aNameFile2", value: placeholder_myFile2 },
+ //{ name: "aNameObj", value: "[object XMLHttpRequest]" },
+ //{ name: "aNameNull", value: "null" },
+ //{ name: "aNameUndef", value: "undefined" },
+];
+
+function checkMPSubmission(sub, expected, test) {
+ function getPropCount(o) {
+ var x, l = 0;
+ for (x in o) ++l;
+ return l;
+ }
+ function mpquote_name(s) {
+ return s.replace(/\r?\n|\r/g, "%0D%0A")
+ .replace(/\"/g, "%22");
+ }
+ function mpquote_filename(s) {
+ return s.replace(/\r/g, "%0D")
+ .replace(/\n/g, "%0A")
+ .replace(/\"/g, "%22");
+ }
+
+ is(sub.length, expected.length,
+ "Correct number of multipart items in " + test);
+
+ if (sub.length != expected.length) {
+ alert(JSON.stringify(sub));
+ }
+
+ var i;
+ for (i = 0; i < expected.length; ++i) {
+ if (!("fileName" in expected[i])) {
+ is(sub[i].headers["Content-Disposition"],
+ "form-data; name=\"" + mpquote_name(expected[i].name) + "\"",
+ "Correct name in " + test);
+ is (getPropCount(sub[i].headers), 1,
+ "Wrong number of headers in " + test);
+ is(sub[i].body,
+ expected[i].value.replace(/\r\n|\r|\n/, "\r\n"),
+ "Correct value in " + test);
+ }
+ else {
+ is(sub[i].headers["Content-Disposition"],
+ "form-data; name=\"" + mpquote_name(expected[i].name) + "\"; filename=\"" +
+ mpquote_filename(expected[i].fileName) + "\"",
+ "Correct name in " + test);
+ is(sub[i].headers["Content-Type"],
+ expected[i].contentType,
+ "Correct content type in " + test);
+ is (getPropCount(sub[i].headers), 2,
+ "Wrong number of headers in " + test);
+ is(sub[i].body,
+ expected[i].value,
+ "Correct value in " + test);
+ }
+ }
+}
+
+function utf8encode(s) {
+ return unescape(encodeURIComponent(s));
+}
+
+function checkURLSubmission(sub, expected) {
+ function urlEscape(s) {
+ return escape(utf8encode(s)).replace(/%20/g, "+")
+ .replace(/\//g, "%2F")
+ .replace(/@/g, "%40");
+ }
+
+ subItems = sub.split("&");
+ is(subItems.length, expected.length,
+ "Correct number of url items");
+ var i;
+ for (i = 0; i < expected.length; ++i) {
+ let expect = urlEscape(expected[i].name) + "=" +
+ urlEscape(("fileName" in expected[i]) ? expected[i].fileName : expected[i].value);
+ is (subItems[i], expect, "expected URL part");
+ }
+}
+
+function checkPlainSubmission(sub, expected) {
+
+ is(sub,
+ expected.map(function(v) {
+ return v.name + "=" +
+ (("fileName" in v) ? v.fileName : v.value) +
+ "\r\n";
+ }).join(""),
+ "Correct submission");
+}
+
+function setDisabled(list, state) {
+ Array.prototype.forEach.call(list, function(e) {
+ e.disabled = state;
+ });
+}
+
+// Run the suite of tests for this variant, returning a Promise that will be
+// resolved when the batch completes. Then and only then runTestVariant may
+// be invoked to run a different variation.
+function runTestVariant(variantLabel) {
+ info("starting test variant: " + variantLabel);
+ return new Promise((resolve) => {
+ // Instantiate the generator.
+ gen = runTestVariantUsingWeirdGenDriver(resolve);
+ // Run the generator to the first yield, at which point it is self-driving.
+ gen.next();
+ });
+}
+function* runTestVariantUsingWeirdGenDriver(finishedVariant) {
+ // Set up the expectedSub array
+ fileReader1 = new FileReader;
+ fileReader1.readAsBinaryString(myFile1);
+ fileReader2 = new FileReader;
+ fileReader2.readAsBinaryString(myFile2);
+ fileReader1.onload = fileReader2.onload = function() { gen.next(); };
+ yield undefined; // Wait for both FileReaders. We don't care which order they finish.
+ yield undefined;
+ function fileFixup(o) {
+ if (o.value === placeholder_myFile1) {
+ o.value = fileReader1.result;
+ o.fileName = myFile1.name;
+ o.contentType = myFile1.type;
+ }
+ else if (o.value === placeholder_myFile2) {
+ o.value = fileReader2.result;
+ o.fileName = myFile2.name;
+ o.contentType = myFile2.type;
+ }
+ else if (o.value === placeholder_emptyFile) {
+ o.value = "";
+ o.fileName = emptyFile.name;
+ o.contentType = emptyFile.type;
+ }
+ };
+ expectedSub.forEach(fileFixup);
+ expectedAugment.forEach(fileFixup);
+
+ var form = $("form");
+
+ // multipart/form-data
+ var iframe = $("target_iframe");
+
+ // Make normal submission
+ form.action = "form_submit_server.sjs";
+ form.method = "POST";
+ form.enctype = "multipart/form-data";
+ form.submit();
+ yield undefined; // Wait for iframe to load as a result of the submission
+ var submission = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ checkMPSubmission(submission, expectedSub, "normal submission");
+
+ // Disabled controls
+ setDisabled(document.querySelectorAll("input, select, textarea"), true);
+ form.submit();
+ yield undefined;
+ submission = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ checkMPSubmission(submission, [], "disabled controls");
+
+ // Reenabled controls
+ setDisabled(document.querySelectorAll("input, select, textarea"), false);
+ form.submit();
+ yield undefined;
+ submission = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ checkMPSubmission(submission, expectedSub, "reenabled controls");
+
+ // text/plain
+ form.action = "form_submit_server.sjs?plain";
+ form.enctype = "text/plain";
+ form.submit();
+ yield undefined;
+ submission = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ checkPlainSubmission(submission, expectedSub);
+
+ // application/x-www-form-urlencoded
+ form.action = "form_submit_server.sjs?url";
+ form.enctype = "application/x-www-form-urlencoded";
+ form.submit();
+ yield undefined;
+ submission = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ checkURLSubmission(submission, expectedSub);
+
+ // application/x-www-form-urlencoded
+ form.action = "form_submit_server.sjs?xxyy";
+ form.method = "GET";
+ form.enctype = "";
+ form.submit();
+ yield undefined;
+ submission = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ checkURLSubmission(submission, expectedSub);
+
+ // application/x-www-form-urlencoded
+ form.action = "form_submit_server.sjs";
+ form.method = "";
+ form.enctype = "";
+ form.submit();
+ yield undefined;
+ submission = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ checkURLSubmission(submission, expectedSub);
+
+ // Send form using XHR and FormData
+ xhr = new XMLHttpRequest();
+ xhr.onload = function() { gen.next(); };
+ xhr.open("POST", "form_submit_server.sjs");
+ xhr.send(new FormData(form));
+ yield undefined; // Wait for XHR load
+ checkMPSubmission(JSON.parse(xhr.responseText), expectedSub, "send form using XHR and FormData");
+
+ // Send disabled form using XHR and FormData
+ setDisabled(document.querySelectorAll("input, select, textarea"), true);
+ xhr.open("POST", "form_submit_server.sjs");
+ xhr.send(new FormData(form));
+ yield undefined;
+ checkMPSubmission(JSON.parse(xhr.responseText), [], "send disabled form using XHR and FormData");
+ setDisabled(document.querySelectorAll("input, select, textarea"), false);
+
+ // Send FormData
+ function addToFormData(fd) {
+ fd.append("aName", "aValue");
+ fd.append("aNameNum", 9.2);
+ fd.append("aNameFile1", myFile1);
+ fd.append("aNameFile2", myFile2);
+ }
+ var fd = new FormData();
+ addToFormData(fd);
+ xhr.open("POST", "form_submit_server.sjs");
+ xhr.send(fd);
+ yield undefined;
+ checkMPSubmission(JSON.parse(xhr.responseText), expectedAugment, "send FormData");
+
+ // Augment <form> using FormData
+ fd = new FormData(form);
+ addToFormData(fd);
+ xhr.open("POST", "form_submit_server.sjs");
+ xhr.send(fd);
+ yield undefined;
+ checkMPSubmission(JSON.parse(xhr.responseText),
+ expectedSub.concat(expectedAugment), "send augmented FormData");
+
+ finishedVariant();
+}
+
+/**
+ * Install our service-worker (parameterized by appending "?MODE"), which will
+ * invoke skipWaiting() and clients.claim() to begin controlling us ASAP. We
+ * wait on the controllerchange event
+ */
+async function installAndBeControlledByServiceWorker(mode) {
+ const scriptURL = "sw_formSubmission.js?" + mode;
+ const controllerChanged = new Promise((resolve) => {
+ navigator.serviceWorker.addEventListener(
+ "controllerchange", () => { resolve(); }, { once: true });
+ });
+
+ info("installing ServiceWorker: " + scriptURL);
+ const swr = await navigator.serviceWorker.register(scriptURL,
+ { scope: "./" });
+ await controllerChanged;
+ ok(navigator.serviceWorker.controller.scriptURL.endsWith(scriptURL),
+ "controlled by the SW we expected");
+ info("became controlled by ServiceWorker.");
+
+ return swr;
+}
+
+async function runAllTestVariants() {
+ // Run the test as it has historically been run, with no ServiceWorkers
+ // anywhere!
+ await runTestVariant("no ServiceWorker");
+
+ // Uncomment the below if something in the test seems broken and you're not
+ // sure whether it's a side-effect of the multiple passes or not.
+ //await runTestVariant("no ServiceWorker second paranoia time");
+
+ // Ensure ServiceWorkers are enabled and that testing mode (which disables
+ // security checks) is on too.
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]});
+
+ // Now run the test with a ServiceWorker that covers the scope but has no
+ // fetch handler, so the optimization case will not actually dispatch a
+ // "fetch" event, but some stuff will happen that can change things enough
+ // to break them like in https://bugzilla.mozilla.org/show_bug.cgi?id=1383518.
+ await installAndBeControlledByServiceWorker("no-fetch");
+ await runTestVariant("ServiceWorker that does not listen for fetch events");
+
+ // Now the ServiceWorker does have a "fetch" event listener, but it will reset
+ // interception every time. This is similar to the prior case but different
+ // enough that it could break things in a different exciting way.
+ await installAndBeControlledByServiceWorker("reset-fetch");
+ await runTestVariant("ServiceWorker that resets all fetches");
+
+ // Now the ServiceWorker resolves the fetch event with `fetch(event.request)`
+ // which makes little sense but is a thing that can happen.
+ const swr = await installAndBeControlledByServiceWorker("proxy-fetch");
+ await runTestVariant("ServiceWorker that proxies all fetches");
+
+ // cleanup.
+ info("unregistering ServiceWorker");
+ await swr.unregister();
+ info("ServiceWorker uninstalled");
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_formSubmission2.html b/dom/html/test/test_formSubmission2.html
new file mode 100644
index 0000000000..fc6b60cdcf
--- /dev/null
+++ b/dom/html/test/test_formSubmission2.html
@@ -0,0 +1,220 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=523771
+-->
+<head>
+ <title>Test for Bug 523771</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=523771">Mozilla Bug 523771</a>
+<p id="display"></p>
+<iframe name="target_iframe" id="target_iframe"></iframe>
+<form action="form_submit_server.sjs" target="target_iframe" id="form"
+ method="POST" enctype="multipart/form-data">
+ <table>
+ <tr>
+ <td>Control type</td>
+ <td>Name and value</td>
+ <td>Name, empty value</td>
+ <td>Name, no value</td>
+ <td>Empty name, with value</td>
+ <td>No name, with value</td>
+ <td>No name or value</td>
+ </tr>
+ <tr>
+ <td>Submit input</td>
+ <td><input type=submit name="n1_1" value="v1_1"></td>
+ <td><input type=submit name="n1_2" value=""></td>
+ <td><input type=submit name="n1_3"></td>
+ <td><input type=submit name="" value="v1_4"></td>
+ <td><input type=submit value="v1_5"></td>
+ <td><input type=submit></td>
+ </tr>
+ <tr>
+ <td>Image input</td>
+ <td><input type=image src="file_formSubmission_img.jpg" name="n2_1" value="v2_1"></td>
+ <td><input type=image src="file_formSubmission_img.jpg" name="n2_2" value=""></td>
+ <td><input type=image src="file_formSubmission_img.jpg" name="n2_3"></td>
+ <td><input type=image src="file_formSubmission_img.jpg" name="" value="v2_4"></td>
+ <td><input type=image src="file_formSubmission_img.jpg" value="v2_5"></td>
+ <td><input type=image src="file_formSubmission_img.jpg"></td>
+ </tr>
+ <tr>
+ <td>Submit button</td>
+ <td><button type=submit name="n3_1" value="v3_1"></button></td>
+ <td><button type=submit name="n3_2" value=""></button></td>
+ <td><button type=submit name="n3_3"></button></td>
+ <td><button type=submit name="" value="v3_4"></button></td>
+ <td><button type=submit value="v3_5"></button></td>
+ <td><button type=submit ></button></td>
+ </tr>
+ <tr>
+ <td>Submit button with text</td>
+ <td><button type=submit name="n4_1" value="v4_1">text here</button></td>
+ <td><button type=submit name="n4_2" value="">text here</button></td>
+ <td><button type=submit name="n4_3">text here</button></td>
+ <td><button type=submit name="" value="v4_4">text here</button></td>
+ <td><button type=submit value="v4_5">text here</button></td>
+ <td><button type=submit>text here</button></td>
+ </tr>
+ </table>
+</form>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gen = runTest();
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ gen.next();
+});
+
+var expectedSub = [
+ // Submit input
+ [{ name: "n1_1", value: "v1_1" }],
+ [{ name: "n1_2", value: "" }],
+ [{ name: "n1_3", value: "Submit Query" }],
+ [],
+ [],
+ [],
+ // Image input
+ [{ name: "n2_1.x", value: "10" },
+ { name: "n2_1.y", value: "7" }],
+ [{ name: "n2_2.x", value: "10" },
+ { name: "n2_2.y", value: "7" }],
+ [{ name: "n2_3.x", value: "10" },
+ { name: "n2_3.y", value: "7" }],
+ [{ name: "x", value: "10" },
+ { name: "y", value: "7" }],
+ [{ name: "x", value: "10" },
+ { name: "y", value: "7" }],
+ [{ name: "x", value: "10" },
+ { name: "y", value: "7" }],
+ // Submit button
+ [{ name: "n3_1", value: "v3_1" }],
+ [{ name: "n3_2", value: "" }],
+ [{ name: "n3_3", value: "" }],
+ [],
+ [],
+ [],
+ // Submit button with text
+ [{ name: "n4_1", value: "v4_1" }],
+ [{ name: "n4_2", value: "" }],
+ [{ name: "n4_3", value: "" }],
+ [],
+ [],
+ [],
+];
+
+function checkSubmission(sub, expected) {
+ function getPropCount(o) {
+ var x, l = 0;
+ for (x in o) ++l;
+ return l;
+ }
+
+ is(sub.length, expected.length,
+ "Correct number of items");
+ var i;
+ for (i = 0; i < expected.length; ++i) {
+ if (!("fileName" in expected[i])) {
+ is(sub[i].headers["Content-Disposition"],
+ "form-data; name=\"" + expected[i].name + "\"",
+ "Correct name");
+ is (getPropCount(sub[i].headers), 1,
+ "Wrong number of headers");
+ }
+ else {
+ is(sub[i].headers["Content-Disposition"],
+ "form-data; name=\"" + expected[i].name + "\"; filename=\"" +
+ expected[i].fileName + "\"",
+ "Correct name");
+ is(sub[i].headers["Content-Type"],
+ expected[i].contentType,
+ "Correct content type");
+ is (getPropCount(sub[i].headers), 2,
+ "Wrong number of headers");
+ }
+ is(sub[i].body,
+ expected[i].value,
+ "Correct value");
+ }
+}
+
+function clickImage(aTarget, aX, aY)
+{
+ aTarget.style.position = "absolute";
+ aTarget.style.top = "0";
+ aTarget.style.left = "0";
+ aTarget.offsetTop;
+
+ var wu = SpecialPowers.getDOMWindowUtils(aTarget.ownerDocument.defaultView);
+
+ wu.sendMouseEvent('mousedown', aX, aY, 0, 1, 0);
+ wu.sendMouseEvent('mouseup', aX, aY, 0, 0, 0);
+
+ aTarget.style.position = "";
+ aTarget.style.top = "";
+ aTarget.style.left = "";
+}
+
+function* runTest() {
+ // Make normal submission
+ var form = $("form");
+ var iframe = $("target_iframe");
+ iframe.onload = function() { gen.next(); };
+
+ var elements = form.querySelectorAll("input, button");
+
+ is(elements.length, expectedSub.length,
+ "tests vs. expected out of sync");
+
+ var i;
+ for (i = 0; i < elements.length && i < expectedSub.length; ++i) {
+ elem = elements[i];
+ if (elem.localName != "input" || elem.type != "image") {
+ elem.click();
+ }
+ else {
+ clickImage(elem, 10, 7);
+ }
+ yield undefined;
+
+ var submission = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ checkSubmission(submission, expectedSub[i]);
+ }
+
+ // Disabled controls
+ var i;
+ for (i = 0; i < elements.length && i < expectedSub.length; ++i) {
+ elem = elements[i];
+ form.onsubmit = function() {
+ elem.disabled = true;
+ }
+ if (elem.localName != "input" || elem.type != "image") {
+ elem.click();
+ }
+ else {
+ clickImage(elem, 10, 7);
+ }
+ yield undefined;
+
+ is(elem.disabled, true, "didn't disable");
+ elem.disabled = false;
+ form.onsubmit = undefined;
+
+ var submission = JSON.parse(iframe.contentDocument.documentElement.textContent);
+ checkSubmission(submission, []);
+ }
+
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_formelements.html b/dom/html/test/test_formelements.html
new file mode 100644
index 0000000000..b753759bcb
--- /dev/null
+++ b/dom/html/test/test_formelements.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=772869
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 772869</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772869">Mozilla Bug 772869</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form id="f">
+ <input name="x">
+ <input type="image" name="a">
+ <input type="file" name="y">
+ <input type="submit" name="z">
+ <input id="w">
+ <input name="w">
+ </form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 772869 **/
+var x = $("f").elements;
+x.something = "another";
+names = [];
+for (var name in x) {
+ names.push(name);
+}
+is(names.length, 9, "Should have 9 enumerated names");
+is(names[0], "0", "Enum entry 1");
+is(names[1], "1", "Enum entry 2");
+is(names[2], "2", "Enum entry 3");
+is(names[3], "3", "Enum entry 4");
+is(names[4], "4", "Enum entry 5");
+is(names[5], "something", "Enum entry 6");
+is(names[6], "namedItem", "Enum entry 7");
+is(names[7], "item", "Enum entry 8");
+is(names[8], "length", "Enum entry 9");
+
+names = Object.getOwnPropertyNames(x);
+is(names.length, 10, "Should have 10 items");
+// Now sort entries 5 through 8, for comparison purposes. We don't sort the
+// whole array, because we want to make sure the ordering between the parts
+// is correct
+temp = names.slice(5, 9);
+temp.sort();
+names.splice.bind(names, 5, 4).apply(null, temp);
+is(names.length, 10, "Should still have 10 items");
+is(names[0], "0", "Entry 1")
+is(names[1], "1", "Entry 2")
+is(names[2], "2", "Entry 3")
+is(names[3], "3", "Entry 4")
+is(names[4], "4", "Entry 5")
+is(names[5], "w", "Entry 6")
+is(names[6], "x", "Entry 7")
+is(names[7], "y", "Entry 8")
+is(names[8], "z", "Entry 9")
+is(names[9], "something", "Entry 10")
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_fragment_form_pointer.html b/dom/html/test/test_fragment_form_pointer.html
new file mode 100644
index 0000000000..ff40385455
--- /dev/null
+++ b/dom/html/test/test_fragment_form_pointer.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=946585
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 946585</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=946585">Mozilla Bug 946585</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<form><div id="formdiv"></div></form>
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+/** Test for Bug 946585 **/
+var formDiv = document.getElementById("formdiv");
+formDiv.innerHTML = '<form>';
+is(formDiv.firstChild, null, "InnerHTML should not produce form element because the div has a form pointer.");
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_frame_count_with_synthetic_doc.html b/dom/html/test/test_frame_count_with_synthetic_doc.html
new file mode 100644
index 0000000000..c282ed09d7
--- /dev/null
+++ b/dom/html/test/test_frame_count_with_synthetic_doc.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ function getWindowLength() {
+ setTimeout(function() {
+ if (window.length) {
+ ok(false, "Synthetic document shouldn't be exposed");
+ }
+
+ // Keep running this check until the test stops
+ getWindowLength();
+ });
+ }
+
+ function addObject() {
+ const object = document.createElement("object");
+ object.data = 'file_bug417760.png';
+ document.body.appendChild(object);
+
+ object.onload = function() {
+ ok(true, "Test passes");
+ SimpleTest.finish();
+ }
+ }
+
+ getWindowLength();
+ addObject();
+ </script>
+ </body>
+</html>
diff --git a/dom/html/test/test_getElementsByName_after_mutation.html b/dom/html/test/test_getElementsByName_after_mutation.html
new file mode 100644
index 0000000000..f88b8d579e
--- /dev/null
+++ b/dom/html/test/test_getElementsByName_after_mutation.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1376695
+-->
+<head>
+ <title>Test for Bug 1376695</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1376695">Mozilla Bug 1376695</a>
+<p id="display"></p>
+<div id="originalFoo" name="foo">
+<pre id="test">
+<script type="application/javascript">
+
+/** Test to ensure that the list returned by getElementsByName is updated after
+ * mutations.
+ **/
+
+var fooList = document.getElementsByName("foo");
+var originalDiv = document.getElementById("originalFoo");
+
+is(fooList.length, 1, "Should find one element with name 'foo' initially");
+is(fooList[0], originalDiv, "Element should be the original div");
+
+var newTree = document.createElement("p");
+var child1 = document.createElement("div");
+var child2 = document.createElement("div");
+child2.setAttribute("name", "foo");
+
+newTree.appendChild(child1);
+newTree.appendChild(child2);
+document.body.appendChild(newTree);
+
+is(fooList.length, 2,
+ "Should find two elements with name 'foo' after appending the new tree");
+is(fooList[1], child2, "Element should be the new appended div with name 'foo'");
+
+document.body.removeChild(newTree);
+
+is(fooList.length, 1,
+ "Should find one element with name 'foo' after removing the new tree");
+is(fooList[0], originalDiv,
+ "Element should be the original div after removing the new tree");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_hidden.html b/dom/html/test/test_hidden.html
new file mode 100644
index 0000000000..7b9d488c0e
--- /dev/null
+++ b/dom/html/test/test_hidden.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=567663
+-->
+<head>
+ <title>Test for Bug 567663</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567663">Mozilla Bug 567663</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <p></p>
+ <p hidden></p>
+</div>
+<pre id="test">
+<script>
+/** Test for Bug 567663 **/
+var ps = document.getElementById("content").getElementsByTagName("p");
+is(ps[0].hidden, false, "First p's IDL attribute was wrong.");
+is(ps[0].hasAttribute("hidden"), false, "First p had a content attribute.");
+is(ps[1].hidden, true, "Second p's IDL attribute was wrong.");
+is(ps[1].hasAttribute("hidden"), true,
+ "Second p didn't have a content attribute.");
+is(ps[1].getAttribute("hidden"), "",
+ "Second p's content attribute was wrong.");
+
+ps[0].hidden = true;
+is(ps[0].getAttribute("hidden"), "",
+ "Content attribute was set to an incorrect value.");
+ps[1].hidden = false;
+is(ps[1].hasAttribute("hidden"), false,
+ "Second p still had a content attribute.");
+
+ps[0].setAttribute("hidden", "banana");
+is(ps[0].hidden, true, "p's IDL attribute was wrong after setting.");
+is(ps[0].getAttribute("hidden"), "banana", "Content attribute changed.");
+
+ps[0].setAttribute("hidden", "false");
+is(ps[0].hidden, true, "p's IDL attribute was wrong after setting.");
+is(ps[0].getAttribute("hidden"), "false", "Content attribute changed.");
+
+ps[0].removeAttribute("hidden");
+is(ps[0].hidden, false,
+ "p's IDL attribute was wrong after removing the content attribute.");
+is(ps[0].hasAttribute("hidden"), false);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_html_attributes_reflection.html b/dom/html/test/test_html_attributes_reflection.html
new file mode 100644
index 0000000000..a3d6c63121
--- /dev/null
+++ b/dom/html/test/test_html_attributes_reflection.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLHtmlElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLHtmlElement attributes reflection **/
+
+// .version
+reflectString({
+ element: document.createElement("html"),
+ attribute: "version",
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_htmlcollection.html b/dom/html/test/test_htmlcollection.html
new file mode 100644
index 0000000000..2d91189f6b
--- /dev/null
+++ b/dom/html/test/test_htmlcollection.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=772869
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 772869</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772869">Mozilla Bug 772869</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <a class="foo" name="x"></a>
+ <span class="foo" id="y"></span>
+ <span class="foo" name="x"></span>
+ <form class="foo" name="z" id="w"></form>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 772869 **/
+var x = document.getElementsByClassName("foo");
+x.something = "another";
+var names = [];
+for (var name in x) {
+ names.push(name);
+}
+is(names.length, 8, "Should have 8 enumerated names");
+is(names[0], "0", "Enum entry 1")
+is(names[1], "1", "Enum entry 2")
+is(names[2], "2", "Enum entry 3")
+is(names[3], "3", "Enum entry 4")
+is(names[4], "something", "Enum entry 5")
+is(names[5], "item", "Enum entry 6")
+is(names[6], "namedItem", "Enum entry 7")
+is(names[7], "length", "Enum entry 8");
+
+names = Object.getOwnPropertyNames(x);
+is(names.length, 9, "Should have 9 items");
+is(names[0], "0", "Entry 1")
+is(names[1], "1", "Entry 2")
+is(names[2], "2", "Entry 3")
+is(names[3], "3", "Entry 4")
+is(names[4], "x", "Entry 5")
+is(names[5], "y", "Entry 6")
+is(names[6], "w", "Entry 7")
+is(names[7], "z", "Entry 8")
+is(names[8], "something", "Entry 9")
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_iframe_sandbox_general.html b/dom/html/test/test_iframe_sandbox_general.html
new file mode 100644
index 0000000000..625b7aeeb2
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_general.html
@@ -0,0 +1,283 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=341604
+Implement HTML5 sandbox attribute for IFRAMEs - general tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 341604 and Bug 766282</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs - general tests **/
+
+SimpleTest.expectAssertions(0, 1);
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestCompleteLog();
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to communicate pass/fail back to this main page.
+// it expects to be called with an object like {ok: true/false, desc:
+// <description of the test> which it then forwards to ok()
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event)
+{
+ ok_wrapper(event.data.ok, event.data.desc);
+}
+
+var completedTests = 0;
+var passedTests = 0;
+
+function ok_wrapper(result, desc) {
+ ok(result, desc);
+
+ completedTests++;
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (completedTests == 32) {
+ is(passedTests, completedTests, "There are " + completedTests + " general tests that should pass");
+ SimpleTest.finish();
+ }
+}
+
+function doTest() {
+ // passes twice if good
+ // 1) test that inline scripts (<script>) can run in an iframe sandboxed with "allow-scripts"
+ // (done in file_iframe_sandbox_c_if1.html which has 'allow-scripts')
+
+ // passes twice if good
+ // 2) test that <script src=...> can run in an iframe sandboxed with "allow-scripts"
+ // (done in file_iframe_sandbox_c_if1.html which has 'allow-scripts')
+
+ // passes twice if good
+ // 3) test that script in an event listener (body onload) can run in an iframe sandboxed with "allow-scripts"
+ // (done in file_iframe_sandbox_c_if1.html which has 'allow-scripts')
+
+ // passes twice if good
+ // 4) test that script in an javascript:url can run in an iframe sandboxed with "allow-scripts"
+ // (done in file_iframe_sandbox_c_if1.html which has 'allow-scripts')
+
+ // fails if bad
+ // 5) test that inline scripts cannot run in an iframe sandboxed without "allow-scripts"
+ // (done in file_iframe_sandbox_c_if2.html which has sandbox='')
+
+ // fails if bad
+ // 6) test that <script src=...> cannot run in an iframe sandboxed without "allow-scripts"
+ // (done in file_iframe_sandbox_c_if2.html which has sandbox='')
+
+ // fails if bad
+ // 7) test that script in an event listener (body onload) cannot run in an iframe sandboxed without "allow-scripts"
+ // (done in file_iframe_sandbox_c_if2.html which has sandbox='')
+
+ // fails if bad
+ // 8) test that script in an event listener (img onerror) cannot run in an iframe sandboxed without "allow-scripts"
+ // (done in file_iframe_sandbox_c_if2.html which has sandbox='')
+
+ // fails if bad
+ // 9) test that script in an javascript:url cannot run in an iframe sandboxed without "allow-scripts"
+ // (done in file_iframe_sandbox_c_if_5.html which has sandbox='allow-same-origin')
+ var if_w = document.getElementById('if_5').contentWindow;
+ sendMouseEvent({type:'click'}, 'a_link', if_w);
+
+ // passes if good
+ // 10) test that a new iframe has sandbox attribute
+ var ifr = document.createElement("iframe");
+ ok_wrapper("sandbox" in ifr, "a new iframe should have a sandbox attribute");
+
+ // passes if good
+ // 11) test that the sandbox attribute's default stringyfied value is an empty string
+ ok_wrapper(ifr.sandbox.length === 0 && ifr.sandbox == "", "default sandbox attribute should be an empty string");
+
+ // passes if good
+ // 12) test that a sandboxed iframe with 'allow-forms' can submit forms
+ // (done in file_iframe_sandbox_c_if3.html which has 'allow-forms' and 'allow-scripts')
+
+ // fails if bad
+ // 13) test that a sandboxed iframe without 'allow-forms' can NOT submit forms
+ // (done in file_iframe_sandbox_c_if1.html which only has 'allow-scripts')
+
+ // fails if bad
+ // 14) test that a sandboxed iframe can't open a new window using the target.attribute
+ // this is done via file_iframe_sandbox_c_if4.html which is sandboxed with "allow-scripts" and "allow-same-origin"
+ // the window it attempts to open calls window.opener.ok(false, ...) and file_iframe_c_if4.html has an ok()
+ // function that calls window.parent.ok_wrapper
+
+ // passes if good
+ // 15) test that a sandboxed iframe can't open a new window using window.open
+ // this is done via file_iframe_sandbox_c_if4.html which is sandboxed with "allow-scripts" and "allow-same-origin"
+ // the window it attempts to open calls window.opener.ok(false, ...) and file_iframe_c_if4.html has an ok()
+ // function that calls window.parent.ok_wrapper
+
+ // passes if good
+ // 16) test that a sandboxed iframe can't open a new window using window.ShowModalDialog
+ // this is done via file_iframe_sandbox_c_if4.html which is sandboxed with "allow-scripts" and "allow-same-origin"
+ // the window it attempts to open calls window.opener.ok(false, ...) and file_iframe_c_if4.html has an ok()
+ // function that calls window.parent.ok_wrapper
+
+ // passes twice if good
+ // 17) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute
+ // is separated with two spaces
+ // done via file_iframe_sandbox_c_if6.html which is sandboxed with " allow-scripts allow-same-origin "
+
+ // passes twice if good
+ // 18) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute
+ // is separated with tabs
+ // done via file_iframe_sandbox_c_if6.html which is sandboxed with "&#x09;allow-scripts&#x09;allow-same-origin&#x09;"
+
+ // passes twice if good
+ // 19) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute
+ // is separated with line feeds
+ // done via file_iframe_sandbox_c_if6.html which is sandboxed with "&#x0a;allow-scripts&#x0a;allow-same-origin&#x0a;"
+
+ // passes twice if good
+ // 20) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute
+ // is separated with form feeds
+ // done via file_iframe_sandbox_c_if6.html which is sandboxed with "&#x0c;allow-scripts&#x0c;allow-same-origin&#x0c;"
+
+ // passes twice if good
+ // 21) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute
+ // is separated with carriage returns
+ // done via file_iframe_sandbox_c_if6.html which is sandboxed with "&#x0d;allow-scripts&#x0d;allow-same-origin&#x0d;"
+
+ // fails if bad
+ // 22) test that an iframe with sandbox="" does NOT have script in a src attribute created by a javascript:
+ // URL executed
+ // done by this page, see if_7
+
+ // passes if good
+ // 23) test that an iframe with sandbox="allow-scripts" DOES have script in a src attribute created by a javascript:
+ // URL executed
+ // done by this page, see if_8
+
+ // fails if bad
+ // 24) test that an iframe with sandbox="", starting out with a document already loaded, does NOT have script in a newly
+ // set src attribute created by a javascript: URL executed
+ // done by this page, see if_9
+
+ // passes if good
+ // 25) test that an iframe with sandbox="allow-scripts", starting out with a document already loaded, DOES have script
+ // in a newly set src attribute created by a javascript: URL executed
+ // done by this page, see if_10
+
+ // passes if good or fails if bad
+ // 26) test that an sandboxed document without 'allow-same-origin' can NOT access indexedDB
+ // done via file_iframe_sandbox_c_if7.html, which has sandbox='allow-scripts'
+
+ // passes if good or fails if bad
+ // 27) test that an sandboxed document with 'allow-same-origin' can access indexedDB
+ // done via file_iframe_sandbox_c_if8.html, which has sandbox='allow-scripts allow-same-origin'
+
+ // fails if bad
+ // 28) Test that a sandboxed iframe can't open a new window using the target.attribute for a
+ // non-existing browsing context (BC341604).
+ // This is done via file_iframe_sandbox_c_if4.html which is sandboxed with "allow-scripts" and "allow-same-origin"
+ // the window it attempts to open calls window.opener.ok(false, ...) and file_iframe_c_if4.html has an ok()
+ // function that calls window.parent.ok_wrapper.
+
+ // passes twice if good
+ // 29-32) Test that sandboxFlagsAsString returns the set flags.
+ // see if_14 and if_15
+
+ // passes once if good
+ // 33) Test that sandboxFlagsAsString returns null if iframe does not have sandbox flag set.
+ // see if_16
+}
+
+addLoadEvent(doTest);
+
+var started_if_9 = false;
+var started_if_10 = false;
+
+function start_if_9() {
+ if (started_if_9)
+ return;
+
+ started_if_9 = true;
+ sendMouseEvent({type:'click'}, 'a_button');
+}
+
+function start_if_10() {
+ if (started_if_10)
+ return;
+
+ started_if_10 = true;
+ sendMouseEvent({type:'click'}, 'a_button2');
+}
+
+function do_if_9() {
+ var if_9 = document.getElementById('if_9');
+ if_9.src = 'javascript:"<html><script>window.parent.ok_wrapper(false, \'an iframe sandboxed without allow-scripts should not execute script in a javascript URL in a newly set src attribute\');<\/script><\/html>"';
+}
+
+function do_if_10() {
+ var if_10 = document.getElementById('if_10');
+ if_10.src = 'javascript:"<html><script>window.parent.ok_wrapper(true, \'an iframe sandboxed with allow-scripts should execute script in a javascript URL in a newly set src attribute\');<\/script><\/html>"';
+}
+
+function eqFlags(a, b) {
+ // both a and b should be either null or have the array same flags
+ if (a === null && b === null) { return true; }
+ if (a === null || b === null) { return false; }
+ if (a.length !== b.length) { return false; }
+ var a_sorted = a.sort();
+ var b_sorted = b.sort();
+ for (var i in a_sorted) {
+ if (a_sorted[i] !== b_sorted[i]) { return false; }
+ }
+ return true;
+}
+
+function getSandboxFlags(doc) {
+ var flags = doc.sandboxFlagsAsString;
+ if (flags === null) { return null; }
+ return flags? flags.split(" "):[];
+}
+
+function test_sandboxFlagsAsString(name, expected) {
+ var ifr = document.getElementById(name);
+ try {
+ var flags = getSandboxFlags(SpecialPowers.wrap(ifr).contentDocument);
+ ok_wrapper(eqFlags(flags, expected), name + ' expected: "' + expected + '", got: "' + flags + '"');
+ } catch (e) {
+ ok_wrapper(false, name + ' expected "' + expected + ', but failed with ' + e);
+ }
+}
+
+</script>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs
+<p id="display"></p>
+<div id="content">
+<iframe sandbox="allow-same-origin allow-scripts" id="if_1" src="file_iframe_sandbox_c_if1.html" height="10" width="10"></iframe>
+<iframe sandbox="aLlOw-SAME-oRiGin ALLOW-sCrIpTs" id="if_1_case_insensitive" src="file_iframe_sandbox_c_if1.html" height="10" width="10"></iframe>
+<iframe sandbox="" id="if_2" src="file_iframe_sandbox_c_if2.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-forms allow-scripts" id="if_3" src="file_iframe_sandbox_c_if3.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin allow-scripts" id="if_4" src="file_iframe_sandbox_c_if4.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin" id="if_5" src="file_iframe_sandbox_c_if5.html" height="10" width="10"></iframe>
+<iframe sandbox=" allow-same-origin allow-scripts " id="if_6_a" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe>
+<iframe sandbox="&#x09;allow-same-origin&#x09;allow-scripts&#x09;" id="if_6_b" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe>
+<iframe sandbox="&#x0a;allow-same-origin&#x0a;allow-scripts&#x0a;" id="if_6_c" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe>
+<iframe sandbox="&#x0c;allow-same-origin&#x0c;allow-scripts&#x0c;" id="if_6_d" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe>
+<iframe sandbox="&#x0d;allow-same-origin&#x0d;allow-scripts&#x0d;" id="if_6_e" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin" id='if_7' src="javascript:'<html><script>window.parent.ok_wrapper(false, \'an iframe sandboxed without allow-scripts should not execute script in a javascript URL in its src attribute\');<\/script><\/html>';" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin allow-scripts" id='if_8' src="javascript:'<html><script>window.parent.ok_wrapper(true, \'an iframe sandboxed without allow-scripts should execute script in a javascript URL in its src attribute\');<\/script><\/html>';" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin" onload='start_if_9()' id='if_9' src="about:blank" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin allow-scripts" onload='start_if_10()' id='if_10' src="about:blank" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id='if_11' src="file_iframe_sandbox_c_if7.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin allow-scripts" id='if_12' src="file_iframe_sandbox_c_if8.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation " id='if_13' src="file_iframe_sandbox_c_if9.html" height="10" width="10" onload='test_sandboxFlagsAsString("if_13",["allow-forms", "allow-pointer-lock", "allow-popups", "allow-same-origin", "allow-scripts", "allow-top-navigation"])'></iframe>
+<iframe sandbox="&#x09;allow-same-origin&#x09;allow-scripts&#x09;" id="if_14" src="file_iframe_sandbox_c_if6.html" height="10" width="10" onload='test_sandboxFlagsAsString("if_14",["allow-same-origin","allow-scripts"])'></iframe>
+<iframe sandbox="" id="if_15" src="file_iframe_sandbox_c_if9.html" height="10" width="10" onload='test_sandboxFlagsAsString("if_15",[])'></iframe>
+<iframe id="if_16" src="file_iframe_sandbox_c_if9.html" height="10" width="10" onload='test_sandboxFlagsAsString("if_16",null)'></iframe>
+<input type='button' id="a_button" onclick='do_if_9()'>
+<input type='button' id="a_button2" onclick='do_if_10()'>
+</div>
+</body>
+</html>
diff --git a/dom/html/test/test_iframe_sandbox_inheritance.html b/dom/html/test/test_iframe_sandbox_inheritance.html
new file mode 100644
index 0000000000..991e7ef78f
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_inheritance.html
@@ -0,0 +1,202 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=341604
+Implement HTML5 sandbox attribute for IFRAMEs - inheritance tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/
+/** Inheritance Tests **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+// A postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to communicate pass/fail back to this main page.
+// It expects to be called with an object like {ok: true/false, desc:
+// <description of the test> which it then forwards to ok().
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+ switch (event.data.type) {
+ case "attempted":
+ testAttempted();
+ break;
+ case "ok":
+ ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted);
+ break;
+ default:
+ // allow for old style message
+ if (event.data.ok != undefined) {
+ ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted);
+ }
+ }
+}
+
+var attemptedTests = 0;
+var passedTests = 0;
+var totalTestsToPass = 15;
+var totalTestsToAttempt = 19;
+
+function ok_wrapper(result, desc, addToAttempted = true) {
+ ok(result, desc);
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (addToAttempted) {
+ testAttempted();
+ }
+}
+
+// Added so that tests that don't register unless they fail,
+// can at least notify that they've attempted to run.
+function testAttempted() {
+ attemptedTests++;
+ if (attemptedTests == totalTestsToAttempt) {
+ // Make sure all tests have had a chance to complete.
+ setTimeout(function() {finish();}, 1000);
+ }
+}
+
+var finishCalled = false;
+
+function finish() {
+ if (!finishCalled) {
+ finishCalled = true;
+ is(passedTests, totalTestsToPass, "There are " + totalTestsToPass + " inheritance tests that should pass");
+
+ SimpleTest.finish();
+ }
+}
+
+function doTest() {
+ // fails if bad
+ // 1) an iframe with no sandbox attribute inside an iframe that has sandbox = ""
+ // should not be able to execute scripts (cannot ever loosen permissions)
+ // (done by file_iframe_sandbox_a_if2.html contained within file_iframe_sandbox_a_if1.html)
+ testAttempted();
+
+ // fails if bad
+ // 2) an iframe with sandbox = "allow-scripts" inside an iframe that has sandbox = ""
+ // should not be able to execute scripts (cannot ever loosen permissions)
+ // (done by file_iframe_sandbox_a_if2.html contained within file_iframe_sandbox_a_if1.html)
+ testAttempted();
+
+ // passes if good and fails if bad
+ // 3) an iframe with no sandbox attribute inside an iframe that has sandbox = "allow-scripts"
+ // should not be same origin with the top window
+ // (done by file_iframe_sandbox_a_if4.html contained within file_iframe_sandbox_a_if3.html)
+
+ // passes if good and fails if bad
+ // 4) an iframe with no sandbox attribute inside an iframe that has sandbox = "allow-scripts"
+ // should not be same origin with its parent
+ // (done by file_iframe_sandbox_a_if4.html contained within file_iframe_sandbox_a_if3.html)
+
+ // passes if good
+ // 5) an iframe with 'allow-same-origin' and 'allow-scripts' inside an iframe with 'allow-same-origin'
+ // and 'allow-scripts' should be same origin with the top window
+ // (done by file_iframe_sandbox_a_if6.html contained within file_iframe_sandbox_a_if5.html)
+
+ // passes if good
+ // 6) an iframe with 'allow-same-origin' and 'allow-scripts' inside an iframe with 'allow-same-origin'
+ // and 'allow-scripts' should be same origin with its parent
+ // (done by file_iframe_sandbox_a_if6.html contained within file_iframe_sandbox_a_if5.html)
+
+ // passes if good
+ // 7) an iframe with no sandbox attribute inside an iframe that has sandbox = "allow-scripts"
+ // should be able to execute scripts
+ // (done by file_iframe_sandbox_a_if7.html contained within file_iframe_sandbox_a_if3.html)
+
+ // fails if bad
+ // 8) an iframe with sandbox="" inside an iframe that has allow-scripts should not be able
+ // to execute scripts
+ // (done by file_iframe_sandbox_a_if2.html contained within file_iframe_sandbox_a_if3.html)
+ testAttempted();
+
+ // passes if good
+ // 9) make sure that changing the sandbox flags on an iframe (if_8) doesn't affect
+ // the sandboxing of subloads of content within that iframe
+ var if_8 = document.getElementById('if_8');
+ if_8.sandbox = 'allow-scripts';
+ if_8.contentWindow.doSubload();
+
+ // passes if good
+ // 10) a <frame> inside an <iframe> sandboxed with 'allow-scripts' should not be same
+ // origin with this document
+ // done by file_iframe_sandbox_a_if11.html which is contained with file_iframe_sandbox_a_if10.html
+
+ // passes if good
+ // 11) a <frame> inside a <frame> inside an <iframe> sandboxed with 'allow-scripts' should not be same
+ // origin with its parent frame or this document
+ // done by file_iframe_sandbox_a_if12.html which is contained with file_iframe_sandbox_a_if11.html
+
+ // passes if good, fails if bad
+ // 12) An <object> inside an <iframe> sandboxed with 'allow-scripts' should not be same
+ // origin with this document
+ // Done by file_iframe_sandbox_a_if14.html which is contained within file_iframe_sandbox_a_if13.html
+
+ // passes if good, fails if bad
+ // 13) An <object> inside an <object> inside an <iframe> sandboxed with 'allow-scripts' should not be same
+ // origin with its parent frame or this document
+ // Done by file_iframe_sandbox_a_if15.html which is contained within file_iframe_sandbox_a_if14.html
+
+ // passes if good, fails if bad
+ // 14) An <object> inside a <frame> inside an <iframe> sandboxed with 'allow-scripts' should not be same
+ // origin with its parent frame or this document
+ // Done by file_iframe_sandbox_a_if15.html which is contained within file_iframe_sandbox_a_if16.html
+ // which is contained within file_iframe_sandbox_a_if10.html
+
+ // passes if good
+ // 15) An <object> inside an <object> inside an <iframe> sandboxed with 'allow-scripts allow-forms'
+ // should be able to submit forms.
+ // Done by file_iframe_sandbox_a_if15.html which is contained within file_iframe_sandbox_a_if14.html
+
+ // passes if good
+ // 16) An <object> inside a <frame> inside an <iframe> sandboxed with 'allow-scripts allow-forms'
+ // should be able to submit forms.
+ // Done by file_iframe_sandbox_a_if15.html which is contained within file_iframe_sandbox_a_if16.html
+ // which is contained within file_iframe_sandbox_a_if10.html
+
+ // fails if bad
+ // 17) An <object> inside an <iframe> sandboxed with 'allow-same-origin'
+ // should not be able to run scripts.
+ // Done by iframe "if_no_scripts", which loads file_iframe_sandbox_srcdoc_no_allow_scripts.html.
+ testAttempted();
+
+ // passes if good
+ // 18) An <object> inside an <iframe> sandboxed with 'allow-scripts allow-same-origin'
+ // should be able to run scripts and be same origin with this document.
+ // Done by iframe "if_scripts", which loads file_iframe_sandbox_srcdoc_allow_scripts.html.
+
+ // passes if good, fails if bad
+ // 19) Make sure that the parent's document's sandboxing flags are copied when
+ // changing the sandbox flags on an iframe inside an iframe.
+ // Done in file_iframe_sandbox_a_if17.html and file_iframe_sandbox_a_if18.html
+}
+
+addLoadEvent(doTest);
+</script>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs
+<p id="display"></p>
+<div id="content">
+<iframe sandbox="" id="if_1" src="file_iframe_sandbox_a_if1.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_3" src="file_iframe_sandbox_a_if3.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts allow-same-origin" id="if_5" src="file_iframe_sandbox_a_if5.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts allow-same-origin" id="if_8" src="file_iframe_sandbox_a_if8.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts allow-forms" id="if_10" src="file_iframe_sandbox_a_if10.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts allow-forms" id="if_13" src="file_iframe_sandbox_a_if13.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin" id="if_no_scripts" srcdoc="<object data='file_iframe_sandbox_srcdoc_no_allow_scripts.html'></object>" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts allow-same-origin" id="if_scripts" srcdoc="<object data='file_iframe_sandbox_srcdoc_allow_scripts.html'></object>" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_17" src="file_iframe_sandbox_a_if17.html" height="10" width="10"></iframe>
+</div>
+</body>
+</html>
diff --git a/dom/html/test/test_iframe_sandbox_navigation.html b/dom/html/test/test_iframe_sandbox_navigation.html
new file mode 100644
index 0000000000..caaf4439b8
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_navigation.html
@@ -0,0 +1,285 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=341604
+Implement HTML5 sandbox attribute for IFRAMEs
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604 - navigation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/
+/** Navigation tests Part 1**/
+
+SimpleTest.requestLongerTimeout(2); // slow on Android
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin'/other windows to communicate pass/fail back to this main page.
+// it expects to be called with an object like {ok: true/false, desc:
+// <description of the test> which it then forwards to ok()
+var bc = new BroadcastChannel("test_iframe_sandbox_navigation");
+bc.addEventListener("message", receiveMessage);
+window.addEventListener("message", receiveMessage);
+
+var testPassesReceived = 0;
+
+function receiveMessage(event) {
+ switch (event.data.type) {
+ case "attempted":
+ testAttempted();
+ break;
+ case "ok":
+ ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted);
+ break;
+ case "if_10":
+ doIf10TestPart2();
+ break;
+ default:
+ // allow for old style message
+ if (event.data.ok != undefined) {
+ ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted);
+ }
+ }
+}
+
+// Open windows for tests to attempt to navigate later.
+var windowsToClose = new Array();
+
+var attemptedTests = 0;
+var passedTests = 0;
+var totalTestsToPass = 7;
+var totalTestsToAttempt = 13;
+
+function ok_wrapper(result, desc, addToAttempted = true) {
+ ok(result, desc);
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (addToAttempted) {
+ testAttempted();
+ }
+}
+
+// Added so that tests that don't register unless they fail,
+// can at least notify that they've attempted to run.
+function testAttempted() {
+ attemptedTests++;
+ if (attemptedTests == totalTestsToAttempt) {
+ // Make sure all tests have had a chance to complete.
+ setTimeout(function() {finish();}, 1000);
+ }
+}
+
+var finishCalled = false;
+
+function finish() {
+ if (!finishCalled) {
+ finishCalled = true;
+ is(passedTests, totalTestsToPass, "There are " + totalTestsToPass + " navigation tests that should pass");
+
+ closeWindows();
+
+ bc.close();
+
+ SimpleTest.finish();
+ }
+}
+
+function checkTestsFinished() {
+ // If our own finish() has not been called, probably failed due to a timeout, so close remaining windows.
+ if (!finishCalled) {
+ closeWindows();
+ }
+}
+
+function closeWindows() {
+ for (var i = 0; i < windowsToClose.length; i++) {
+ windowsToClose[i].close();
+ }
+}
+
+function doTest() {
+ // passes if good
+ // 1) A sandboxed iframe is allowed to navigate itself
+ // (done by file_iframe_sandbox_d_if1.html which has 'allow-scripts' and navigates to
+ // file_iframe_sandbox_navigation_pass.html).
+
+ // passes if good
+ // 2) A sandboxed iframe is allowed to navigate its children, even if they are sandboxed
+ // (done by file_iframe_sandbox_d_if2.html which has 'allow-scripts', it navigates a child
+ // iframe containing file_iframe_sandbox_navigation_start.html to file_iframe_sandbox_navigation_pass.html).
+
+ // fails if bad
+ // 3) A sandboxed iframe is not allowed to navigate its ancestor
+ // (done by file_iframe_sandbox_d_if4.html contained within file_iframe_sandbox_d_if3.html,
+ // it attempts to navigate file_iframe_sandbox_d_if3.html to file_iframe_sandbox_navigation_fail.html).
+
+ // fails if bad
+ // 4) A sandboxed iframe is not allowed to navigate its sibling
+ // (done by file_iframe_sandbox_d_if5.html which has 'allow scripts allow-same-origin'
+ // and attempts to navigate file_iframe_navigation_start.html contained in if_sibling on this
+ // page to file_iframe_sandbox_navigation_fail.html).
+
+ // passes if good, fails if bad
+ // 5) When a link is clicked in a sandboxed iframe, the document navigated to is sandboxed
+ // the same as the original document and is not same origin with parent document
+ // (done by file_iframe_sandbox_d_if6.html which simulates a link click and navigates
+ // to file_iframe_sandbox_d_if7.html which attempts to call back into its parent).
+
+ // fails if bad
+ // 6) An iframe (if_8) has sandbox="allow-same-origin allow-scripts", the sandboxed document
+ // (file_iframe_sandbox_d_if_8.html) that it contains accesses its parent (this file) and removes
+ // 'allow-same-origin' and then triggers a reload.
+ // The document should not be able to access its parent (this file).
+
+ // fails if bad
+ // 7) An iframe (if_9) has sandbox="allow-same-origin allow-scripts", the sandboxed document
+ // (file_iframe_sandbox_d_if_9.html) that it contains accesses its parent (this file) and removes
+ // 'allow-scripts' and then triggers a reload.
+ // The document should not be able to run a script and access its parent (this file).
+
+ // passes if good
+ // 8) a document in an iframe with sandbox='allow-scripts' should have a different null
+ // principal in its original document than a document to which it navigates itself
+ // file_iframe_sandbox_d_if_10.html does this, co-ordinating with this page via postMessage
+
+ // passes if good
+ // 9) a document (file_iframe_sandbox_d_if11.html in an iframe (if_11) with sandbox='allow-scripts'
+ // is navigated to file_iframe_sandbox_d_if12.html - when that document loads
+ // a message is sent back to this document, which adds 'allow-same-origin' to if_11 and then
+ // calls .back on it - file_iframe_sandbox_if12.html should be able to call back into this
+ // document - this is all contained in file_iframe_sandbox_d_if13.html which is opened in another
+ // tab so it has its own isolated session history
+ window.open("file_iframe_sandbox_d_if13.html");
+
+ // open up the top navigation tests
+
+ // fails if bad
+ // 10) iframe with sandbox='allow-scripts' can NOT navigate top
+ // file_iframe_sandbox_e_if1.html contains file_iframe_sandbox_e_if6.html which
+ // attempts to navigate top
+ windowsToClose.push(window.open("file_iframe_sandbox_e_if1.html"));
+
+ // fails if bad
+ // 11) iframe with sandbox='allow-scripts' nested inside iframe with
+ // 'allow-top-navigation allow-scripts' can NOT navigate top
+ // file_iframe_sandbox_e_if2.html contains file_iframe_sandbox_e_if1.html which
+ // contains file_iframe_sandbox_e_if6.html which attempts to navigate top
+ windowsToClose.push(window.open("file_iframe_sandbox_e_if2.html"));
+
+ // passes if good
+ // 12) iframe with sandbox='allow-top-navigation allow-scripts' can navigate top
+ // file_iframe_sandbox_e_if3.html contains file_iframe_sandbox_e_if5.html which navigates top
+ window.open("file_iframe_sandbox_e_if3.html");
+
+ // passes if good
+ // 13) iframe with sandbox='allow-top-navigation allow-scripts' nested inside an iframe with
+ // 'allow-top-navigation allow-scripts' can navigate top
+ // file_iframe_sandbox_e_if4.html contains file_iframe_sandbox_e_if3.html which contains
+ // file_iframe_sandbox_e_if5.html which navigates top
+ window.open("file_iframe_sandbox_e_if4.html");
+}
+
+addLoadEvent(doTest);
+
+window.modified_if_8 = false;
+
+function reload_if_8() {
+ var if_8 = document.getElementById('if_8');
+ if_8.src = 'file_iframe_sandbox_d_if8.html';
+}
+
+function modify_if_8() {
+ // If this is the second time this has been called
+ // that's a failed test (allow-same-origin was removed
+ // the first time).
+ if (window.modified_if_8) {
+ ok_wrapper(false, "a sandboxed iframe from which 'allow-same-origin' was removed should not be able to access its parent");
+
+ // need to return here since we end up in an infinite loop otherwise
+ return;
+ }
+
+ var if_8 = document.getElementById('if_8');
+ window.modified_if_8 = true;
+
+ if_8.sandbox = 'allow-scripts';
+ testAttempted();
+ sendMouseEvent({type:'click'}, 'a_button');
+}
+
+window.modified_if_9 = false;
+
+function reload_if_9() {
+ var if_9 = document.getElementById('if_9');
+ if_9.src = 'file_iframe_sandbox_d_if9.html';
+}
+
+function modify_if_9() {
+ // If this is the second time this has been called
+ // that's a failed test (allow-scripts was removed
+ // the first time).
+ if (window.modified_if_9) {
+ ok_wrapper(false, "an sandboxed iframe from which 'allow-scripts' should be removed should not be able to access its parent via a script", false);
+
+ // need to return here since we end up in an infinite loop otherwise
+ return;
+ }
+
+ var if_9 = document.getElementById('if_9');
+ window.modified_if_9 = true;
+
+ if_9.sandbox = 'allow-same-origin';
+
+ testAttempted();
+ sendMouseEvent({type:'click'}, 'a_button2');
+}
+
+var firstPrincipal = "";
+var secondPrincipal;
+
+function doIf10TestPart1() {
+ if (firstPrincipal != "")
+ return;
+
+ // use SpecialPowers to get the principal of if_10.
+ // NB: We stringify here and below because special-powers wrapping doesn't
+ // preserve identity.
+ var if_10 = document.getElementById('if_10');
+ firstPrincipal = SpecialPowers.wrap(if_10).contentDocument.nodePrincipal.origin;
+ if_10.src = 'file_iframe_sandbox_d_if10.html';
+}
+
+function doIf10TestPart2() {
+ var if_10 = document.getElementById('if_10');
+ // use SpecialPowers to get the principal of if_10
+ secondPrincipal = SpecialPowers.wrap(if_10).contentDocument.nodePrincipal.origin;
+ ok_wrapper(firstPrincipal != secondPrincipal, "documents should NOT have the same principal if they are sandboxed without" +
+ " allow-same-origin and the first document is navigated to the second");
+}
+</script>
+<body onunload="checkTestsFinished()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs
+<p id="display"></p>
+<div id="content">
+<iframe sandbox="allow-scripts" id="if_1" src="file_iframe_sandbox_d_if1.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_2" src="file_iframe_sandbox_d_if2.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_3" src="file_iframe_sandbox_d_if3.html" height="10" width="10"></iframe>
+<iframe id="if_sibling" name="if_sibling" src="about:blank" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts allow-same-origin" id="if_5" src="file_iframe_sandbox_d_if5.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_6" src="file_iframe_sandbox_d_if6.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin allow-scripts" id="if_8" src="file_iframe_sandbox_d_if8.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin allow-scripts" id="if_9" src="file_iframe_sandbox_d_if9.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_10" src="file_iframe_sandbox_navigation_start.html" onload='doIf10TestPart1()' height="10" width="10"></iframe>
+</div>
+<input type='button' id="a_button" onclick='reload_if_8()'>
+<input type='button' id="a_button2" onclick='reload_if_9()'>
+</body>
+</html>
diff --git a/dom/html/test/test_iframe_sandbox_navigation2.html b/dom/html/test/test_iframe_sandbox_navigation2.html
new file mode 100644
index 0000000000..f17c23a458
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_navigation2.html
@@ -0,0 +1,216 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=341604
+Implement HTML5 sandbox attribute for IFRAMEs
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604 - navigation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/
+/** Navigation tests Part 2**/
+
+SimpleTest.expectAssertions(0);
+SimpleTest.requestLongerTimeout(2); // slow on Android
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin'/other windows to communicate pass/fail back to this main page.
+// it expects to be called with an object like {ok: true/false, desc:
+// <description of the test> which it then forwards to ok()
+var bc = new BroadcastChannel("test_iframe_sandbox_navigation");
+bc.addEventListener("message", receiveMessage);
+window.addEventListener("message", receiveMessage);
+
+var testPassesReceived = 0;
+
+function receiveMessage(event) {
+ switch (event.data.type) {
+ case "attempted":
+ testAttempted();
+ break;
+ case "ok":
+ ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted);
+ break;
+ default:
+ // allow for old style message
+ if (event.data.ok != undefined) {
+ ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted);
+ }
+ }
+}
+
+// Open windows for tests to attempt to navigate later.
+var windowsToClose = new Array();
+windowsToClose.push(window.open("about:blank", "window_to_navigate"));
+windowsToClose.push(window.open("about:blank", "window_to_navigate2"));
+var iframesWithWindowsToClose = new Array();
+
+var attemptedTests = 0;
+var passedTests = 0;
+var totalTestsToPass = 12;
+var totalTestsToAttempt = 15;
+
+function ok_wrapper(result, desc, addToAttempted = true) {
+ ok(result, desc);
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (addToAttempted) {
+ testAttempted();
+ }
+}
+
+// Added so that tests that don't register unless they fail,
+// can at least notify that they've attempted to run.
+function testAttempted() {
+ attemptedTests++;
+ if (attemptedTests == totalTestsToAttempt) {
+ // Make sure all tests have had a chance to complete.
+ setTimeout(function() {finish();}, 1000);
+ }
+}
+
+var finishCalled = false;
+
+function finish() {
+ if (!finishCalled) {
+ finishCalled = true;
+ is(passedTests, totalTestsToPass, "There are " + totalTestsToPass + " navigation tests that should pass");
+
+ for (var i = 0; i < windowsToClose.length; i++) {
+ windowsToClose[i].close();
+ }
+
+ bc.close();
+
+ SimpleTest.finish();
+ }
+}
+
+function checkTestsFinished() {
+ // If our own finish() has not been called, probably failed due to a timeout, so close remaining windows.
+ if (!finishCalled) {
+ for (var i = 0; i < windowsToClose.length; i++) {
+ windowsToClose[i].close();
+ }
+ }
+}
+
+function doTest() {
+ // fails if bad
+ // 14) iframe with sandbox='allow-same-origin allow-scripts allow-top-navigation' should not
+ // be able to navigate another window (opened by another browsing context) using its name.
+ // file_iframe_sandbox_d_if14.html in if_14 attempts to navigate "window_to_navigate",
+ // which has been opened in preparation.
+
+ // fails if bad
+ // 15) iframe with sandbox='allow-scripts' should not be able to navigate top using its
+ // real name (instead of _top) as allow-top-navigation is not specified.
+ // file_iframe_sandbox_e_if7.html contains file_iframe_sandbox_e_if8.html, which
+ // attempts to navigate top by name.
+ windowsToClose.push(window.open("file_iframe_sandbox_e_if7.html"));
+
+ // fails if bad
+ // 16) iframe with sandbox='allow-same-origin allow-scripts allow-top-navigation' should not
+ // be able to use its parent's name (instead of _parent) to navigate it, when it is not top.
+ // (Note: this would apply to other ancestors that are not top as well.)
+ // file_iframe_sandbox_d_if15.html in if_15 contains file_iframe_sandbox_d_if16.html, which
+ // tries to navigate if_15 by its name (if_parent).
+
+ // passes if good, fails if bad
+ // 17) A sandboxed iframe is allowed to navigate itself using window.open().
+ // (Done by file_iframe_sandbox_d_if17.html which has 'allow-scripts' and navigates to
+ // file_iframe_sandbox_navigation_pass.html).
+
+ // passes if good, fails if bad
+ // 18) A sandboxed iframe is allowed to navigate its children with window.open(), even if
+ // they are sandboxed. (Done by file_iframe_sandbox_d_if18.html which has 'allow-scripts',
+ // it navigates a child iframe to file_iframe_sandbox_navigation_pass.html).
+
+ // passes if good, fails if bad
+ // 19) A sandboxed iframe is not allowed to navigate its ancestor with window.open().
+ // (Done by file_iframe_sandbox_d_if20.html contained within file_iframe_sandbox_d_if19.html,
+ // it attempts to navigate file_iframe_sandbox_d_if19.html to file_iframe_sandbox_navigation_fail.html).
+
+ // passes if good, fails if bad
+ // 20) iframe with sandbox='allow-same-origin allow-scripts allow-top-navigation' should not
+ // be able to navigate another window (opened by another browsing context) using window.open(..., "<name>").
+ // file_iframe_sandbox_d_if14.html in if_14 attempts to navigate "window_to_navigate2",
+ // which has been opened in preparation, using window.open(..., "window_to_navigate2").
+
+ // passes if good, fails if bad
+ // 21) iframe with sandbox='allow-same-origin allow-scripts allow-top-navigation' should not
+ // be able to use its parent's name (not _parent) to navigate it using window.open(), when it is not top.
+ // (Note: this would apply to other ancestors that are not top as well.)
+ // file_iframe_sandbox_d_if21.html in if_21 contains file_iframe_sandbox_d_if22.html, which
+ // tries to navigate if_21 by its name (if_parent2).
+
+ // passes if good, fails if bad
+ // 22) iframe with sandbox='allow-top-navigation allow-scripts' can navigate top with window.open().
+ // file_iframe_sandbox_e_if9.html contains file_iframe_sandbox_e_if11.html which navigates top.
+ window.open("file_iframe_sandbox_e_if9.html");
+
+ // passes if good, fails if bad
+ // 23) iframe with sandbox='allow-top-navigation allow-scripts' nested inside an iframe with
+ // 'allow-top-navigation allow-scripts' can navigate top, with window.open().
+ // file_iframe_sandbox_e_if10.html contains file_iframe_sandbox_e_if9.html which contains
+ // file_iframe_sandbox_e_if11.html which navigates top.
+ window.open("file_iframe_sandbox_e_if10.html");
+
+ // passes if good, fails if bad
+ // 24) iframe with sandbox='allow-scripts' can NOT navigate top with window.open().
+ // file_iframe_sandbox_e_if12.html contains file_iframe_sandbox_e_if14.html which navigates top.
+ window.open("file_iframe_sandbox_e_if12.html");
+
+ // passes if good, fails if bad
+ // 25) iframe with sandbox='allow-scripts' nested inside an iframe with
+ // 'allow-top-navigation allow-scripts' can NOT navigate top, with window.open(..., "_top").
+ // file_iframe_sandbox_e_if13.html contains file_iframe_sandbox_e_if12.html which contains
+ // file_iframe_sandbox_e_if14.html which navigates top.
+ window.open("file_iframe_sandbox_e_if13.html");
+
+ // passes if good, fails if bad
+ // 26) iframe with sandbox='allow-scripts' should not be able to navigate top using its real name
+ // (not with _top e.g. window.open(..., "topname")) as allow-top-navigation is not specified.
+ // file_iframe_sandbox_e_if15.html contains file_iframe_sandbox_e_if16.html, which
+ // attempts to navigate top by name using window.open().
+ window.open("file_iframe_sandbox_e_if15.html");
+
+ // passes if good
+ // 27) iframe with sandbox='allow-scripts allow-popups' should be able to
+ // navigate a window, that it has opened, using it's name.
+ // file_iframe_sandbox_d_if23.html in if_23 opens a window and then attempts
+ // to navigate it using it's name in the target of an anchor.
+ iframesWithWindowsToClose.push("if_23");
+
+ // passes if good, fails if bad
+ // 28) iframe with sandbox='allow-scripts allow-popups' should be able to
+ // navigate a window, that it has opened, using window.open(..., "<name>").
+ // file_iframe_sandbox_d_if23.html in if_23 opens a window and then attempts
+ // to navigate it using it's name in the target of window.open().
+}
+
+addLoadEvent(doTest);
+</script>
+<body onunload="checkTestsFinished()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs
+<p id="display"></p>
+<div id="content">
+<iframe sandbox="allow-same-origin allow-scripts allow-top-navigation" id="if_14" src="file_iframe_sandbox_d_if14.html" height="10" width="10"></iframe>
+<iframe id="if_15" name="if_parent" src="file_iframe_sandbox_d_if15.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_17" src="file_iframe_sandbox_d_if17.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_18" src="file_iframe_sandbox_d_if18.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_19" src="file_iframe_sandbox_d_if19.html" height="10" width="10"></iframe>
+<iframe id="if_21" name="if_parent2" src="file_iframe_sandbox_d_if21.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts allow-popups" id="if_23" src="file_iframe_sandbox_d_if23.html" height="10" width="10"></iframe>
+</div>
+</body>
+</html>
diff --git a/dom/html/test/test_iframe_sandbox_popups.html b/dom/html/test/test_iframe_sandbox_popups.html
new file mode 100644
index 0000000000..c05b1fc67f
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_popups.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=766282
+implement allow-popups directive for iframe sandbox
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 766282</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to communicate pass/fail back to this main page.
+// it expects to be called with an object like {ok: true/false, desc:
+// <description of the test> which it then forwards to ok()
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event)
+{
+ ok_wrapper(event.data.ok, event.data.desc);
+}
+
+var completedTests = 0;
+var passedTests = 0;
+
+function ok_wrapper(result, desc) {
+ ok(result, desc);
+
+ completedTests++;
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (completedTests == 3) {
+ is(passedTests, completedTests, "There are " + completedTests + " popups tests that should pass");
+ SimpleTest.finish();
+ }
+}
+
+function doTest() {
+ // passes if good
+ // 1) Test that a sandboxed iframe with "allow-popups" can open a new window using the target.attribute.
+ // This is done via file_iframe_sandbox_h_if1.html which is sandboxed with "allow-popups allow-scripts allow-same-origin".
+ // The window it attempts to open calls window.opener.ok(true, ...) and file_iframe_h_if1.html has an ok()
+ // function that calls window.parent.ok_wrapper.
+
+ // passes if good
+ // 2) Test that a sandboxed iframe with "allow-popups" can open a new window using window.open.
+ // This is done via file_iframe_sandbox_h_if1.html which is sandboxed with "allow-popups allow-scripts allow-same-origin".
+ // The window it attempts to open calls window.opener.ok(true, ...) and file_iframe_h_if1.html has an ok()
+ // function that calls window.parent.ok_wrapper.
+
+ // passes if good, fails if bad
+ // 3) Test that a sandboxed iframe with "allow-popups" can open a new window using the target.attribute
+ // for a non-existing browsing context (BC766282).
+ // This is done via file_iframe_sandbox_h_if1.html which is sandboxed with "allow-popups allow-scripts allow-same-origin".
+ // The window it attempts to open calls window.opener.ok(true, ...) and file_iframe_h_if1.html has an ok()
+ // function that calls window.parent.ok_wrapper.
+}
+
+addLoadEvent(doTest);
+
+</script>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=766282">Mozilla Bug 766282</a> - implement allow-popups directive for iframe sandbox
+<p id="display"></p>
+<div id="content">
+<iframe sandbox="allow-popups allow-same-origin allow-scripts" id="if1" src="file_iframe_sandbox_h_if1.html" height="10" width="10"></iframe>
+</div>
+</body>
+</html>
diff --git a/dom/html/test/test_iframe_sandbox_popups_inheritance.html b/dom/html/test/test_iframe_sandbox_popups_inheritance.html
new file mode 100644
index 0000000000..af4a03932e
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_popups_inheritance.html
@@ -0,0 +1,157 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=766282
+Implement HTML5 sandbox allow-popuos directive for IFRAMEs - inheritance tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 766282</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<script type="application/javascript">
+
+SimpleTest.expectAssertions(0, 5);
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+// A postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to communicate pass/fail back to this main page.
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event) {
+ switch (event.data.type) {
+ case "attempted":
+ testAttempted();
+ break;
+ case "ok":
+ ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted);
+ break;
+ default:
+ // allow for old style message
+ if (event.data.ok != undefined) {
+ ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted);
+ }
+ }
+}
+
+var iframesWithWindowsToClose = new Array();
+
+var attemptedTests = 0;
+var passedTests = 0;
+var totalTestsToPass = 15;
+var totalTestsToAttempt = 21;
+
+function ok_wrapper(result, desc, addToAttempted = true) {
+ ok(result, desc);
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (addToAttempted) {
+ testAttempted();
+ }
+}
+
+// Added so that tests that don't register unless they fail,
+// can at least notify that they've attempted to run.
+function testAttempted() {
+ attemptedTests++;
+ if (attemptedTests == totalTestsToAttempt) {
+ // Make sure all tests have had a chance to complete.
+ setTimeout(function() {finish();}, 1000);
+ }
+}
+
+var finishCalled = false;
+
+function finish() {
+ if (!finishCalled) {
+ finishCalled = true;
+ is(passedTests, totalTestsToPass, "There are " + totalTestsToPass + " inheritance tests that should pass");
+
+ closeWindows();
+
+ SimpleTest.finish();
+ }
+}
+
+function checkTestsFinished() {
+ // If our own finish() has not been called, probably failed due to a timeout, so close remaining windows.
+ if (!finishCalled) {
+ closeWindows();
+ }
+}
+
+function closeWindows() {
+ for (var i = 0; i < iframesWithWindowsToClose.length; i++) {
+ document.getElementById(iframesWithWindowsToClose[i]).contentWindow.postMessage({type: "closeWindows"}, "*");
+ }
+}
+
+function doTest() {
+ // passes if good and fails if bad
+ // 1,2,3) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups
+ // allow-same-origin" should not have its origin sandbox flag set and be able to access document.cookie.
+ // (Done by file_iframe_sandbox_k_if5.html opened from file_iframe_sandbox_k_if4.html)
+ // This is repeated for 3 different ways of opening the window,
+ // see file_iframe_sandbox_k_if4.html for details.
+
+ // passes if good
+ // 4,5,6) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups
+ // allow-top-navigation" should not have its top-level navigation sandbox flag set and be able to
+ // navigate top. (Done by file_iframe_sandbox_k_if5.html (and if6) opened from
+ // file_iframe_sandbox_k_if4.html). This is repeated for 3 different ways of opening the window,
+ // see file_iframe_sandbox_k_if4.html for details.
+
+ // passes if good
+ // 7,8,9) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups
+ // all-forms" should not have its forms sandbox flag set and be able to submit forms.
+ // (Done by file_iframe_sandbox_k_if7.html opened from file_iframe_sandbox_k_if4.html)
+ // This is repeated for 3 different ways of opening the window,
+ // see file_iframe_sandbox_k_if4.html for details.
+
+ // passes if good
+ // 10,11,12) Make sure that the sandbox flags copied to a new browsing context are taken from the
+ // current active document not the browsing context (iframe / docShell).
+ // This is done by removing allow-same-origin and calling doSubOpens from file_iframe_sandbox_k_if8.html,
+ // which opens file_iframe_sandbox_k_if9.html in 3 different ways.
+ // It then navigates to file_iframe_sandbox_k_if1.html to run tests 13 - 21 below.
+ var if_8_1 = document.getElementById('if_8_1');
+ if_8_1.sandbox = 'allow-scripts allow-popups';
+ if_8_1.contentWindow.doSubOpens();
+
+ // passes if good and fails if bad
+ // 13,14,15) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups"
+ // should have its origin sandbox flag set and not be able to access document.cookie.
+ // This is done by file_iframe_sandbox_k_if8.html navigating to file_iframe_sandbox_k_if1.html
+ // after allow-same-origin has been removed from iframe if_8_1. file_iframe_sandbox_k_if1.html
+ // opens file_iframe_sandbox_k_if2.html in 3 different ways to perform the tests.
+ iframesWithWindowsToClose.push("if_8_1");
+
+ // fails if bad
+ // 16,17,18) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups"
+ // should have its forms sandbox flag set and not be able to submit forms.
+ // This is done by file_iframe_sandbox_k_if2.html, see test 10 for details of how this is opened.
+
+ // fails if bad
+ // 19,20,21) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups"
+ // should have its top-level navigation sandbox flag set and not be able to navigate top.
+ // This is done by file_iframe_sandbox_k_if2.html, see test 10 for details of how this is opened.
+}
+
+addLoadEvent(doTest);
+</script>
+
+<body onunload="checkTestsFinished()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=766282">Mozilla Bug 766282</a> - Implement HTML5 sandbox allow-popups directive for IFRAMEs
+<p id="display"></p>
+<div id="content">
+<iframe sandbox="allow-scripts allow-popups allow-same-origin allow-forms allow-top-navigation" id="if_4" src="file_iframe_sandbox_k_if4.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts allow-popups allow-same-origin" id="if_8_1" src="file_iframe_sandbox_k_if8.html" height="10" width="10"></iframe>
+</div>
+</body>
+</html>
diff --git a/dom/html/test/test_iframe_sandbox_redirect.html b/dom/html/test/test_iframe_sandbox_redirect.html
new file mode 100644
index 0000000000..ff13e52487
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_redirect.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=985135
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 985135</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 985135 **/
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(function() {
+ try {
+ var doc = frames[0].document;
+ ok(false, "Should not be able to get the document");
+ isnot(doc.body.textContent.slice(0, -1), "I have been redirected",
+ "Should not happen");
+ SimpleTest.finish();
+ } catch (e) {
+ // Check that we got the right document
+ window.onmessage = function(event) {
+ is(event.data, "who are you? redirect target",
+ "Should get the message we expect");
+ SimpleTest.finish();
+ }
+
+ frames[0].postMessage("who are you?", "*");
+ }
+ });
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=985135">Mozilla Bug 985135</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<iframe src="file_iframe_sandbox_redirect.html" sandbox="allow-scripts"></iframe>
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_iframe_sandbox_refresh.html b/dom/html/test/test_iframe_sandbox_refresh.html
new file mode 100644
index 0000000000..81107fe3dc
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_refresh.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1156059
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Tests for Bug 1156059</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ // Tests for Bug 1156059
+ // See ok messages in iframes for test cases.
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("We cannot detect when the sandbox blocks the META REFRESH, so we need to allow a reasonable amount of time for them to fail.");
+
+ var testCases = [
+ {
+ desc: "Meta refresh without allow-scripts should be ignored.",
+ numberOfLoads: 0,
+ numberOfLoadsExpected: 1
+ },
+ {
+ desc: "Meta refresh check should be case insensitive.",
+ numberOfLoads: 0,
+ numberOfLoadsExpected: 1
+ },
+ {
+ desc: "Meta refresh with allow-scripts should work.",
+ numberOfLoads: 0,
+ numberOfLoadsExpected: 2
+ },
+ {
+ desc: "Refresh HTTP headers should not be affected by sandbox.",
+ numberOfLoads: 0,
+ numberOfLoadsExpected: 2
+ }
+ ];
+
+ var totalLoads = 0;
+ var totalLoadsExpected = testCases.reduce(function(partialSum, testCase) {
+ return partialSum + testCase.numberOfLoadsExpected;
+ }, 0);
+
+ function processLoad(testCaseIndex) {
+ testCases[testCaseIndex].numberOfLoads++;
+
+ if (++totalLoads == totalLoadsExpected) {
+ // Give the tests that should block the refresh a bit of extra time to
+ // fail. The worst that could happen here is that we get a false pass.
+ window.setTimeout(processResults, 500);
+ }
+ }
+
+ function processResults() {
+ testCases.forEach(function(testCase, index) {
+ var msg = "Test Case " + index + ": " + testCase.desc;
+ is(testCase.numberOfLoads, testCase.numberOfLoadsExpected, msg);
+ });
+
+ SimpleTest.finish();
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1156059">Mozilla Bug 1156059</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+<iframe
+ onload="processLoad(0)"
+ srcdoc="<meta http-equiv='refresh' content='0; url=data:text/html,Refreshed'>"
+ sandbox="allow-forms allow-pointer-lock allow-popups allow-same-origin allow-top-navigation"
+></iframe>
+
+<iframe
+ onload="processLoad(1)"
+ srcdoc="<meta http-equiv='rEfReSh' content='0; url=data:text/html,Refreshed'>"
+ sandbox="allow-forms allow-pointer-lock allow-popups allow-same-origin allow-top-navigation"
+></iframe>
+
+<iframe
+ onload="processLoad(2)"
+ srcdoc="<meta http-equiv='refresh' content='0; url=data:text/html,Refreshed'>"
+ sandbox="allow-scripts"
+></iframe>
+
+<iframe
+ onload="processLoad(3)"
+ src="file_iframe_sandbox_refresh.html"
+ sandbox
+></iframe>
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_iframe_sandbox_same_origin.html b/dom/html/test/test_iframe_sandbox_same_origin.html
new file mode 100644
index 0000000000..b936453bbd
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_same_origin.html
@@ -0,0 +1,108 @@
+\<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=341604
+Implement HTML5 sandbox attribute for IFRAMEs - same origin tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/
+/** Same Origin Tests **/
+
+SimpleTest.waitForExplicitFinish();
+
+var completedTests = 0;
+var passedTests = 0;
+
+function ok_wrapper(result, desc) {
+ ok(result, desc);
+
+ completedTests++;
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (completedTests == 14) {
+ is(passedTests, completedTests, "There are " + completedTests + " same-origin tests that should pass");
+
+ SimpleTest.finish();
+ }
+}
+
+function receiveMessage(event)
+{
+ ok_wrapper(event.data.ok, event.data.desc);
+}
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to communicate pass/fail back to this main page.
+// it expects to be called with an object like {ok: true/false, desc:
+// <description of the test> which it then forwards to ok()
+window.addEventListener("message", receiveMessage);
+
+function doTest() {
+ // 1) test that we can't access an iframe sandboxed without "allow-same-origin"
+ var if_1 = document.getElementById("if_1");
+ try {
+ var b = if_1.contentDocument.body;
+ ok_wrapper(false, "accessing body of a sandboxed document should not be allowed");
+ } catch (err){
+ ok_wrapper(true, "accessing body of a sandboxed document should not be allowed");
+ }
+
+ // 2) test that we can access an iframe sandboxed with "allow-same-origin"
+ var if_2 = document.getElementById("if_2");
+
+ try {
+ var b = if_2.contentDocument.body;
+ ok_wrapper(true, "accessing body of a sandboxed document with allow-same-origin should be allowed");
+ } catch (err) {
+ ok_wrapper(false, "accessing body of a sandboxed document with allow-same-origin should be allowed");
+ }
+
+ // 3) test that a sandboxed iframe without 'allow-same-origin' cannot access its parent
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+
+ // 4) test that a sandboxed iframe with 'allow-same-origin' can access its parent
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 5) check that a sandboxed iframe with "allow-same-origin" can access document.cookie
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 6) check that a sandboxed iframe with "allow-same-origin" can access window.localStorage
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 7) check that a sandboxed iframe with "allow-same-origin" can access window.sessionStorage
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 8) check that a sandboxed iframe WITHOUT "allow-same-origin" can NOT access document.cookie
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+
+ // 9) check that a sandboxed iframe WITHOUT "allow-same-origin" can NOT access window.localStorage
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+
+ // 10) check that a sandboxed iframe WITHOUT "allow-same-origin" can NOT access window.sessionStorage
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+
+ // 11) check that XHR works normally in a sandboxed iframe with "allow-same-origin" and "allow-scripts"
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 12) check that XHR is blocked in a sandboxed iframe with "allow-scripts" but WITHOUT "allow-same-origin"
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+}
+addLoadEvent(doTest);
+</script>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs
+<p id="display"></p>
+<div id="content">
+<iframe sandbox="" id="if_1" src="file_iframe_sandbox_b_if1.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin allow-scripts" id="if_2" src="file_iframe_sandbox_b_if2.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_3" src="file_iframe_sandbox_b_if3.html" height="10" width="10"></iframe>
+</div>
diff --git a/dom/html/test/test_iframe_sandbox_workers.html b/dom/html/test/test_iframe_sandbox_workers.html
new file mode 100644
index 0000000000..c86f2ab528
--- /dev/null
+++ b/dom/html/test/test_iframe_sandbox_workers.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=341604
+Implement HTML5 sandbox attribute for IFRAMEs - tests for workers
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs - test for workers **/
+
+SimpleTest.waitForExplicitFinish();
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to communicate pass/fail back to this main page.
+// it expects to be called with an object like {ok: true/false, desc:
+// <description of the test> which it then forwards to ok()
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event)
+{
+ ok_wrapper(event.data.ok, event.data.desc);
+}
+
+var completedTests = 0;
+var passedTests = 0;
+
+function ok_wrapper(result, desc) {
+ ok(result, desc);
+
+ completedTests++;
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (completedTests == 3) {
+ is(passedTests, 3, "There are 3 worker tests that should pass");
+ SimpleTest.finish();
+ }
+}
+
+function doTest() {
+ // passes if good
+ // 1) test that a worker in a sandboxed iframe with 'allow-scripts' can be loaded
+ // from a data: URI
+ // (done by file_iframe_sandbox_g_if1.html)
+
+ // passes if good
+ // 2) test that a worker in a sandboxed iframe with 'allow-scripts' can be loaded
+ // from a blob URI created by the sandboxed document itself
+ // (done by file_iframe_sandbox_g_if1.html)
+
+ // passes if good
+ // 3) test that a worker in a sandboxed iframe with 'allow-scripts' without
+ // 'allow-same-origin' cannot load a script via a relative URI
+ // (done by file_iframe_sandbox_g_if1.html)
+}
+
+addLoadEvent(doTest);
+</script>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs
+<p id="display"></p>
+<div id="content">
+<iframe sandbox="allow-scripts" id="if_1" src="file_iframe_sandbox_g_if1.html" height="10" width="10"></iframe>
+</div>
+</body>
+</html>
diff --git a/dom/html/test/test_imageSrcSet.html b/dom/html/test/test_imageSrcSet.html
new file mode 100644
index 0000000000..695d1c2643
--- /dev/null
+++ b/dom/html/test/test_imageSrcSet.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=980243
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 980243</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 980243 **/
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function() {
+ var img = document.querySelector("img");
+ img.onload = function() {
+ ok(true, "Reached here");
+ SimpleTest.finish();
+ }
+ // If ths spec ever changes to treat .src sets differently from
+ // setAttribute("src"), we'll need some sort of canonicalization step
+ // earlier to make the attr value an absolute URI.
+ img.setAttribute("src", img.getAttribute("src"));
+ });
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=980243">Mozilla Bug 980243</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <img src="file_formSubmission_img.jpg">
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_image_clone_load.html b/dom/html/test/test_image_clone_load.html
new file mode 100644
index 0000000000..e808c80a53
--- /dev/null
+++ b/dom/html/test/test_image_clone_load.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for image clones doing their load</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+var t = async_test("The clone of an image should do the load of the same image, and do it synchronously");
+t.step(function() {
+ var img = new Image();
+ img.onload = t.step_func(function() {
+ var clone = img.cloneNode();
+ assert_not_equals(img.naturalWidth, 0, "Should have a width");
+ assert_equals(clone.naturalWidth, img.naturalWidth,
+ "Clone should have a width too");
+ // And make sure the clone fires onload too, which happens async.
+ clone.onload = function() { t.done() }
+ });
+ img.src = "image.png";
+});
+</script>
diff --git a/dom/html/test/test_img_attributes_reflection.html b/dom/html/test/test_img_attributes_reflection.html
new file mode 100644
index 0000000000..b89b4cec05
--- /dev/null
+++ b/dom/html/test/test_img_attributes_reflection.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLImageElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for HTMLImageElement attributes reflection **/
+
+reflectString({
+ element: document.createElement("img"),
+ attribute: "alt",
+})
+
+reflectURL({
+ element: document.createElement("img"),
+ attribute: "src",
+})
+
+reflectString({
+ element: document.createElement("img"),
+ attribute: "srcset",
+})
+
+reflectLimitedEnumerated({
+ element: document.createElement("img"),
+ attribute: "crossOrigin",
+ // "" is a valid value per spec, but gets mapped to the "anonymous" state,
+ // just like invalid values, so just list it under invalidValues
+ validValues: [ "anonymous", "use-credentials" ],
+ invalidValues: [
+ "", " aNOnYmous ", " UsE-CreDEntIALS ", "foobar", "FOOBAR", " fOoBaR "
+ ],
+ defaultValue: { invalid: "anonymous", missing: null },
+ nullable: true,
+})
+
+reflectString({
+ element: document.createElement("img"),
+ attribute: "useMap",
+})
+
+reflectBoolean({
+ element: document.createElement("img"),
+ attribute: "isMap",
+})
+
+ok("width" in document.createElement("img"), "img.width is present")
+ok("height" in document.createElement("img"), "img.height is present")
+ok("naturalWidth" in document.createElement("img"), "img.naturalWidth is present")
+ok("naturalHeight" in document.createElement("img"), "img.naturalHeight is present")
+ok("complete" in document.createElement("img"), "img.complete is present")
+
+reflectString({
+ element: document.createElement("img"),
+ attribute: "name",
+})
+
+reflectString({
+ element: document.createElement("img"),
+ attribute: "align",
+})
+
+reflectUnsignedInt({
+ element: document.createElement("img"),
+ attribute: "hspace",
+})
+
+reflectUnsignedInt({
+ element: document.createElement("img"),
+ attribute: "vspace",
+})
+
+reflectURL({
+ element: document.createElement("img"),
+ attribute: "longDesc",
+})
+
+reflectString({
+ element: document.createElement("img"),
+ attribute: "border",
+ extendedAttributes: { TreatNullAs: "EmptyString" },
+})
+
+reflectURL({
+ element: document.createElement("img"),
+ attribute: "lowsrc",
+})
+
+ok("x" in document.createElement("img"), "img.x is present")
+ok("y" in document.createElement("img"), "img.y is present")
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_input_file_cancel_event.html b/dom/html/test/test_input_file_cancel_event.html
new file mode 100644
index 0000000000..f0fd81c433
--- /dev/null
+++ b/dom/html/test/test_input_file_cancel_event.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for the input type=file cancel event</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<input type=file></input>
+
+<script>
+SimpleTest.waitForExplicitFinish();
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+MockFilePicker.useBlobFile();
+MockFilePicker.returnValue = MockFilePicker.returnCancel;
+
+let input = document.querySelector('input[type=file]');
+input.addEventListener('cancel', event => {
+ ok(true, "cancel event correctly sent");
+
+ is(event.target, input, "Has correct event target");
+ is(event.isTrusted, true, "Event is trusted");
+ is(event.bubbles, true, "Event bubbles");
+ is(event.cancelable, false, "Event is not cancelable");
+ is(event.composed, false, "Event is not composed");
+
+ SimpleTest.executeSoon(function() {
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+ });
+});
+input.addEventListener('change' , () => {
+ ok(false, "unexpected change event");
+})
+input.click();
+</script>
+</body>
+</html>
+
diff --git a/dom/html/test/test_input_files_not_nsIFile.html b/dom/html/test/test_input_files_not_nsIFile.html
new file mode 100644
index 0000000000..e70bc093ee
--- /dev/null
+++ b/dom/html/test/test_input_files_not_nsIFile.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for &lt;input type='file'&gt; handling when its "files" do not implement nsIFile</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<div id="content">
+ <input id='a' type='file'>
+</div>
+<button id='b' onclick="document.getElementById('a').click();">Show Filepicker</button>
+
+<input type="file" id="file" />
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+SimpleTest.waitForFocus(function() {
+ MockFilePicker.useBlobFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ var b = document.getElementById('b');
+ b.focus(); // Be sure the element is visible.
+
+ document.getElementById('a').addEventListener("change", function(aEvent) {
+ ok(true, "change event correctly sent");
+
+ SimpleTest.executeSoon(function() {
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+ });
+ });
+
+ b.click();
+});
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/html/test/test_input_lastInteractiveValue.html b/dom/html/test/test_input_lastInteractiveValue.html
new file mode 100644
index 0000000000..6ac29edaef
--- /dev/null
+++ b/dom/html/test/test_input_lastInteractiveValue.html
@@ -0,0 +1,134 @@
+<!doctype html>
+<title>Test for HTMLInputElement.lastInteractiveValue</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<script src="/tests/SimpleTest/NativeKeyCodes.js"></script>
+<link href="/tests/SimpleTest/test.css"/>
+<body>
+<script>
+const kIsMac = navigator.platform.indexOf("Mac") > -1;
+const kIsWin = navigator.platform.indexOf("Win") > -1;
+
+function getFreshInput() {
+ let input = document.body.appendChild(document.createElement("input"));
+ input.focus();
+ return input;
+}
+
+// XXX This should be add_setup, but bug 1776589
+add_task(async function ensure_focus() {
+ await SimpleTest.promiseFocus(window);
+});
+
+add_task(async function simple() {
+ let input = getFreshInput();
+
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "", "Initial state");
+
+ sendString("abc");
+
+ is(input.value, "abc", ".value after interactive edit");
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after interactive edit");
+
+ input.value = "muahahaha";
+ is(input.value, "muahahaha", ".value after script edit");
+
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after script edit");
+});
+
+add_task(async function test_default_value() {
+ let input = getFreshInput();
+ input.defaultValue = "default value";
+
+ is(input.value, "default value", ".defaultValue affects .value");
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "", "Default value is not interactive");
+
+ sendString("abc");
+
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "default valueabc", "After interaction with default value");
+});
+
+// This happens in imdb.com login form.
+add_task(async function clone_after_interactive_edit() {
+ let input = getFreshInput();
+
+ sendString("abc");
+
+ is(input.value, "abc", ".value after interactive edit");
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after interactive edit");
+
+ let clone = input.cloneNode(true);
+ is(clone.value, "abc", ".value after clone");
+ is(SpecialPowers.wrap(clone).lastInteractiveValue, "abc", ".lastInteractiveValue after clone");
+
+ clone.type = "hidden";
+
+ clone.value = "something random";
+ is(SpecialPowers.wrap(clone).lastInteractiveValue, "", ".lastInteractiveValue after clone in non-text-input");
+});
+
+add_task(async function set_user_input() {
+ let input = getFreshInput();
+
+ input.value = "";
+
+ SpecialPowers.wrap(input).setUserInput("abc");
+
+ is(input.value, "abc", ".value after setUserInput edit");
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after setUserInput");
+
+ input.value = "foobar";
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after script edit after setUserInput");
+});
+
+
+// TODO(emilio): Maybe execCommand shouldn't be considered interactive, but it
+// matches pre-existing behavior effectively.
+add_task(async function exec_command() {
+ let input = getFreshInput();
+
+ document.execCommand("insertText", false, "a");
+
+ is(input.value, "a", ".value after execCommand edit");
+
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "a", ".lastInteractiveValue after execCommand");
+
+ input.value = "foobar";
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "a", ".lastInteractiveValue after script edit after execCommand");
+});
+
+add_task(async function cut_paste() {
+ if (true) {
+ // TODO: the above condition should be if (!kIsMac && !kIsWin), but this
+ // fails (intermittently?) in those platforms, see bug 1776838. Disable for
+ // now.
+ todo(false, "synthesizeNativeKey doesn't work elsewhere (yet)");
+ return;
+ }
+
+ function doSynthesizeNativeKey(keyCode, modifiers, chars) {
+ return new Promise((resolve, reject) => {
+ if (!synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, keyCode, modifiers, chars, chars, resolve)) {
+ reject(new Error("Couldn't synthesize native key"));
+ }
+ });
+ }
+
+ let input = getFreshInput();
+
+ sendString("abc");
+
+ input.select();
+
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue before cut");
+
+ await doSynthesizeNativeKey(kIsMac ? MAC_VK_ANSI_X : WIN_VK_X, { accelKey: true }, "x");
+
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "", ".lastInteractiveValue after cut");
+
+ await doSynthesizeNativeKey(kIsMac ? MAC_VK_ANSI_V : WIN_VK_V, { accelKey: true }, "v");
+
+ is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after paste");
+});
+
+</script>
diff --git a/dom/html/test/test_inputmode.html b/dom/html/test/test_inputmode.html
new file mode 100644
index 0000000000..56bb101e8a
--- /dev/null
+++ b/dom/html/test/test_inputmode.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Tests for inputmode attribute</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/SpecialPowers.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<div>
+<input id="a1" inputmode="none">
+<input id="a2" inputmode="text">
+<input id="a3" inputmode="tel">
+<input id="a4" inputmode="url">
+<input id="a5" inputmode="email">
+<input id="a6" inputmode="numeric">
+<input id="a7" inputmode="decimal">
+<input id="a8" inputmode="search">
+<input id="a9">
+<input id="a10" type="number" inputmode="numeric">
+<input id="a11" type="date" inputmode="numeric">
+<input id="a12" type="time" inputmode="numeric">
+<textarea id="b1" inputmode="none"></textarea>
+<textarea id="b2" inputmode="text"></textarea>
+<textarea id="b3" inputmode="tel"></textarea>
+<textarea id="b4" inputmode="url"></textarea>
+<textarea id="b5" inputmode="email"></textarea>
+<textarea id="b6" inputmode="numeric"></textarea>
+<textarea id="b7" inputmode="decimal"></textarea>
+<textarea id="b8" inputmode="search"></textarea>
+<textarea id="b9"></textarea>
+<div contenteditable id="c1" inputmode="none"><span>c1</span></div>
+<div contenteditable id="c2" inputmode="text"><span>c2</span></div>
+<div contenteditable id="c3" inputmode="tel"><span>c3</span></div>
+<div contenteditable id="c4" inputmode="url"><span>c4</span></div>
+<div contenteditable id="c5" inputmode="email"><span>c5</span></div>
+<div contenteditable id="c6" inputmode="numeric"><span>c6</span></div>
+<div contenteditable id="c7" inputmode="decimal"><span>c7</span></div>
+<div contenteditable id="c8" inputmode="search"><span>c8</span></div>
+<div contenteditable id="c9"><span>c9</span></div>
+<input id="d1" inputmode="URL"> <!-- no lowercase -->
+</div>
+<pre id="test">
+<script class=testbody" type="application/javascript">
+// eslint-disable-next-line mozilla/no-addtask-setup
+add_task(async function setup() {
+ await new Promise(r => SimpleTest.waitForFocus(r));
+});
+
+add_task(async function basic() {
+ const tests = [
+ { id: "a1", inputmode: "none", desc: "inputmode of input element is none" },
+ { id: "a2", inputmode: "text", desc: "inputmode of input element is text" },
+ { id: "a3", inputmode: "tel", desc: "inputmode of input element is tel" },
+ { id: "a4", inputmode: "url", desc: "inputmode of input element is url" },
+ { id: "a5", inputmode: "email", desc: "inputmode of input element is email" },
+ { id: "a6", inputmode: "numeric", desc: "inputmode of input element is numeric" },
+ { id: "a7", inputmode: "decimal", desc: "inputmode of input element is decimal" },
+ { id: "a8", inputmode: "search", desc: "inputmode of input element is search" },
+ { id: "a9", inputmode: "", desc: "no inputmode of input element" },
+ { id: "a10", inputmode: "numeric", desc: "inputmode of input type=number is numeric" },
+ { id: "a11", inputmode: "", desc: "no inputmode due to type=date" },
+ { id: "a12", inputmode: "", desc: "no inputmode due to type=time" },
+ { id: "b1", inputmode: "none", desc: "inputmode of textarea element is none" },
+ { id: "b2", inputmode: "text", desc: "inputmode of textarea element is text" },
+ { id: "b3", inputmode: "tel", desc: "inputmode of textarea element is tel" },
+ { id: "b4", inputmode: "url", desc: "inputmode of textarea element is url" },
+ { id: "b5", inputmode: "email", desc: "inputmode of textarea element is email" },
+ { id: "b6", inputmode: "numeric", desc: "inputmode of textarea element is numeric" },
+ { id: "b7", inputmode: "decimal", desc: "inputmode of textarea element is decimal" },
+ { id: "b8", inputmode: "search", desc: "inputmode of textarea element is search" },
+ { id: "b9", inputmode: "", desc: "no inputmode of textarea element" },
+ { id: "c1", inputmode: "none", desc: "inputmode of contenteditable is none" },
+ { id: "c2", inputmode: "text", desc: "inputmode of contenteditable is text" },
+ { id: "c3", inputmode: "tel", desc: "inputmode of contentedtiable is tel" },
+ { id: "c4", inputmode: "url", desc: "inputmode of contentedtiable is url" },
+ { id: "c5", inputmode: "email", desc: "inputmode of contentedtable is email" },
+ { id: "c6", inputmode: "numeric", desc: "inputmode of contenteditable is numeric" },
+ { id: "c7", inputmode: "decimal", desc: "inputmode of contenteditable is decimal" },
+ { id: "c8", inputmode: "search", desc: "inputmode of contenteditable is search" },
+ { id: "c9", inputmode: "", desc: "no inputmode of contentedtiable" },
+ { id: "d1", inputmode: "url", desc: "inputmode of input element is URL" },
+ ];
+
+ for (let test of tests) {
+ let element = document.getElementById(test.id);
+ if (element.tagName == "DIV") {
+ // Set caret to text node in contenteditable
+ window.getSelection().removeAllRanges();
+ let range = document.createRange();
+ range.setStart(element.firstChild.firstChild, 1);
+ range.setEnd(element.firstChild.firstChild, 1);
+ window.getSelection().addRange(range);
+ } else {
+ // input and textarea element
+ element.focus();
+ }
+ is(SpecialPowers.DOMWindowUtils.focusedInputMode, test.inputmode, test.desc);
+ }
+});
+
+add_task(async function dynamicChange() {
+ const tests = ["a3", "b3", "c3"];
+ for (let test of tests) {
+ let element = document.getElementById(test);
+ element.focus();
+ is(SpecialPowers.DOMWindowUtils.focusedInputMode, "tel", "Initial inputmode");
+ element.inputMode = "url";
+ is(SpecialPowers.DOMWindowUtils.focusedInputMode, "url",
+ "inputmode in InputContext has to sync with current inputMode property");
+ element.setAttribute("inputmode", "decimal");
+ is(SpecialPowers.DOMWindowUtils.focusedInputMode, "decimal",
+ "inputmode in InputContext has to sync with current inputmode attribute");
+ // Storing the original value may be safer.
+ element.inputMode = "tel";
+ }
+
+ let element = document.getElementById("a3");
+ element.focus();
+ is(SpecialPowers.DOMWindowUtils.focusedInputMode, "tel", "Initial inputmode");
+ document.getElementById("a4").inputMode = "email";
+ is(SpecialPowers.DOMWindowUtils.focusedInputMode, "tel",
+ "inputmode in InputContext keeps focused inputmode value");
+ // Storing the original value may be safer.
+ document.getElementById("a4").inputMode = "url";
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_li_attributes_reflection.html b/dom/html/test/test_li_attributes_reflection.html
new file mode 100644
index 0000000000..fd6795226b
--- /dev/null
+++ b/dom/html/test/test_li_attributes_reflection.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLLIElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLLIElement attributes reflection **/
+
+// .value
+reflectInt({
+ element: document.createElement("li"),
+ attribute: "value",
+ nonNegative: false,
+});
+
+// .type
+reflectString({
+ element: document.createElement("li"),
+ attribute: "type"
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_link_attributes_reflection.html b/dom/html/test/test_link_attributes_reflection.html
new file mode 100644
index 0000000000..c75c9e2572
--- /dev/null
+++ b/dom/html/test/test_link_attributes_reflection.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLLinkElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLLinkElement attributes reflection **/
+
+// .href (URL)
+reflectURL({
+ element: document.createElement("link"),
+ attribute: "href",
+});
+
+// .crossOrigin (String or null)
+reflectLimitedEnumerated({
+ element: document.createElement("link"),
+ attribute: "crossOrigin",
+ // "" is a valid value per spec, but gets mapped to the "anonymous" state,
+ // just like invalid values, so just list it under invalidValues
+ validValues: [ "anonymous", "use-credentials" ],
+ invalidValues: [
+ "", " aNOnYmous ", " UsE-CreDEntIALS ", "foobar", "FOOBAR", " fOoBaR "
+ ],
+ defaultValue: { invalid: "anonymous", missing: null },
+ nullable: true,
+})
+
+// .rel (String)
+reflectString({
+ element: document.createElement("link"),
+ attribute: "rel",
+});
+
+// .media (String)
+reflectString({
+ element: document.createElement("link"),
+ attribute: "media",
+});
+
+// .hreflang (String)
+reflectString({
+ element: document.createElement("link"),
+ attribute: "hreflang",
+});
+
+// .type (String)
+reflectString({
+ element: document.createElement("link"),
+ attribute: "type",
+});
+
+
+// .charset (String)
+reflectString({
+ element: document.createElement("link"),
+ attribute: "charset",
+});
+
+// .rev (String)
+reflectString({
+ element: document.createElement("link"),
+ attribute: "rev",
+});
+
+// .target (String)
+reflectString({
+ element: document.createElement("link"),
+ attribute: "target",
+});
+
+// .as (String)
+reflectLimitedEnumerated({
+ element: document.createElement("link"),
+ attribute: "as",
+ validValues: [ "fetch", "audio", "font", "image", "script", "style", "track", "video" ],
+ invalidValues: [
+ "", "audi", "doc", "Emb", "foobar", "FOOBAR", " fOoBaR ", "OBJ", "document", "embed", "manifest", "object", "report", "serviceworker", "sharedworker", "worker", "xslt"
+ ],
+ defaultValue: { invalid: "", missing: "" },
+ nullable: false,
+})
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_link_sizes.html b/dom/html/test/test_link_sizes.html
new file mode 100644
index 0000000000..b242748886
--- /dev/null
+++ b/dom/html/test/test_link_sizes.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<html>
+<head>
+<title>Test link.sizes attribute</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" sizes="16x16 24x24 32x32 48x48">
+</head>
+<body>
+
+<pre id="test">
+<script>
+
+ var links = document.getElementsByTagName('link');
+ for (var i = 0; i < links.length; ++i) {
+ var link = links[i];
+ ok("sizes" in link, "link.sizes exists");
+
+ if (link.rel == 'shortcut icon') {
+ is(link.sizes.value, "16x16 24x24 32x32 48x48", 'link.sizes.value correct value');
+ is(link.sizes.length, 4, 'link.sizes.length correct value');
+ ok(link.sizes.contains('32x32'), 'link.sizes.contains() works');
+ link.sizes.add('64x64');
+ is(link.sizes.length, 5, 'link.sizes.length correct value');
+ link.sizes.remove('64x64');
+ is(link.sizes.length, 4, 'link.sizes.length correct value');
+ is(link.sizes + "", "16x16 24x24 32x32 48x48", 'link.sizes stringify correct value');
+ } else {
+ is(link.sizes.value, "", 'link.sizes correct value');
+ }
+ }
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_map_attributes_reflection.html b/dom/html/test/test_map_attributes_reflection.html
new file mode 100644
index 0000000000..8835fb29d2
--- /dev/null
+++ b/dom/html/test/test_map_attributes_reflection.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLMapElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLMapElement attributes reflection **/
+
+// .name (String)
+reflectString({
+ element: document.createElement("map"),
+ attribute: "name",
+})
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_meta_attributes_reflection.html b/dom/html/test/test_meta_attributes_reflection.html
new file mode 100644
index 0000000000..e0cf0c347d
--- /dev/null
+++ b/dom/html/test/test_meta_attributes_reflection.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLMetaElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLMetaElement attributes reflection **/
+
+// .name (String)
+reflectString({
+ element: document.createElement("meta"),
+ attribute: "name",
+})
+
+// .httpEquiv (String)
+reflectString({
+ element: document.createElement("meta"),
+ attribute: { content: "http-equiv", idl: "httpEquiv" },
+})
+
+// .content (String)
+reflectString({
+ element: document.createElement("meta"),
+ attribute: "content",
+})
+
+// .scheme (String)
+reflectString({
+ element: document.createElement("meta"),
+ attribute: "scheme",
+})
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_mod_attributes_reflection.html b/dom/html/test/test_mod_attributes_reflection.html
new file mode 100644
index 0000000000..0efa7c52bf
--- /dev/null
+++ b/dom/html/test/test_mod_attributes_reflection.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLModElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLModElement attributes reflection **/
+
+// .cite (URL)
+reflectURL({
+ element: document.createElement("ins"),
+ attribute: "cite",
+})
+reflectURL({
+ element: document.createElement("del"),
+ attribute: "cite",
+})
+
+// .dateTime (String)
+reflectString({
+ element: document.createElement("ins"),
+ attribute: "dateTime",
+})
+reflectString({
+ element: document.createElement("del"),
+ attribute: "dateTime",
+})
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_multipleFilePicker.html b/dom/html/test/test_multipleFilePicker.html
new file mode 100644
index 0000000000..c4a71151aa
--- /dev/null
+++ b/dom/html/test/test_multipleFilePicker.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for single filepicker per event</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <div id='foo'><a href='#'>Click here to test this issue</a></div>
+ <script>
+
+SimpleTest.requestFlakyTimeout("Timeouts are needed to simulate user-interaction");
+SimpleTest.waitForExplicitFinish();
+
+let clickCount = 0;
+let foo = document.getElementById('foo');
+foo.addEventListener('click', _ => {
+ if (++clickCount < 10) {
+ let input = document.createElement('input');
+ input.type = 'file';
+ foo.appendChild(input);
+ input.click();
+ }
+});
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+let pickerCount = 0;
+
+SpecialPowers.pushPrefEnv({
+ set: [["dom.disable_open_during_load", true]],
+})
+// Let's do the first click.
+.then(() => {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = function(filepicker) {
+ ++pickerCount;
+ resolve();
+ }
+ setTimeout(_ => {
+ is(pickerCount, 0, "No file picker initially");
+ synthesizeMouseAtCenter(foo, {});
+ }, 0);
+ })
+})
+
+// Let's wait a bit more, then let's do a click.
+.then(() => {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = function(filepicker) {
+ ++pickerCount;
+ resolve();
+ }
+
+ setTimeout(() => {
+ is(pickerCount, 1, "Only 1 file picker");
+ is(clickCount, 10, "10 clicks triggered");
+ clickCount = 0;
+ pickerCount = 0;
+ synthesizeMouseAtCenter(foo, {});
+ }, 1000);
+ });
+})
+
+// Another click...
+.then(_ => {
+ setTimeout(() => {
+ is(pickerCount, 1, "Only 1 file picker");
+ is(clickCount, 10, "10 clicks triggered");
+ MockFilePicker.cleanup();
+ SimpleTest.finish();
+ }, 1000);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_named_options.html b/dom/html/test/test_named_options.html
new file mode 100644
index 0000000000..8c38425240
--- /dev/null
+++ b/dom/html/test/test_named_options.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=772869
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 772869</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772869">Mozilla Bug 772869</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <select id="s">
+ <option name="x"></option>
+ <option name="y" id="z"></option>
+ <option name="z" id="x"></option>
+ <option id="w"></option>
+ </select>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 772869 **/
+var opt = $("s").options;
+opt.loopy = "something"
+var names = Object.getOwnPropertyNames(opt);
+is(names.length, 9, "Should have nine entries");
+is(names[0], "0", "Entry 1")
+is(names[1], "1", "Entry 2")
+is(names[2], "2", "Entry 3")
+is(names[3], "3", "Entry 4")
+is(names[4], "x", "Entry 5")
+is(names[5], "y", "Entry 6")
+is(names[6], "z", "Entry 7")
+is(names[7], "w", "Entry 8")
+is(names[8], "loopy", "Entry 9")
+
+var names2 = [];
+for (var name in opt) {
+ names2.push(name);
+}
+is(names2.length, 11, "Should have eleven enumerated names");
+is(names2[0], "0", "Enum entry 1")
+is(names2[1], "1", "Enum entry 2")
+is(names2[2], "2", "Enum entry 3")
+is(names2[3], "3", "Enum entry 4")
+is(names2[4], "loopy", "Enum entry 5")
+is(names2[5], "add", "Enum entrry 6")
+is(names2[6], "remove", "Enum entry 7")
+is(names2[7], "length", "Enum entry 8")
+is(names2[8], "selectedIndex", "Enum entry 9")
+is(names2[9], "item", "Enum entry 10")
+is(names2[10], "namedItem", "Enum entry 11")
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_nested_invalid_fieldsets.html b/dom/html/test/test_nested_invalid_fieldsets.html
new file mode 100644
index 0000000000..7c00693697
--- /dev/null
+++ b/dom/html/test/test_nested_invalid_fieldsets.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=914029
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 914029</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 914029 **/
+
+ var innerFieldset = document.createElement("fieldset");
+ var outerFieldset = document.createElement("fieldset");
+ var textarea = document.createElement("textarea");
+ textarea.setAttribute("required", "");
+ innerFieldset.appendChild(textarea);
+ outerFieldset.appendChild(innerFieldset);
+ SpecialPowers.forceGC();
+ ok(true, "This page did not crash - dynamically added nested invalid fieldsets" +
+ " work correctly.");
+ var innerFieldset = document.createElement("fieldset");
+ var outerFieldset = document.createElement("fieldset");
+ var textarea = document.createElement("textarea");
+ var textarea2 = document.createElement("textarea");
+ textarea.setAttribute("required", "");
+ innerFieldset.appendChild(textarea);
+ innerFieldset.appendChild(textarea2);
+ outerFieldset.appendChild(innerFieldset);
+ SpecialPowers.forceGC();
+ ok(true, "This page did not crash - dynamically added nested invalid fieldsets" +
+ " work correctly.");
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=914029">Mozilla Bug 914029</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_nestediframe.html b/dom/html/test/test_nestediframe.html
new file mode 100644
index 0000000000..ddbf0ca9dc
--- /dev/null
+++ b/dom/html/test/test_nestediframe.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for same URLs nested iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ test_nestediframe body
+<script>
+
+SimpleTest.waitForExplicitFinish();
+
+function reportState(msg) {
+ if (location.href.includes("#")) {
+ parent.postMessage(msg, "*");
+ return;
+ }
+
+ if (msg == "OK 1") {
+ ok(true, "First frame loaded");
+ } else if (msg == "KO 2") {
+ ok(true, "Second frame load failed");
+ SimpleTest.finish();
+ } else {
+ ok(false, "Unknown message: " + msg);
+ }
+}
+
+addEventListener("message", event => {
+ reportState(event.data);
+});
+
+var recursion;
+if (!location.href.includes("#")) {
+ recursion = 1;
+} else {
+ recursion = parseInt(location.href.split("#")[1]) + 1;
+}
+
+var ifr = document.createElement('iframe');
+ifr.src = location.href.split("#")[0] + "#" + recursion;
+
+ifr.onload = function() {
+ reportState("OK " + recursion);
+}
+ifr.onerror = function() {
+ reportState("KO " + recursion);
+}
+
+document.body.appendChild(ifr);
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_non-ascii-cookie.html b/dom/html/test/test_non-ascii-cookie.html
new file mode 100644
index 0000000000..a15923f39d
--- /dev/null
+++ b/dom/html/test/test_non-ascii-cookie.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=784367
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for non-ASCII cookie values</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=784367">Mozilla Bug 784367</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for non-ASCII cookie values **/
+
+SimpleTest.waitForExplicitFinish();
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("file_cookiemanager.js"));
+
+function getCookieFromManager() {
+ return new Promise(resolve => {
+ gScript.addMessageListener("getCookieFromManager:return", function gcfm({ cookie }) {
+ gScript.removeMessageListener("getCookieFromManager:return", gcfm);
+ resolve(cookie);
+ });
+ gScript.sendAsyncMessage("getCookieFromManager", { host: location.hostname, path: location.pathname });
+ });
+}
+
+SpecialPowers.pushPrefEnv({
+ "set": [
+ // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+ ["network.cookie.sameSite.laxByDefault", false],
+ ]
+}, () => {
+ var c = document.cookie;
+ is(document.cookie, 'abc=012©ABC\ufffdDEF', "document.cookie should be decoded as UTF-8");
+
+ var newCookie;
+
+ getCookieFromManager().then((cookie) => {
+ is(cookie, document.cookie, "nsICookieManager should be consistent with document.cookie");
+ newCookie = 'def=∼≩≭≧∯≳≲≣∽≸≸∺≸∠≯≮≥≲≲≯≲∽≡≬≥≲≴∨∱∩∾';
+ document.cookie = newCookie;
+ is(document.cookie, c + '; ' + newCookie, "document.cookie should be encoded as UTF-8");
+
+ return getCookieFromManager();
+ }).then((cookie) => {
+ is(cookie, document.cookie, "nsICookieManager should be consistent with document.cookie");
+ var date1 = new Date();
+ date1.setTime(0);
+ document.cookie = newCookie + 'def=;expires=' + date1.toGMTString();
+ gScript.destroy();
+ SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault");
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_non-ascii-cookie.html^headers^ b/dom/html/test/test_non-ascii-cookie.html^headers^
new file mode 100644
index 0000000000..54aa6c3e72
--- /dev/null
+++ b/dom/html/test/test_non-ascii-cookie.html^headers^
@@ -0,0 +1 @@
+Set-Cookie: abc=012©ABC©DEF
diff --git a/dom/html/test/test_object_attributes_reflection.html b/dom/html/test/test_object_attributes_reflection.html
new file mode 100644
index 0000000000..d55183db07
--- /dev/null
+++ b/dom/html/test/test_object_attributes_reflection.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLObjectElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLObjectElement attributes reflection **/
+
+// .data (URL)
+reflectURL({
+ element: document.createElement("object"),
+ attribute: "data",
+});
+
+// .type (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "type",
+});
+
+// .name (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "name",
+});
+
+// .useMap (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "useMap",
+});
+
+// .width (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "width",
+});
+
+// .height (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "height",
+});
+
+// .align (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "align",
+});
+
+// .archive (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "archive",
+});
+
+// .code (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "code",
+});
+
+// .declare (String)
+reflectBoolean({
+ element: document.createElement("object"),
+ attribute: "declare",
+});
+
+// .hspace (unsigned int)
+reflectUnsignedInt({
+ element: document.createElement("object"),
+ attribute: "hspace",
+});
+
+// .standby (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "standby",
+});
+
+// .vspace (unsigned int)
+reflectUnsignedInt({
+ element: document.createElement("object"),
+ attribute: "vspace",
+});
+
+// .codeBase (URL)
+reflectURL({
+ element: document.createElement("object"),
+ attribute: "codeBase",
+});
+
+// .codeType (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "codeType",
+});
+
+// .border (String)
+reflectString({
+ element: document.createElement("object"),
+ attribute: "border",
+ extendedAttributes: { TreatNullAs: "EmptyString" },
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_ol_attributes_reflection.html b/dom/html/test/test_ol_attributes_reflection.html
new file mode 100644
index 0000000000..a941914077
--- /dev/null
+++ b/dom/html/test/test_ol_attributes_reflection.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLOLElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLOLElement attributes reflection **/
+
+// .reversed (boolean)
+reflectBoolean({
+ element: document.createElement("ol"),
+ attribute: "reversed",
+})
+
+// .start
+reflectInt({
+ element: document.createElement("ol"),
+ attribute: "start",
+ nonNegative: false,
+ defaultValue: 1,
+});
+
+// .type
+reflectString({
+ element: document.createElement("ol"),
+ attribute: "type"
+});
+
+// .compact
+reflectBoolean({
+ element: document.createElement("ol"),
+ attribute: "compact",
+})
+
+// Additional tests for ol.start behavior when li elements are added
+var ol = document.createElement("ol");
+var li = document.createElement("li");
+li.value = 42;
+ol.appendChild(li);
+is(ol.start, 1, "ol.start with one li child, li.value = 42:");
+li.value = -42;
+is(ol.start, 1, "ol.start with one li child, li.value = 42:");
+ol.removeAttribute("start");
+li.removeAttribute("value");
+ol.appendChild(document.createElement("li"));
+ol.reversed = true;
+todo_is(ol.start, 2, "ol.start with two li children, ol.reversed == true:");
+li.value = 42;
+todo_is(ol.start, 2, "ol.start with two li childern, ol.reversed == true:");
+ol.start = 42;
+is(ol.start, 42, "ol.start = 42:");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_option_defaultSelected.html b/dom/html/test/test_option_defaultSelected.html
new file mode 100644
index 0000000000..6d999c0b29
--- /dev/null
+++ b/dom/html/test/test_option_defaultSelected.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=927796
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 927796</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=927796">Mozilla Bug 927796</a>
+<p id="display">
+<select id="s1">
+ <option selected>one</option>
+ <option>two</option>
+</select>
+<select id="s2" size="5">
+ <option selected>one</option>
+ <option>two</option>
+</select>
+</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+ <script type="application/javascript">
+
+ /** Test for Bug 927796 **/
+ var s1 = $("s1");
+ s1.options[0].defaultSelected = false;
+ is(s1.options[0].selected, true,
+ "First option in combobox should still be selected");
+ is(s1.options[1].selected, false,
+ "Second option in combobox should not be selected");
+
+ var s2 = $("s2");
+ s2.options[0].defaultSelected = false;
+ is(s2.options[0].selected, false,
+ "First option in listbox should not be selected");
+ is(s2.options[1].selected, false,
+ "Second option in listbox should not be selected");
+ </script>
+</body>
+</html>
diff --git a/dom/html/test/test_option_selected_state.html b/dom/html/test/test_option_selected_state.html
new file mode 100644
index 0000000000..30a634de58
--- /dev/null
+++ b/dom/html/test/test_option_selected_state.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=942648
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 942648</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=942648">Mozilla Bug 942648</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+ <select>
+ <option value="1">1</option>
+ <option id="e1" value="2">2</option>
+ </select>
+ <select>
+ <option value="1">1</option>
+ <option id="e2" selected value="2">2</option>
+ </select>
+ <select>
+ <option value="1">1</option>
+ <option id="e3" selected="" value="2">2</option>
+ </select>
+ <select>
+ <option value="1">1</option>
+ <option id="e4" selected="selected" value="2">2</option>
+ </select>
+</pre>
+ <script type="application/javascript">
+
+ /** Test for Bug 942648 **/
+SimpleTest.waitForExplicitFinish();
+ window.onload = function() {
+ var e1 = document.getElementById('e1');
+ var e2 = document.getElementById('e2');
+ var e3 = document.getElementById('e3');
+ var e4 = document.getElementById('e4');
+ ok(!e1.selected, "e1 should not be selected");
+ ok(e2.selected, "e2 should be selected");
+ ok(e3.selected, "e3 should be selected");
+ ok(e4.selected, "e4 should be selected");
+ e1.setAttribute('selected', 'selected');
+ e2.setAttribute('selected', 'selected');
+ e3.setAttribute('selected', 'selected');
+ e4.setAttribute('selected', 'selected');
+ ok(e1.selected, "e1 should now be selected");
+ ok(e2.selected, "e2 should still be selected");
+ ok(e3.selected, "e3 should still be selected");
+ ok(e4.selected, "e4 should still be selected");
+ SimpleTest.finish();
+ };
+ </script>
+</body>
+</html>
diff --git a/dom/html/test/test_param_attributes_reflection.html b/dom/html/test/test_param_attributes_reflection.html
new file mode 100644
index 0000000000..977fb61935
--- /dev/null
+++ b/dom/html/test/test_param_attributes_reflection.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLParamElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLParamElement attributes reflection **/
+
+// .name
+reflectString({
+ element: document.createElement("param"),
+ attribute: "name",
+});
+
+// .value
+reflectString({
+ element: document.createElement("param"),
+ attribute: "value"
+});
+
+// .type
+reflectString({
+ element: document.createElement("param"),
+ attribute: "type"
+});
+
+// .valueType
+reflectString({
+ element: document.createElement("param"),
+ attribute: "valueType"
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_plugin.tst b/dom/html/test/test_plugin.tst
new file mode 100644
index 0000000000..323fae03f4
--- /dev/null
+++ b/dom/html/test/test_plugin.tst
@@ -0,0 +1 @@
+foobar
diff --git a/dom/html/test/test_q_attributes_reflection.html b/dom/html/test/test_q_attributes_reflection.html
new file mode 100644
index 0000000000..a840e6f0e5
--- /dev/null
+++ b/dom/html/test/test_q_attributes_reflection.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLQuoteElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLQuoteElement attributes reflection **/
+
+// .cite
+reflectURL({
+ element: document.createElement("q"),
+ attribute: "cite",
+});
+
+reflectURL({
+ element: document.createElement("blockquote"),
+ attribute: "cite",
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_restore_from_parser_fragment.html b/dom/html/test/test_restore_from_parser_fragment.html
new file mode 100644
index 0000000000..7fb3b75e46
--- /dev/null
+++ b/dom/html/test/test_restore_from_parser_fragment.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=644959
+-->
+<head>
+ <title>Test for Bug 644959</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=644959">Mozilla Bug 644959</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 644959 **/
+
+var content = document.getElementById('content');
+
+function appendHTML(aParent, aElementString)
+{
+ aParent.innerHTML = "<form>" + aElementString + "</form>";
+}
+
+function clearHTML(aParent)
+{
+ aParent.innerHTML = "";
+}
+
+var tests = [
+ [ "button", "<button></button>" ],
+ [ "input", "<input>" ],
+ [ "textarea", "<textarea></textarea>" ],
+ [ "select", "<select></select>" ],
+];
+
+var element = null;
+
+for (var test of tests) {
+ appendHTML(content, test[1]);
+ element = content.getElementsByTagName(test[0])[0];
+ is(element.disabled, false, "element shouldn't be disabled");
+ element.disabled = true;
+ is(element.disabled, true, "element should be disabled");
+
+ clearHTML(content);
+
+ appendHTML(content, test[1]);
+ element = content.getElementsByTagName(test[0])[0];
+ is(element.disabled, false, "element shouldn't be disabled");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_rowscollection.html b/dom/html/test/test_rowscollection.html
new file mode 100644
index 0000000000..0e5152e1d5
--- /dev/null
+++ b/dom/html/test/test_rowscollection.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=772869
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 772869</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772869">Mozilla Bug 772869</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <table id="f">
+ <thead>
+ <tr id="x"></tr>
+ </thead>
+ <tfoot>
+ <tr id="z"></tr>
+ <tr id="w"></tr>
+ </tfoot>
+ <tr id="x"></tr>
+ <tr id="y"></tr>
+ <tbody>
+ <tr id="z"></tr>
+ </tbody>
+ </table>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 772869 **/
+var x = $("f").rows;
+x.something = "another";
+var names = [];
+for (var name in x) {
+ names.push(name);
+}
+is(names.length, 10, "Should have 10 enumerated names");
+is(names[0], "0", "Enum entry 1")
+is(names[1], "1", "Enum entry 2")
+is(names[2], "2", "Enum entry 3")
+is(names[3], "3", "Enum entry 4")
+is(names[4], "4", "Enum entry 5")
+is(names[5], "5", "Enum entry 6")
+is(names[6], "something", "Enum entry 7")
+is(names[7], "item", "Enum entry 8")
+is(names[8], "namedItem", "Enum entry 9")
+is(names[9], "length", "Enum entry 10");
+
+names = Object.getOwnPropertyNames(x);
+is(names.length, 11, "Should have 11 items");
+is(names[0], "0", "Entry 1")
+is(names[1], "1", "Entry 2")
+is(names[2], "2", "Entry 3")
+is(names[3], "3", "Entry 4")
+is(names[4], "4", "Entry 5")
+is(names[5], "5", "Entry 6")
+is(names[6], "x", "Entry 7")
+is(names[7], "y", "Entry 8")
+is(names[8], "z", "Entry 9")
+is(names[9], "w", "Entry 10")
+is(names[10], "something", "Entry 11")
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_script_module.html b/dom/html/test/test_script_module.html
new file mode 100644
index 0000000000..26b9cd6f65
--- /dev/null
+++ b/dom/html/test/test_script_module.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLScriptElement with nomodule attribute</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+ <script>
+onmessage = (e) => {
+ if ("done" in e.data) {
+ SimpleTest.finish();
+ } else if ("check" in e.data) {
+ ok(e.data.check, e.data.msg);
+ } else {
+ ok(false, "Unknown message");
+ }
+}
+
+
+var ifr = document.createElement('iframe');
+ifr.src = "file_script_module.html";
+document.body.appendChild(ifr);
+
+
+SimpleTest.waitForExplicitFinish();
+ </script>
+
+</body>
+</html>
diff --git a/dom/html/test/test_set_input_files.html b/dom/html/test/test_set_input_files.html
new file mode 100644
index 0000000000..3b7bf20909
--- /dev/null
+++ b/dom/html/test/test_set_input_files.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1384030
+-->
+<head>
+ <title>Test for Setting &lt;input type=file&gt;.files </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1384030">Mozilla Bug 1384030</a>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Setting <input type=file>.files **/
+
+function runTest()
+{
+ const form = document.createElement("form");
+ const formInput = document.createElement("input");
+ formInput.type = "file";
+ formInput.name = "inputFile";
+ form.appendChild(formInput);
+
+ const input = document.createElement("input");
+ input.type = "file";
+ SpecialPowers.wrap(input).mozSetFileArray([
+ new File(["foo"], "foo"),
+ new File(["bar"], "bar")
+ ]);
+
+ formInput.files = input.files;
+
+ const inputFiles = (new FormData(form)).getAll("inputFile");
+ is(inputFiles.length, 2, "FormData should contain two input files");
+
+ is(inputFiles[0].name, "foo", "Input file name should be 'foo'");
+ is(inputFiles[1].name, "bar", "Input file name should be 'bar'");
+
+ is(inputFiles[0], input.files[0],
+ "Expect the same File object as input file 'foo'");
+ is(inputFiles[1], input.files[1],
+ "Expect the same File object as input file 'bar'");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+window.addEventListener('load', runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_srcdoc-2.html b/dom/html/test/test_srcdoc-2.html
new file mode 100644
index 0000000000..5db7d69529
--- /dev/null
+++ b/dom/html/test/test_srcdoc-2.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=802895
+-->
+ <head>
+<title>Test session history for srcdoc iframes introduced in bug 802895</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=802895">Mozilla Bug 802895</a>
+
+<iframe id="pframe" name="pframe" src="file_srcdoc-2.html"></iframe>
+<pre id="test">
+<script>
+
+ SimpleTest.waitForExplicitFinish();
+ var pframe = $("pframe");
+
+ //disable bfcache
+ pframe.contentWindow.addEventListener("unload", function () { });
+
+ var loadState = 0;
+ pframe.onload = function () {
+ SimpleTest.executeSoon(function () {
+
+ var pDoc = pframe.contentDocument;
+
+ if (loadState == 0) {
+ var div = pDoc.createElement("div");
+ div.id = "modifyCheck";
+ div.innerHTML = "hello again";
+ pDoc.body.appendChild(div);
+ ok(pDoc.getElementById("modifyCheck"), "Child element not created");
+ pframe.src = "about:blank";
+ loadState = 1;
+ }
+ else if (loadState == 1) {
+ loadState = 2;
+ window.history.back();
+ }
+ else if (loadState == 2) {
+ ok(!pDoc.getElementById("modifyCheck"), "modifyCheck element shouldn't be present");
+ is(pDoc.getElementById("iframe").contentDocument.body.innerHTML,
+ "Hello World", "srcdoc iframe not present");
+ SimpleTest.finish();
+ }
+
+ })
+ };
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_srcdoc.html b/dom/html/test/test_srcdoc.html
new file mode 100644
index 0000000000..b3137f4e0a
--- /dev/null
+++ b/dom/html/test/test_srcdoc.html
@@ -0,0 +1,118 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=802895
+-->
+ <head>
+<title>Tests for srcdoc iframes introduced in bug 802895</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=802895">Mozilla Bug 802895</a>
+
+<iframe id="pframe" src="file_srcdoc.html"></iframe>
+
+<pre id="test">
+<script>
+
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("untriaged");
+ var pframe = $("pframe");
+
+ var loadState = 0;
+ pframe.contentWindow.addEventListener("load", function () {
+
+ var pframeDoc = pframe.contentDocument;
+
+ var iframe = pframeDoc.getElementById("iframe");
+ var innerDoc = iframe.contentDocument;
+ var iframe1 = pframeDoc.getElementById("iframe1");
+ var innerDoc1 = iframe1.contentDocument;
+
+ var finish = false;
+ var finish1 = false;
+ var finish3 = false;
+
+
+
+ is(iframe.srcdoc, "Hello World", "Bad srcdoc attribute contents")
+
+ is(innerDoc.domain, document.domain, "Wrong domain");
+ is(innerDoc.referrer, pframeDoc.referrer, "Wrong referrer");
+ is(innerDoc.body.innerHTML, "Hello World", "Wrong body");
+ is(innerDoc.compatMode, "CSS1Compat", "Not standards compliant");
+
+ is(innerDoc1.domain, document.domain, "Wrong domain with src attribute");
+ is(innerDoc1.referrer, pframeDoc.referrer, "Wrong referrer with src attribute");
+ is(innerDoc1.body.innerHTML, "Goodbye World", "Wrong body with src attribute")
+ is(innerDoc1.compatMode, "CSS1Compat", "Not standards compliant with src attribute");
+
+ var iframe2 = pframeDoc.getElementById("iframe2");
+ var innerDoc2 = iframe2.contentDocument;
+ try {
+ innerDoc2.domain;
+ foundError = false;
+ }
+ catch (error) {
+ foundError = true;
+ }
+ ok(foundError, "srcdoc iframe not sandboxed");
+
+ //Test changed srcdoc attribute
+ iframe.onload = function () {
+
+ iframe = pframeDoc.getElementById("iframe");
+ innerDoc = iframe.contentDocument;
+
+ is(iframe.srcdoc, "Hello again", "Bad srcdoc attribute contents with srcdoc attribute changed");
+ is(innerDoc.domain, document.domain, "Wrong domain with srcdoc attribute changed");
+ is(innerDoc.referrer, pframeDoc.referrer, "Wrong referrer with srcdoc attribute changed");
+ is(innerDoc.body.innerHTML, "Hello again", "Wrong body with srcdoc attribute changed");
+ is(innerDoc.compatMode, "CSS1Compat", "Not standards compliant with srcdoc attribute changed");
+
+ finish = true;
+ if (finish && finish1 && finish3) {
+ SimpleTest.finish();
+ }
+ };
+
+ iframe.srcdoc = "Hello again";
+
+ var iframe3 = pframeDoc.getElementById("iframe3");
+
+ // Test srcdoc attribute removal
+ iframe3.onload = function () {
+ var innerDoc3 = iframe3.contentDocument;
+ is(innerDoc3.body.innerText, "Gone", "Bad srcdoc attribute removal");
+ finish3 = true;
+ if (finish && finish1 && finish3) {
+ SimpleTest.finish();
+ }
+ }
+
+ iframe3.removeAttribute("srcdoc");
+
+
+ var iframe1load = false;
+ iframe1.onload = function () {
+ iframe1load = true;
+ }
+
+ iframe1.src = "data:text/plain;charset=US-ASCII,Goodbyeeee";
+
+ // Need to test that changing the src doesn't change the iframe.
+ setTimeout(function () {
+ ok(!iframe1load, "Changing src attribute shouldn't cause a load when srcdoc is set");
+ finish1 = true;
+ if (finish && finish1 && finish3) {
+ SimpleTest.finish();
+ }
+ }, 2000);
+
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_style_attributes_reflection.html b/dom/html/test/test_style_attributes_reflection.html
new file mode 100644
index 0000000000..745ed7435f
--- /dev/null
+++ b/dom/html/test/test_style_attributes_reflection.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for HTMLStyleElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLStyleElement attributes reflection **/
+
+var e = document.createElement("style");
+
+// .media
+reflectString({
+ element: e,
+ attribute: "media"
+});
+
+// .type
+reflectString({
+ element: e,
+ attribute: "type"
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_track.html b/dom/html/test/test_track.html
new file mode 100644
index 0000000000..50051bf2d6
--- /dev/null
+++ b/dom/html/test/test_track.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=833386
+-->
+<head>
+ <meta charset='utf-8'>
+ <title>Test for Bug 833386 - HTMLTrackElement</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/html/test/reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+reflectLimitedEnumerated({
+ element: document.createElement("track"),
+ attribute: "kind",
+ validValues: ["subtitles", "captions", "descriptions", "chapters",
+ "metadata"],
+ invalidValues: ["foo", "bar", "\u0000", "null", "", "subtitle", "caption",
+ "description", "chapter", "meta"],
+ defaultValue: { missing: "subtitles", invalid: "metadata" },
+});
+
+// Default attribute
+reflectBoolean({
+ element: document.createElement("track"),
+ attribute: "default"
+});
+
+// Label attribute
+reflectString({
+ element: document.createElement("track"),
+ attribute: "label",
+ otherValues: [ "foo", "BAR", "_FoO", "\u0000", "null", "white space" ]
+});
+
+// Source attribute
+reflectURL({
+ element: document.createElement("track"),
+ attribute: "src",
+ otherValues: ["foo", "bar", "\u0000", "null", ""]
+});
+
+// Source Language attribute
+reflectString({
+ element: document.createElement("track"),
+ attribute: "srclang",
+ otherValues: ["foo", "bar", "\u0000", "null", ""]
+});
+
+var track = document.createElement("track");
+is(track.readyState, 0, "Default ready state should be 0 (NONE).");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_ul_attributes_reflection.html b/dom/html/test/test_ul_attributes_reflection.html
new file mode 100644
index 0000000000..cd5f6b1cc2
--- /dev/null
+++ b/dom/html/test/test_ul_attributes_reflection.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for HTMLUListElement attributes reflection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="reflect.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for HTMLUListElement attributes reflection **/
+
+// .compact
+reflectBoolean({
+ element: document.createElement("ul"),
+ attribute: "compact"
+});
+
+// .type
+reflectString({
+ element: document.createElement("ul"),
+ attribute: "type"
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_viewport_resize.html b/dom/html/test/test_viewport_resize.html
new file mode 100644
index 0000000000..e800aa592a
--- /dev/null
+++ b/dom/html/test/test_viewport_resize.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1135812
+-->
+<head>
+ <title>Test for Bug 1135812</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1135812">Mozilla Bug 1135812</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+
+<iframe style="width: 50px;"
+ srcdoc='<picture><source srcset="data:,a" media="(min-width: 150px)" /><source srcset="data:,b" media="(min-width: 100px)" /><img src="data:,c" /></picture>'></iframe>
+<script>
+ SimpleTest.waitForExplicitFinish();
+ addEventListener('load', function() {
+ var iframe = document.querySelector('iframe');
+ var img = iframe.contentDocument.querySelector('img');
+ is(img.currentSrc, 'data:,c');
+
+ img.onload = function() {
+ is(img.currentSrc, 'data:,a');
+ img.onload = function() {
+ is(img.currentSrc, 'data:,b');
+ SimpleTest.finish();
+ }
+ img.onerror = img.onload;
+ iframe.style.width = '120px';
+ };
+ img.onerror = img.onload;
+
+ iframe.style.width = '200px';
+ }, true);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_window_open_close.html b/dom/html/test/test_window_open_close.html
new file mode 100644
index 0000000000..0869100b4c
--- /dev/null
+++ b/dom/html/test/test_window_open_close.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Opens a popup. Link should load in main browser window. Popup should be closed when link clicked.
+function openWindow1() {
+ return window.open('file_window_open_close_outer.html','','width=300,height=200');
+}
+
+// Opens a new tab T1. Link opens in another new tab T2. T1 should close when link clicked.
+function openWindow2() {
+ return window.open('file_window_open_close_outer.html');
+}
+
+// Opens a new window. Link should open in a new tab of that window, but then both windows should close.
+function openWindow3() {
+ return window.open('file_window_open_close_outer.html', '', 'toolbar=1');
+}
+
+var TESTS = [openWindow1, openWindow2, openWindow3];
+
+function popupLoad(win)
+{
+ info("Sending click");
+ sendMouseEvent({type: "click"}, "link", win);
+ ok(true, "Didn't crash");
+
+ next();
+}
+
+function next()
+{
+ if (!TESTS.length) {
+ SimpleTest.finish();
+ } else {
+ var test = TESTS.shift();
+ var w = test();
+ w.addEventListener("load", (e) => popupLoad(w));
+ }
+}
+</script>
+
+<body onload="next()">
+</body>
+</html>
diff --git a/dom/html/test/test_window_open_from_closing.html b/dom/html/test/test_window_open_from_closing.html
new file mode 100644
index 0000000000..0d38c88d84
--- /dev/null
+++ b/dom/html/test/test_window_open_from_closing.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>window.open from a window being closed</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <h1>window.open from a window being closed</h1>
+<script>
+add_task(async function() {
+ const RELS = ["", "#noopener", "#opener"];
+ const FEATURES = [
+ "",
+ "noopener",
+ "width=300",
+ "width=300,noopener",
+ ];
+
+ let resolver;
+ let channel = new BroadcastChannel("test");
+ channel.onmessage = function(e) {
+ info("message from broadcastchannel: " + e.data);
+ if (e.data == "load") {
+ resolver();
+ }
+ };
+
+ for (let rel of RELS) {
+ for (let feature of FEATURES) {
+ info(`running test: rel=${rel}, feature=${feature}`);
+
+ let loadPromise = new Promise(r => { resolver = r; });
+ window.open("file_window_close_and_open.html" + rel, "_blank", feature);
+ await loadPromise;
+ ok(true, "popup opened successfully - closing...");
+ channel.postMessage("close");
+ }
+ }
+});
+</script>
+</body>
+</html>