Skip to content

Commit

Permalink
LibWeb: Update FormData constructor to reflect the spec
Browse files Browse the repository at this point in the history
  • Loading branch information
shlyakpavel committed Dec 23, 2024
1 parent 5c03258 commit 9524330
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 6 deletions.
30 changes: 26 additions & 4 deletions Libraries/LibWeb/XHR/FormData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/FileAPI/Blob.h>
#include <LibWeb/FileAPI/File.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/FormControlInfrastructure.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/XHR/FormData.h>

Expand All @@ -18,17 +20,37 @@ namespace Web::XHR {
GC_DEFINE_ALLOCATOR(FormData);

// https://xhr.spec.whatwg.org/#dom-formdata
WebIDL::ExceptionOr<GC::Ref<FormData>> FormData::construct_impl(JS::Realm& realm, GC::Ptr<HTML::HTMLFormElement> form)
WebIDL::ExceptionOr<GC::Ref<FormData>> FormData::construct_impl(JS::Realm& realm, GC::Ptr<HTML::HTMLFormElement> form, Optional<GC::Ptr<HTML::HTMLElement>> submitter)
{
Vector<FormDataEntry> list;
// 1. If form is given, then:
if (form) {
// 1. Let list be the result of constructing the entry list for form.
// 1. If submitter is non-null, then:
if (submitter.has_value() && submitter.value()) {
auto& submitter_element = submitter.value();

// 1. If submitter is not a submit button, then throw a TypeError.
if (!is<HTML::FormAssociatedElement>(*submitter_element)) {
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Submitter is not associated with a form."sv };
}
auto* form_associated_element = dynamic_cast<HTML::FormAssociatedElement*>(submitter_element.ptr());

if (!form_associated_element->is_submit_button()) {
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Submitter is not a valid submit button."sv };
}
// 2. If submitter’s form owner is not form, then throw a "NotFoundError" DOMException.
auto* form_owner = form_associated_element->form();
if (form_owner && form_owner != form) {
return WebIDL::NotFoundError::create(realm, "Submitter does not belong to the provided form."_string);
}
}

// 2. Let list be the result of constructing the entry list for form.
auto entry_list = TRY(construct_entry_list(realm, *form));
// 2. If list is null, then throw an "InvalidStateError" DOMException.
// 3. If list is null, then throw an "InvalidStateError" DOMException.
if (!entry_list.has_value())
return WebIDL::InvalidStateError::create(realm, "Form element does not contain any entries."_string);
// 3. Set this’s entry list to list.
// 4. Set this’s entry list to list.
list = entry_list.release_value();
}

Expand Down
2 changes: 1 addition & 1 deletion Libraries/LibWeb/XHR/FormData.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class FormData : public Bindings::PlatformObject {
public:
virtual ~FormData() override;

static WebIDL::ExceptionOr<GC::Ref<FormData>> construct_impl(JS::Realm&, GC::Ptr<HTML::HTMLFormElement> form = {});
static WebIDL::ExceptionOr<GC::Ref<FormData>> construct_impl(JS::Realm&, GC::Ptr<HTML::HTMLFormElement> form = {}, Optional<GC::Ptr<HTML::HTMLElement>> submitter = {});
static WebIDL::ExceptionOr<GC::Ref<FormData>> construct_impl(JS::Realm&, Vector<FormDataEntry> entry_list);

static WebIDL::ExceptionOr<GC::Ref<FormData>> create(JS::Realm&, Vector<DOMURL::QueryParam> entry_list);
Expand Down
2 changes: 1 addition & 1 deletion Libraries/LibWeb/XHR/FormData.idl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ typedef (File or USVString) FormDataEntryValue;
// https://xhr.spec.whatwg.org/#interface-formdata
[Exposed=(Window,Worker)]
interface FormData {
constructor(optional HTMLFormElement form);
constructor(optional HTMLFormElement form, optional HTMLElement? submitter = null);

undefined append(USVString name, USVString value);
undefined append(USVString name, Blob blobValue, optional USVString filename);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Harness status: OK

Found 9 tests

6 Pass
3 Fail
Pass FormData construction should allow a null submitter
Pass FormData construction should allow an undefined form and an undefined submitter
Pass FormData construction should allow an undefined form and a null submitter
Pass FormData construction should throw a TypeError if a non-null submitter is not a submit button
Pass FormData construction should throw a 'NotFoundError' DOMException if a non-null submitter is not owned by the form
Fail The constructed FormData object should contain an in-tree-order entry for a named submit button submitter
Pass The constructed FormData object should not contain an entry for an unnamed submit button submitter
Fail The constructed FormData object should contain in-tree-order entries for an activated Image Button submitter
Fail The constructed FormData object should contain in-tree-order entries for an unactivated Image Button submitter
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<meta charset='utf-8'>
<link rel='help' href='https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set'>
<link ref='help' href='https://xhr.spec.whatwg.org/#dom-formdata'>
<script src='../../resources/testharness.js'></script>
<script src='../../resources/testharnessreport.js'></script>

<button name=outerNamed value=GO form='myform'></button>
<form id='myform' onsubmit='return false'>
<input name=n1 value=v1>
<button name=named value=GO></button>
<button id=unnamed value=unnamed></button>
<button form="another" name=unassociated value=unassociated></button>
<input type=image name=namedImage src='/media/1x1-green.png'></button>
<input type=image id=unnamedImage src='/media/1x1-green.png'></button>
<input type=image name=unactivatedImage src='/media/1x1-green.png'></button>
<input name=n3 value=v3>
</form>

<form id='another'>
<button name=unassociated2 value=unassociated></button>
</form>

<script>
function assertFormDataEntries(formData, expectedEntries) {
const expectedEntryNames = expectedEntries.map((entry) => entry[0]);
const actualEntries = [...formData.entries()];
const actualEntryNames = actualEntries.map((entry) => entry[0]);
assert_array_equals(actualEntryNames, expectedEntryNames);
for (let i = 0; i < actualEntries.length; i++) {
assert_array_equals(actualEntries[i], expectedEntries[i]);
}
}

const form = document.querySelector('#myform');

test(() => {
assertFormDataEntries(
new FormData(form, null),
[['n1', 'v1'], ['n3', 'v3']]
);
}, 'FormData construction should allow a null submitter'); // the use case here is so web developers can avoid null checks, e.g. `new FormData(e.target, e.submitter)`

test(() => {
assertFormDataEntries(new FormData(undefined, undefined), []);
}, 'FormData construction should allow an undefined form and an undefined submitter');

test(() => {
assertFormDataEntries(new FormData(undefined, null), []);
}, 'FormData construction should allow an undefined form and a null submitter');

test(() => {
assert_throws_js(TypeError, () => new FormData(form, document.querySelector('[name=n1]')));
}, 'FormData construction should throw a TypeError if a non-null submitter is not a submit button');

test(() => {
assert_throws_dom('NotFoundError', () => new FormData(form, document.querySelector('[name=unassociated]')));
assert_throws_dom('NotFoundError', () => new FormData(form, document.querySelector('[name=unassociated2]')));
}, "FormData construction should throw a 'NotFoundError' DOMException if a non-null submitter is not owned by the form");

test(() => {
assertFormDataEntries(
new FormData(form, document.querySelector('[name=named]')),
[['n1', 'v1'], ['named', 'GO'], ['n3', 'v3']]
);
assertFormDataEntries(
new FormData(form, document.querySelector('[name=outerNamed]')),
[['outerNamed', 'GO'], ['n1', 'v1'], ['n3', 'v3']]
);
}, 'The constructed FormData object should contain an in-tree-order entry for a named submit button submitter');

test(() => {
assertFormDataEntries(
new FormData(form, document.querySelector('#unnamed')),
[['n1', 'v1'], ['n3', 'v3']]
);
}, 'The constructed FormData object should not contain an entry for an unnamed submit button submitter');

test(() => {
const submitter1 = document.querySelector('[name=namedImage]');
submitter1.click();
const submitter2 = document.querySelector('#unnamedImage');
submitter2.click();
assertFormDataEntries(
new FormData(form, submitter1),
[['n1', 'v1'], ['namedImage.x', '0'], ['namedImage.y', '0'], ['n3', 'v3']]
);
assertFormDataEntries(
new FormData(form, submitter2),
[['n1', 'v1'], ['x', '0'], ['y', '0'], ['n3', 'v3']]
);
}, 'The constructed FormData object should contain in-tree-order entries for an activated Image Button submitter');

test(() => {
assertFormDataEntries(
new FormData(form, document.querySelector('[name=unactivatedImage]')),
[['n1', 'v1'], ['unactivatedImage.x', '0'], ['unactivatedImage.y', '0'], ['n3', 'v3']]
);
}, 'The constructed FormData object should contain in-tree-order entries for an unactivated Image Button submitter');
</script>

0 comments on commit 9524330

Please sign in to comment.