Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
ElementInternals.setFormValue(<nullish value>) doesn't clear submissi…
…on value https://bugs.webkit.org/show_bug.cgi?id=258870 <rdar://problem/111802198> Reviewed by Ryosuke Niwa. With current bindings implementation, given an optional nullish interface / union type without default value, it's impossible to distiungish in DOM code whether a null / undefined was passed or an argument was missing: both compile to std::nullopt. This was causing two bugs in ElementInternals.setFormValue() [1]: 1) nullish `value` parameter was not clearing the submission value; 2) nullish `state` parameter was perceived as missing and the `value` was used instead. On one hand, we could revise all WebIDL files with optional nullish interface / union types to ensure that we don't have extra / missing default values (oftentimes we do), and then make ones without defaults values compile to std::nullopt (for missing argument) / std::nullptr_t (for nullish argument). On the other, that would be quite massive change and this distinction isn't current needed anywhere but ElementInternals.setFormValue(), so this change hand-rolls custom bindings just for this method, fixing both issues by checking argument count and leveraging std::nullptr_t. [1] https://html.spec.whatwg.org/multipage/custom-elements.html#dom-elementinternals-setformvalue * LayoutTests/fast/forms/state-restore-form-associated-custom-elements-2-expected.txt: Added. * LayoutTests/fast/forms/state-restore-form-associated-custom-elements-2.html: Added. * LayoutTests/fast/forms/state-restore-form-associated-custom-elements.html: * LayoutTests/imported/w3c/web-platform-tests/custom-elements/form-associated/ElementInternals-setFormValue-nullish-value-expected.txt: Added. * LayoutTests/imported/w3c/web-platform-tests/custom-elements/form-associated/ElementInternals-setFormValue-nullish-value.html: Added. * Source/WebCore/bindings/js/JSCustomElementInterface.cpp: (WebCore::JSCustomElementInterface::invokeFormStateRestoreCallback): * Source/WebCore/bindings/js/JSElementInternalsCustom.cpp: (WebCore::JSElementInternals::setFormValue): * Source/WebCore/dom/ElementInternals.cpp: (WebCore::ElementInternals::setFormValue): * Source/WebCore/dom/ElementInternals.h: * Source/WebCore/dom/ElementInternals.idl: * Source/WebCore/html/CustomElementFormValue.h: * Source/WebCore/html/FormAssociatedCustomElement.cpp: (WebCore::FormAssociatedCustomElement::setFormValue): (WebCore::FormAssociatedCustomElement::appendFormData): (WebCore::FormAssociatedCustomElement::saveFormControlState const): * Source/WebCore/html/FormAssociatedCustomElement.h: Canonical link: https://commits.webkit.org/266126@main
- Loading branch information
Alexey Shvayka
committed
Jul 18, 2023
1 parent
a445773
commit b691e0b
Showing
13 changed files
with
304 additions
and
48 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
LayoutTests/fast/forms/state-restore-form-associated-custom-elements-2-expected.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
|
||
Upgraded form-associated custom elements without an owner: | ||
PASS $("noowner-upgrade1").restoredState is undefined | ||
PASS $("noowner-upgrade2").restoredState is "bar" | ||
PASS $("noowner-upgrade3").restoredState is undefined | ||
PASS isFormDataEqual($("noowner-upgrade4").restoredState, __formData1) is true | ||
|
||
Upgraded form-associated custom elements with a form owner: | ||
PASS $("upgrade1").restoredState is undefined | ||
PASS $("upgrade2").restoredState is undefined | ||
PASS isFormDataEqual($("upgrade3").restoredState, __formData1) is true | ||
PASS $("upgrade4").restoredState is "bar" | ||
|
||
Predefined form-associated custom elements without an owner: | ||
PASS $("noowner-predefined1").restoredState is undefined | ||
PASS $("noowner-predefined2").restoredState is "bar" | ||
PASS $("noowner-predefined3").restoredState is undefined | ||
PASS isFormDataEqual($("noowner-predefined4").restoredState, __formData2) is true | ||
|
||
Predefined form-associated custom elements with a form owner: | ||
PASS $("predefined1").restoredState is undefined | ||
PASS isFormDataEqual($("predefined2").restoredState, __formData1) is true | ||
PASS $("predefined3").restoredState is undefined | ||
PASS $("predefined4").restoredState is "foo" | ||
PASS successfullyParsed is true | ||
|
||
TEST COMPLETE | ||
|
||
|
145 changes: 145 additions & 0 deletions
145
LayoutTests/fast/forms/state-restore-form-associated-custom-elements-2.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<script src="../../resources/js-test.js"></script> | ||
<script src="resources/common.js"></script> | ||
</head> | ||
<body> | ||
<div id="console"></div> | ||
|
||
<input id=emptyOnFirstVisit> | ||
|
||
<script> | ||
jsTestIsAsync = true; | ||
|
||
window.__formData1 = new FormData; | ||
__formData1.append("foo", "bar"); | ||
|
||
window.__formData2 = new FormData; | ||
__formData2.append("a", "1"); | ||
__formData2.append("b", "2"); | ||
|
||
function getXFooConstructor() { | ||
return class extends HTMLElement { | ||
static formAssociated = true; | ||
|
||
constructor() { | ||
super(); | ||
this._internals = this.attachInternals(); | ||
} | ||
|
||
connectedCallback() { | ||
const value = this._parseFormValue(this.dataset.submissionValue); | ||
const state = this._parseFormValue(this.dataset.state); | ||
|
||
this._internals.setFormValue(value, state); | ||
} | ||
|
||
formStateRestoreCallback(state) { | ||
this.restoredState = state; | ||
} | ||
|
||
_parseFormValue(attrValue) { | ||
return typeof attrValue === "string" && attrValue.startsWith("@") ? window[`__${attrValue.slice(1)}`] : attrValue; | ||
} | ||
}; | ||
} | ||
|
||
function isFormDataEqual(a, b) { | ||
const bEntries = [...b]; | ||
|
||
return [...a].every(([aKey, aValue], index) => { | ||
const [bKey, bValue] = bEntries[index]; | ||
return aKey === bKey && aValue === bValue; | ||
}); | ||
} | ||
|
||
customElements.define("x-foo-predefined", getXFooConstructor()); | ||
|
||
function makeForms(stage) { | ||
var beforeForms = '<div id=parent>' + | ||
'<x-foo-upgrade id="noowner-upgrade2" name="noowner-upgrade2" data-submission-value="@formData2" data-state="bar"></x-foo-upgrade>' + | ||
'<x-foo-upgrade id="noowner-upgrade4" name="noowner-upgrade4" data-submission-value="foo" data-state="@formData1"></x-foo-upgrade>' + | ||
'<x-foo-predefined id="noowner-predefined2" name="noowner-predefined2" data-submission-value="foo" data-state="bar"></x-foo-predefined>' + | ||
'<x-foo-predefined id="noowner-predefined4" name="noowner-predefined4" data-submission-value="@formData1" data-state="@formData2"></x-foo-predefined>'; | ||
|
||
var backForm = '<form action="data:text/html,<script>history.back();</script>" id=form1 name=form1>' + | ||
'</form>'; | ||
|
||
var query = stage == 1 ? "?session=0123456" : "?session=7654321111"; | ||
var sameActionForm1 = '<form action="http://example.com/foo.cgi' + query + '#bar" id=form2 name=form2>' + | ||
'<x-foo-upgrade id="upgrade2" name="upgrade2" data-submission-value="foo"></x-foo-upgrade>' + | ||
'<x-foo-upgrade id="upgrade4" name="upgrade4" data-submission-value="foo" data-state="bar"></x-foo-upgrade>' + | ||
'<x-foo-predefined id="predefined2" name="predefined2" data-submission-value="foo" data-state="@formData1"></x-foo-predefined>' + | ||
'<x-foo-predefined id="predefined4" name="predefined4" data-submission-value="@formData2" data-state="foo"></x-foo-predefined>' + | ||
'</form>'; | ||
|
||
var sameActionForm2 = '<form action="http://example.com/foo.cgi?action=login#bar" id=form3 name=form3>' + | ||
'<x-foo-upgrade id="upgrade1" name="upgrade1" data-submission-value="@formData2"></x-foo-upgrade>' + | ||
'<x-foo-upgrade id="upgrade3" name="upgrade3" data-submission-value="@formData2" data-state="@formData1"></x-foo-upgrade>' + | ||
'<x-foo-predefined id="predefined1" name="predefined1" data-submission-value="foo"></x-foo-predefined>' + | ||
'<x-foo-predefined id="predefined3" name="predefined3" data-submission-value="bar"></x-foo-predefined>' + | ||
'</form>'; | ||
|
||
var afterForms = | ||
'<x-foo-upgrade id="noowner-upgrade1" name="noowner-upgrade1" data-submission-value="foo"></x-foo-upgrade>' + | ||
'<x-foo-upgrade id="noowner-upgrade3" name="noowner-upgrade3" data-submission-value="@formData2"></x-foo-upgrade>' + | ||
'<x-foo-predefined id="noowner-predefined1" name="noowner-predefined1" data-submission-value="foo"></x-foo-predefined>' + | ||
'<x-foo-predefined id="noowner-predefined3" name="noowner-predefined3" data-submission-value="@formData1"></x-foo-predefined>' + | ||
'</div>'; | ||
|
||
document.write( | ||
beforeForms + | ||
(stage === 1 ? backForm + sameActionForm1 : sameActionForm1 + backForm) + | ||
sameActionForm2 + | ||
afterForms | ||
); | ||
|
||
customElements.define("x-foo-upgrade", getXFooConstructor()); | ||
} | ||
|
||
function runTest() | ||
{ | ||
var state = $('emptyOnFirstVisit'); | ||
if (!state.value) { | ||
state.value = 'visited'; | ||
makeForms(1); | ||
|
||
setTimeout(function() { | ||
$('form1').submit(); | ||
}); | ||
} else { | ||
makeForms(2); | ||
|
||
debug('\nUpgraded form-associated custom elements without an owner:'); | ||
shouldBe('$("noowner-upgrade1").restoredState', 'undefined'); | ||
shouldBeEqualToString('$("noowner-upgrade2").restoredState', 'bar'); | ||
shouldBe('$("noowner-upgrade3").restoredState', 'undefined'); | ||
shouldBeTrue('isFormDataEqual($("noowner-upgrade4").restoredState, __formData1)'); | ||
|
||
debug('\nUpgraded form-associated custom elements with a form owner:'); | ||
shouldBe('$("upgrade1").restoredState', 'undefined'); | ||
shouldBe('$("upgrade2").restoredState', 'undefined'); | ||
shouldBeTrue('isFormDataEqual($("upgrade3").restoredState, __formData1)'); | ||
shouldBeEqualToString('$("upgrade4").restoredState', 'bar'); | ||
|
||
debug('\nPredefined form-associated custom elements without an owner:'); | ||
shouldBe('$("noowner-predefined1").restoredState', 'undefined'); | ||
shouldBeEqualToString('$("noowner-predefined2").restoredState', 'bar'); | ||
shouldBe('$("noowner-predefined3").restoredState', 'undefined'); | ||
shouldBeTrue('isFormDataEqual($("noowner-predefined4").restoredState, __formData2)'); | ||
|
||
debug('\nPredefined form-associated custom elements with a form owner:'); | ||
shouldBe('$("predefined1").restoredState', 'undefined'); | ||
shouldBeTrue('isFormDataEqual($("predefined2").restoredState, __formData1)'); | ||
shouldBe('$("predefined3").restoredState', 'undefined'); | ||
shouldBeEqualToString('$("predefined4").restoredState', 'foo'); | ||
|
||
$('parent').innerHTML = ''; | ||
setTimeout(function() { finishJSTest(); }); | ||
} | ||
} | ||
|
||
runTest(); | ||
</script> | ||
</body> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
.../custom-elements/form-associated/ElementInternals-setFormValue-nullish-value-expected.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
PASS ElementInternals.setFormValue(null) clears submission value | ||
PASS ElementInternals.setFormValue(undefined) clears submission value | ||
|
45 changes: 45 additions & 0 deletions
45
...rm-tests/custom-elements/form-associated/ElementInternals-setFormValue-nullish-value.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>ElementInternals.setFormValue(nullish value) should clear submission value</title> | ||
<link rel="help" href="https://html.spec.whatwg.org/multipage/custom-elements.html#dom-elementinternals-setformvalue"> | ||
<script src="/resources/testharness.js"></script> | ||
<script src="/resources/testharnessreport.js"></script> | ||
<script> | ||
customElements.define("test-form-element", class extends HTMLElement { | ||
static formAssociated = true; | ||
constructor() { | ||
super(); | ||
this.internals = this.attachInternals(); | ||
} | ||
}); | ||
</script> | ||
</head> | ||
<body> | ||
<form id="form-null"> | ||
<test-form-element id="input-null" name="input-null"></test-form-element> | ||
</form> | ||
|
||
<form id="form-undefined"> | ||
<test-form-element id="input-undefined" name="input-undefined"></test-form-element> | ||
</form> | ||
|
||
<script> | ||
test(() => { | ||
const input = document.getElementById("input-null"); | ||
input.internals.setFormValue("fail"); | ||
input.internals.setFormValue(null); | ||
const formData = new FormData(document.getElementById("form-null")); | ||
assert_false(formData.has("input-null")); | ||
}, "ElementInternals.setFormValue(null) clears submission value"); | ||
|
||
test(() => { | ||
const input = document.getElementById("input-undefined"); | ||
input.internals.setFormValue("fail"); | ||
input.internals.setFormValue(undefined); | ||
const formData = new FormData(document.getElementById("form-undefined")); | ||
assert_false(formData.has("input-undefined")); | ||
}, "ElementInternals.setFormValue(undefined) clears submission value"); | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.