From 0a65a1edfe4fe5a5dd73444f218996e6a2b1c9e8 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Mon, 23 Dec 2024 21:13:15 +0100 Subject: [PATCH] Add FormData builtin --- builtins/web/form-data.cpp | 334 ++++++++++++++++++ builtins/web/form-data.h | 56 +++ cmake/builtins.cmake | 1 + .../api/request/request-init-002.any.js.json | 26 ++ .../response-consume-stream.any.js.json | 47 +++ .../response/response-init-002.any.js.json | 26 ++ .../xhr/formdata/append.any.js.json | 23 ++ .../xhr/formdata/constructor.any.js.json | 5 + .../xhr/formdata/delete.any.js.json | 11 + .../xhr/formdata/foreach.any.js.json | 14 + .../expectations/xhr/formdata/get.any.js.json | 20 ++ .../expectations/xhr/formdata/has.any.js.json | 11 + .../xhr/formdata/iteration.any.js.json | 11 + .../xhr/formdata/set-blob.any.js.json | 17 + .../expectations/xhr/formdata/set.any.js.json | 23 ++ tests/wpt-harness/pre-harness.js | 1 - tests/wpt-harness/tests.json | 12 +- 17 files changed, 636 insertions(+), 2 deletions(-) create mode 100644 builtins/web/form-data.cpp create mode 100644 builtins/web/form-data.h create mode 100644 tests/wpt-harness/expectations/fetch/api/request/request-init-002.any.js.json create mode 100644 tests/wpt-harness/expectations/fetch/api/response/response-consume-stream.any.js.json create mode 100644 tests/wpt-harness/expectations/fetch/api/response/response-init-002.any.js.json create mode 100644 tests/wpt-harness/expectations/xhr/formdata/append.any.js.json create mode 100644 tests/wpt-harness/expectations/xhr/formdata/constructor.any.js.json create mode 100644 tests/wpt-harness/expectations/xhr/formdata/delete.any.js.json create mode 100644 tests/wpt-harness/expectations/xhr/formdata/foreach.any.js.json create mode 100644 tests/wpt-harness/expectations/xhr/formdata/get.any.js.json create mode 100644 tests/wpt-harness/expectations/xhr/formdata/has.any.js.json create mode 100644 tests/wpt-harness/expectations/xhr/formdata/iteration.any.js.json create mode 100644 tests/wpt-harness/expectations/xhr/formdata/set-blob.any.js.json create mode 100644 tests/wpt-harness/expectations/xhr/formdata/set.any.js.json diff --git a/builtins/web/form-data.cpp b/builtins/web/form-data.cpp new file mode 100644 index 00000000..8f72ed10 --- /dev/null +++ b/builtins/web/form-data.cpp @@ -0,0 +1,334 @@ +#include "blob.h" +#include "builtin.h" +#include "encode.h" +#include "file.h" +#include "form-data.h" + +#include "host_api.h" +#include "js/TypeDecls.h" +#include "js/Value.h" + +namespace { + +using builtins::web::form_data::FormData; +using builtins::web::form_data::FormDataEntry; + +FormDataEntry entry_from_kv_pair(std::string_view name, HandleValue value) { + FormDataEntry entry; + entry.name = name; + entry.value = value; + + return entry; +} + +bool name_from_args(JSContext* cx, const JS::CallArgs& args, host_api::HostString& out) { + JS::RootedValue val(cx, args[0]); + if (!val.isString()) { + return false; + } + + auto encoded = core::encode(cx, val); + if (!encoded) { + return false; + } + + out = std::move(encoded); + return true; +} + +} // namespace + +namespace builtins { +namespace web { +namespace form_data { + +using host_api::HostString; +using blob::Blob; +using file::File; + +const JSFunctionSpec FormData::static_methods[] = { + JS_FS_END, +}; + +const JSPropertySpec FormData::static_properties[] = { + JS_PS_END, +}; + +const JSFunctionSpec FormData::methods[] = { + JS_FN("append", append, 0, JSPROP_ENUMERATE), + JS_FN("delete", remove, 0, JSPROP_ENUMERATE), + JS_FN("get", get, 0, JSPROP_ENUMERATE), + JS_FN("getAll", FormData::getAll, 0, JSPROP_ENUMERATE), + JS_FN("has", FormData::has, 0, JSPROP_ENUMERATE), + JS_FN("set", FormData::set, 0, JSPROP_ENUMERATE), + JS_FS_END, +}; + +const JSPropertySpec FormData::properties[] = { + JS_PS_END, +}; + +FormData::EntryList* FormData::entry_list(JSObject *self) { + MOZ_ASSERT(is_instance(self)); + + auto entries = static_cast( + JS::GetReservedSlot(self, static_cast(FormData::Slots::Entries)).toPrivate()); + + MOZ_ASSERT(entries); + return entries; +} + +bool FormData::append(JSContext* cx, HandleObject self, std::string_view key, HandleValue value) { + auto entries = entry_list(self); + + RootedString str(cx, JS::ToString(cx, value)); + if (!str) { + return false; + } + + RootedValue str_val(cx, StringValue(str)); + auto entry = entry_from_kv_pair(key, str_val); + + entries->append(entry); + return true; +} + +bool FormData::append(JSContext* cx, HandleObject self, std::string_view key, HandleValue value, HandleValue filename) { + if (!value.isObject()) { + return false; + } + + auto entries = entry_list(self); + RootedObject obj(cx, &value.toObject()); + + if (File::is_instance(obj)) { + auto entry = entry_from_kv_pair(key, value); + entries->append(entry); + } else if (Blob::is_instance(obj)) { + HandleValueArray arr = HandleValueArray(value); + RootedObject file_bits(cx, NewArrayObject(cx, arr)); + if (!file_bits) { + return false; + } + + RootedValue file_bits_val(cx, JS::ObjectValue(*file_bits)); + RootedValue opts_val(cx, JS::NullValue()); + RootedValue filename_val(cx); + + if (filename.isNullOrUndefined()) { + RootedString default_name(cx, JS_NewStringCopyZ(cx, "blob")); + if (!default_name) { + return false; + } + + RootedValue default_name_val(cx, JS::StringValue(default_name)); + filename_val = default_name_val; + } else { + filename_val = filename; + } + + RootedObject file(cx, File::create(cx, file_bits_val, filename_val, opts_val)); + if (!file) { + return false; + } + + RootedValue file_val(cx, JS::ObjectValue(*file)); + auto entry = entry_from_kv_pair(key, file_val); + entries->append(entry); + } + + return true; +} + +bool FormData::append(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(2) + + RootedValue name(cx, args.get(0)); + RootedValue value(cx, args.get(1)); + RootedValue filename(cx, args.get(2)); + + HostString name_to_add; + if (!name_from_args(cx, args, name_to_add)) { + return false; + } + + switch(args.length()) { + case 2: + return append(cx, self, name_to_add, value); + default: + case 3: + return append(cx, self, name_to_add, value, filename); + return false; + } +} + +bool FormData::remove(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + RootedValue name(cx, args.get(0)); + HostString name_to_remove; + if (!name_from_args(cx, args, name_to_remove)) { + return false; + } + + entry_list(self)->eraseIf([&](const FormDataEntry &entry) { + return entry.name == std::string_view(name_to_remove); + }); + + return true; +} + +bool FormData::get(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + RootedValue name(cx, args.get(0)); + HostString name_to_get; + if (!name_from_args(cx, args, name_to_get)) { + return false; + } + + auto entries = entry_list(self); + auto it = std::find_if(entries->begin(), entries->end(), [&](const FormDataEntry &entry) { + return entry.name == std::string_view(name_to_get); + }); + + if (it != entries->end()) { + args.rval().set(it->value); + } else { + args.rval().setNull(); + } + + return true; +} + +bool FormData::getAll(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + HostString name_to_get; + if (!name_from_args(cx, args, name_to_get)) { + return false; + } + + auto entries = entry_list(self); + + JS::RootedObject array(cx, JS::NewArrayObject(cx, 0)); + if (!array) { + return false; + } + + uint32_t index = 0; + for (auto &entry : *entries) { + if (entry.name == std::string_view(name_to_get)) { + JS::RootedValue val(cx, entry.value); + if (!JS_DefineElement(cx, array, index++, val, JSPROP_ENUMERATE)) { + return false; + } + } + } + + args.rval().setObject(*array); + return true; +} + +bool FormData::has(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + RootedValue name(cx, args.get(0)); + + if (!name.isString()) { + return false; + } + + auto name_to_find = core::encode(cx, name); + if (!name_to_find) { + return false; + } + + auto entries = entry_list(self); + auto it = std::find_if(entries->begin(), entries->end(), [&](const FormDataEntry &entry) { + return entry.name == std::string_view(name_to_find); + }); + + args.rval().setBoolean(it != entries->end()); + return true; +} + +bool FormData::set(JSContext *cx, unsigned argc, JS::Value *vp) { + METHOD_HEADER(1) + + RootedValue name(cx, args.get(0)); + RootedValue value(cx, args.get(1)); + RootedValue filename(cx, args.get(2)); + + HostString name_to_find; + if (!name_from_args(cx, args, name_to_find)) { + return false; + } + + auto entries = entry_list(self); + auto it = std::find_if(entries->begin(), entries->end(), [&](const FormDataEntry &entry) { + return entry.name == std::string_view(name_to_find); + }); + + if (it != entries->end()) { + it->value = value; + return true; + } else { + switch(args.length()) { + case 2: + return append(cx, self, name_to_find, value); + default: + case 3: + return append(cx, self, name_to_find, value, filename); + } + } +} + +bool FormData::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { + CTOR_HEADER("FormData", 0); + + // The FormData constructor optionally takes HTMLFormElement and HTMLElement as parameters. + // As we do not support DOM we throw if the first parameter is not undefined. + // + // See https://min-common-api.proposal.wintercg.org/#issue-92f53c35 + if (!args.get(0).isNullOrUndefined()) { + return api::throw_error(cx, api::Errors::TypeError, "FormData.constructor", "form", "be null or undefined"); + } + + RootedObject self(cx, JS_NewObjectForConstructor(cx, &class_, args)); + + if (!self) { + return false; + } + + SetReservedSlot(self, static_cast(Slots::Entries), JS::PrivateValue(new EntryList)); + + args.rval().setObject(*self); + return true; +} + +void FormData::finalize(JS::GCContext *gcx, JSObject *self) { + MOZ_ASSERT(is_instance(self)); + auto entries = entry_list(self); + if (entries) { + free(entries); + } +} + +void FormData::trace(JSTracer *trc, JSObject *self) { + MOZ_ASSERT(is_instance(self)); + auto entries = entry_list(self); + entries->trace(trc); +} + +bool FormData::init_class(JSContext *cx, JS::HandleObject global) { + return init_class_impl(cx, global); +} + +bool install(api::Engine *engine) { + return FormData::init_class(engine->cx(), engine->global()); +} + +} // namespace form_data +} // namespace web +} // namespace builtins diff --git a/builtins/web/form-data.h b/builtins/web/form-data.h new file mode 100644 index 00000000..cf01c1ef --- /dev/null +++ b/builtins/web/form-data.h @@ -0,0 +1,56 @@ +#ifndef BUILTINS_WEB_FORM_FDATA_H +#define BUILTINS_WEB_FORM_FDATA_H + +#include "builtin.h" + +namespace builtins { +namespace web { +namespace form_data { + +struct FormDataEntry { + std::string name; + JS::Heap value; + + void trace(JSTracer *trc) { TraceEdge(trc, &value, "FormDataEntry value"); } +}; + +class FormData : public TraceableBuiltinImpl { + static bool append(JSContext *cx, unsigned argc, JS::Value *vp); + static bool remove(JSContext *cx, unsigned argc, JS::Value *vp); + static bool get(JSContext *cx, unsigned argc, JS::Value *vp); + static bool getAll(JSContext *cx, unsigned argc, JS::Value *vp); + static bool has(JSContext *cx, unsigned argc, JS::Value *vp); + static bool set(JSContext *cx, unsigned argc, JS::Value *vp); + + static bool append(JSContext *cx, HandleObject self, std::string_view key, HandleValue val); + static bool append(JSContext *cx, HandleObject self, std::string_view key, HandleValue val, + HandleValue filename); + + using EntryList = JS::GCVector; + static EntryList *entry_list(JSObject *self); + +public: + static constexpr const char *class_name = "FormData"; + + static const JSFunctionSpec static_methods[]; + static const JSPropertySpec static_properties[]; + static const JSFunctionSpec methods[]; + static const JSPropertySpec properties[]; + + static constexpr unsigned ctor_length = 0; + + enum Slots { Entries, Count }; + + static bool init_class(JSContext *cx, HandleObject global); + static bool constructor(JSContext *cx, unsigned argc, Value *vp); + static void finalize(JS::GCContext *gcx, JSObject *self); + static void trace(JSTracer *trc, JSObject *self); +}; + +bool install(api::Engine *engine); + +} // namespace form_data +} // namespace web +} // namespace builtins + +#endif // BUILTINS_WEB_FORM_FDATA_H diff --git a/cmake/builtins.cmake b/cmake/builtins.cmake index 474d4ffc..9e6c7fdb 100644 --- a/cmake/builtins.cmake +++ b/cmake/builtins.cmake @@ -19,6 +19,7 @@ add_builtin(builtins/web/structured-clone.cpp) add_builtin(builtins/web/base64.cpp) add_builtin(builtins/web/blob.cpp) add_builtin(builtins/web/file.cpp) +add_builtin(builtins/web/form-data.cpp) add_builtin( builtins::web::dom_exception SRC diff --git a/tests/wpt-harness/expectations/fetch/api/request/request-init-002.any.js.json b/tests/wpt-harness/expectations/fetch/api/request/request-init-002.any.js.json new file mode 100644 index 00000000..c1f37244 --- /dev/null +++ b/tests/wpt-harness/expectations/fetch/api/request/request-init-002.any.js.json @@ -0,0 +1,26 @@ +{ + "Initialize Request with headers values": { + "status": "PASS" + }, + "Initialize Request's body with \"undefined\", undefined": { + "status": "PASS" + }, + "Initialize Request's body with \"null\", null": { + "status": "PASS" + }, + "Initialize Request's body with \"[object Blob]\", application/octet-binary": { + "status": "PASS" + }, + "Initialize Request's body with \"[object Object]\", multipart/form-data": { + "status": "FAIL" + }, + "Initialize Request's body with \"This is a USVString\", text/plain;charset=UTF-8": { + "status": "PASS" + }, + "Initialize Request's body with \"hi!\", text/plain;charset=UTF-8": { + "status": "PASS" + }, + "Initialize Request's body with \"name=value\", application/x-www-form-urlencoded;charset=UTF-8": { + "status": "PASS" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/fetch/api/response/response-consume-stream.any.js.json b/tests/wpt-harness/expectations/fetch/api/response/response-consume-stream.any.js.json new file mode 100644 index 00000000..0af94bba --- /dev/null +++ b/tests/wpt-harness/expectations/fetch/api/response/response-consume-stream.any.js.json @@ -0,0 +1,47 @@ +{ + "Read empty text response's body as readableStream": { + "status": "PASS" + }, + "Read empty blob response's body as readableStream": { + "status": "PASS" + }, + "Read blob response's body as readableStream with mode=undefined": { + "status": "PASS" + }, + "Read text response's body as readableStream with mode=undefined": { + "status": "PASS" + }, + "Read URLSearchParams response's body as readableStream with mode=undefined": { + "status": "PASS" + }, + "Read array buffer response's body as readableStream with mode=undefined": { + "status": "PASS" + }, + "Read form data response's body as readableStream with mode=undefined": { + "status": "FAIL" + }, + "Read blob response's body as readableStream with mode=byob": { + "status": "FAIL" + }, + "Read text response's body as readableStream with mode=byob": { + "status": "FAIL" + }, + "Read URLSearchParams response's body as readableStream with mode=byob": { + "status": "FAIL" + }, + "Read array buffer response's body as readableStream with mode=byob": { + "status": "FAIL" + }, + "Read form data response's body as readableStream with mode=byob": { + "status": "FAIL" + }, + "Getting an error Response stream": { + "status": "FAIL" + }, + "Getting a redirect Response stream": { + "status": "PASS" + }, + "Reading with offset from Response stream": { + "status": "FAIL" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/fetch/api/response/response-init-002.any.js.json b/tests/wpt-harness/expectations/fetch/api/response/response-init-002.any.js.json new file mode 100644 index 00000000..f44af7e2 --- /dev/null +++ b/tests/wpt-harness/expectations/fetch/api/response/response-init-002.any.js.json @@ -0,0 +1,26 @@ +{ + "Initialize Response with headers values": { + "status": "PASS" + }, + "Initialize Response's body with application/octet-binary": { + "status": "PASS" + }, + "Initialize Response's body with multipart/form-data": { + "status": "FAIL" + }, + "Initialize Response's body with application/x-www-form-urlencoded;charset=UTF-8": { + "status": "PASS" + }, + "Initialize Response's body with text/plain;charset=UTF-8": { + "status": "PASS" + }, + "Read Response's body as readableStream": { + "status": "PASS" + }, + "Testing empty Response Content-Type header": { + "status": "PASS" + }, + "Testing null Response body": { + "status": "PASS" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/xhr/formdata/append.any.js.json b/tests/wpt-harness/expectations/xhr/formdata/append.any.js.json new file mode 100644 index 00000000..5525996c --- /dev/null +++ b/tests/wpt-harness/expectations/xhr/formdata/append.any.js.json @@ -0,0 +1,23 @@ +{ + "testFormDataAppend1": { + "status": "PASS" + }, + "testFormDataAppend2": { + "status": "PASS" + }, + "testFormDataAppendUndefined1": { + "status": "PASS" + }, + "testFormDataAppendUndefined2": { + "status": "PASS" + }, + "testFormDataAppendNull1": { + "status": "PASS" + }, + "testFormDataAppendNull2": { + "status": "PASS" + }, + "testFormDataAppendEmptyBlob": { + "status": "FAIL" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/xhr/formdata/constructor.any.js.json b/tests/wpt-harness/expectations/xhr/formdata/constructor.any.js.json new file mode 100644 index 00000000..2e64ae4b --- /dev/null +++ b/tests/wpt-harness/expectations/xhr/formdata/constructor.any.js.json @@ -0,0 +1,5 @@ +{ + "Constructors should throw a type error": { + "status": "FAIL" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/xhr/formdata/delete.any.js.json b/tests/wpt-harness/expectations/xhr/formdata/delete.any.js.json new file mode 100644 index 00000000..60836523 --- /dev/null +++ b/tests/wpt-harness/expectations/xhr/formdata/delete.any.js.json @@ -0,0 +1,11 @@ +{ + "testFormDataDelete": { + "status": "PASS" + }, + "testFormDataDeleteNonExistentKey": { + "status": "PASS" + }, + "testFormDataDeleteOtherKey": { + "status": "PASS" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/xhr/formdata/foreach.any.js.json b/tests/wpt-harness/expectations/xhr/formdata/foreach.any.js.json new file mode 100644 index 00000000..c2744c9a --- /dev/null +++ b/tests/wpt-harness/expectations/xhr/formdata/foreach.any.js.json @@ -0,0 +1,14 @@ +{ + "Iterator should return duplicate keys and non-deleted values": { + "status": "FAIL" + }, + "Entries iterator should return duplicate keys and non-deleted values": { + "status": "FAIL" + }, + "Keys iterator should return duplicates": { + "status": "FAIL" + }, + "Values iterator should return non-deleted values": { + "status": "FAIL" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/xhr/formdata/get.any.js.json b/tests/wpt-harness/expectations/xhr/formdata/get.any.js.json new file mode 100644 index 00000000..a326afbc --- /dev/null +++ b/tests/wpt-harness/expectations/xhr/formdata/get.any.js.json @@ -0,0 +1,20 @@ +{ + "testFormDataGet": { + "status": "PASS" + }, + "testFormDataGetNull1": { + "status": "PASS" + }, + "testFormDataGetNull2": { + "status": "PASS" + }, + "testFormDataGetAll": { + "status": "PASS" + }, + "testFormDataGetAllEmpty1": { + "status": "PASS" + }, + "testFormDataGetAllEmpty2": { + "status": "PASS" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/xhr/formdata/has.any.js.json b/tests/wpt-harness/expectations/xhr/formdata/has.any.js.json new file mode 100644 index 00000000..07fd7fc8 --- /dev/null +++ b/tests/wpt-harness/expectations/xhr/formdata/has.any.js.json @@ -0,0 +1,11 @@ +{ + "testFormDataHas": { + "status": "PASS" + }, + "testFormDataHasEmpty1": { + "status": "PASS" + }, + "testFormDataHasEmpty2": { + "status": "PASS" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/xhr/formdata/iteration.any.js.json b/tests/wpt-harness/expectations/xhr/formdata/iteration.any.js.json new file mode 100644 index 00000000..88a70e54 --- /dev/null +++ b/tests/wpt-harness/expectations/xhr/formdata/iteration.any.js.json @@ -0,0 +1,11 @@ +{ + "Iteration skips elements removed while iterating": { + "status": "FAIL" + }, + "Removing elements already iterated over causes an element to be skipped during iteration": { + "status": "FAIL" + }, + "Appending a value pair during iteration causes it to be reached during iteration": { + "status": "FAIL" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/xhr/formdata/set-blob.any.js.json b/tests/wpt-harness/expectations/xhr/formdata/set-blob.any.js.json new file mode 100644 index 00000000..19917be1 --- /dev/null +++ b/tests/wpt-harness/expectations/xhr/formdata/set-blob.any.js.json @@ -0,0 +1,17 @@ +{ + "blob without type": { + "status": "FAIL" + }, + "blob with type": { + "status": "FAIL" + }, + "blob with custom name": { + "status": "FAIL" + }, + "file without lastModified or custom name": { + "status": "FAIL" + }, + "file with lastModified and custom name": { + "status": "FAIL" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/xhr/formdata/set.any.js.json b/tests/wpt-harness/expectations/xhr/formdata/set.any.js.json new file mode 100644 index 00000000..d7bf186f --- /dev/null +++ b/tests/wpt-harness/expectations/xhr/formdata/set.any.js.json @@ -0,0 +1,23 @@ +{ + "testFormDataSet1": { + "status": "PASS" + }, + "testFormDataSet2": { + "status": "PASS" + }, + "testFormDataSetUndefined1": { + "status": "PASS" + }, + "testFormDataSetUndefined2": { + "status": "PASS" + }, + "testFormDataSetNull1": { + "status": "PASS" + }, + "testFormDataSetNull2": { + "status": "PASS" + }, + "testFormDataSetEmptyBlob": { + "status": "PASS" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/pre-harness.js b/tests/wpt-harness/pre-harness.js index 1b198a15..2ae3e599 100644 --- a/tests/wpt-harness/pre-harness.js +++ b/tests/wpt-harness/pre-harness.js @@ -28,7 +28,6 @@ globalThis.Window={ } globalThis.crypto.subtle.generateKey = function () {return Promise.reject(new Error('globalThis.crypto.subtle.generateKey unimplemented'))} -globalThis.FormData = class FormData{}; globalThis.SharedArrayBuffer = class SharedArrayBuffer{}; globalThis.MessageChannel = class MessageChannel{}; ; diff --git a/tests/wpt-harness/tests.json b/tests/wpt-harness/tests.json index 333fa950..322e0d3c 100644 --- a/tests/wpt-harness/tests.json +++ b/tests/wpt-harness/tests.json @@ -285,5 +285,15 @@ "webidl/ecmascript-binding/es-exceptions/DOMException-constants.any.js", "webidl/ecmascript-binding/es-exceptions/DOMException-constructor-and-prototype.any.js", "webidl/ecmascript-binding/es-exceptions/DOMException-constructor-behavior.any.js", - "webidl/ecmascript-binding/es-exceptions/DOMException-custom-bindings.any.js" + "webidl/ecmascript-binding/es-exceptions/DOMException-custom-bindings.any.js", + "xhr/formdata/constructor.any.js", + "xhr/formdata/append.any.js", + "xhr/formdata/constructor.any.js", + "xhr/formdata/delete.any.js", + "xhr/formdata/foreach.any.js", + "xhr/formdata/get.any.js", + "xhr/formdata/has.any.js", + "xhr/formdata/iteration.any.js", + "xhr/formdata/set.any.js", + "xhr/formdata/set-blob.any.js" ]