diff --git a/FileAPI/file/send-file-formdata-controls.tentative.html b/FileAPI/file/send-file-formdata-controls.html
similarity index 92%
rename from FileAPI/file/send-file-formdata-controls.tentative.html
rename to FileAPI/file/send-file-formdata-controls.html
index 4259741b63ef31..2982b13be70722 100644
--- a/FileAPI/file/send-file-formdata-controls.tentative.html
+++ b/FileAPI/file/send-file-formdata-controls.html
@@ -1,10 +1,6 @@
-
FormData: Upload files named using controls (tentative)
-
+FormData: Upload files named using controls
-FormData: Upload files named using punctuation (tentative)
-
+FormData: Upload files named using punctuation
+
@@ -30,32 +31,85 @@
customElements.define('my-control', MyControl);
const $ = document.querySelector.bind(document);
-function submitPromise(t) {
+function submitPromise(t, extractFromIframe) {
+ if (!extractFromIframe) {
+ extractFromIframe = (iframe) => iframe.contentWindow.location.search;
+ }
return new Promise((resolve, reject) => {
const iframe = $('iframe');
- iframe.onload = () => resolve(iframe.contentWindow.location.search);
+ iframe.onload = () => resolve(extractFromIframe(iframe));
iframe.onerror = () => reject(new Error('iframe onerror fired'));
$('form').submit();
});
}
-function testSerializedEntry({name, expectedName, value, expectedValue, description}) {
- promise_test(async t => {
- $('#container').innerHTML = '' +
- '';
- if (name !== undefined) {
- $('my-control').setAttribute("name", name);
- }
- if (Array.isArray(value)) {
- $('my-control').setValues(value);
- } else {
- $('my-control').value = value;
- }
- const query = await submitPromise(t);
- assert_equals(query, `?${expectedName}=${expectedValue}`);
- }, description);
+function testSerializedEntry({name, value, expected, description}) {
+ // urlencoded
+ {
+ const {name: expectedName, value: expectedValue} = expected.urlencoded;
+ promise_test(async t => {
+ $('#container').innerHTML = '' +
+ '';
+ if (name !== undefined) {
+ $('my-control').setAttribute("name", name);
+ }
+ if (Array.isArray(value)) {
+ $('my-control').setValues(value);
+ } else {
+ $('my-control').value = value;
+ }
+ const query = await submitPromise(t);
+ assert_equals(query, `?${expectedName}=${expectedValue}`);
+ }, `${description} (urlencoded)`);
+ }
+
+ // formdata
+ {
+ const {name: expectedName, filename: expectedFilename, value: expectedValue} = expected.formdata;
+ promise_test(async t => {
+ $('#container').innerHTML =
+ '' +
+ '';
+ if (name !== undefined) {
+ $('my-control').setAttribute("name", name);
+ }
+ if (Array.isArray(value)) {
+ $('my-control').setValues(value);
+ } else {
+ $('my-control').value = value;
+ }
+ const escaped = await submitPromise(t, iframe => iframe.contentDocument.body.textContent);
+ const formdata = escaped
+ .replace(/\r\n?|\n/g, "\r\n")
+ .replace(
+ /\\x[0-9A-Fa-f]{2}/g,
+ escape => String.fromCodePoint(parseInt(escape.substring(2), 16))
+ );
+ const boundary = formdata.split("\r\n")[0];
+ const expected = [
+ boundary,
+ ...(() => {
+ if (expectedFilename === undefined) {
+ return [`Content-Disposition: form-data; name="${expectedName}"`];
+ } else {
+ return [
+ `Content-Disposition: form-data; name="${expectedName}"; filename="${expectedFilename}"`,
+ "Content-Type: text/plain"
+ ];
+ }
+ })(),
+ "",
+ expectedValue,
+ boundary + "--",
+ ""
+ ].join("\r\n");
+ assert_equals(formdata, expected);
+ }, `${description} (formdata)`);
+ }
}
promise_test(t => {
@@ -145,152 +199,380 @@
testSerializedEntry({
name: 'a\nb',
value: 'c',
- expectedName: 'a%0D%0Ab',
- expectedValue: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
description: 'Newline normalization - \\n in name'
});
testSerializedEntry({
name: 'a\rb',
value: 'c',
- expectedName: 'a%0D%0Ab',
- expectedValue: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
description: 'Newline normalization - \\r in name'
});
testSerializedEntry({
name: 'a\r\nb',
value: 'c',
- expectedName: 'a%0D%0Ab',
- expectedValue: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
description: 'Newline normalization - \\r\\n in name'
});
testSerializedEntry({
name: 'a\n\rb',
value: 'c',
- expectedName: 'a%0D%0A%0D%0Ab',
- expectedValue: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0A%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0A%0D%0Ab',
+ value: 'c'
+ }
+ },
description: 'Newline normalization - \\n\\r in name'
});
testSerializedEntry({
name: 'a',
value: 'b\nc',
- expectedName: 'a',
- expectedValue: 'b%0D%0Ac',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
description: 'Newline normalization - \\n in value'
});
testSerializedEntry({
name: 'a',
value: 'b\rc',
- expectedName: 'a',
- expectedValue: 'b%0D%0Ac',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
description: 'Newline normalization - \\r in value'
});
testSerializedEntry({
name: 'a',
value: 'b\r\nc',
- expectedName: 'a',
- expectedValue: 'b%0D%0Ac',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
description: 'Newline normalization - \\r\\n in value'
});
testSerializedEntry({
name: 'a',
value: 'b\n\rc',
- expectedName: 'a',
- expectedValue: 'b%0D%0A%0D%0Ac',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0A%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\n\r\nc'
+ }
+ },
description: 'Newline normalization - \\n\\r in value'
});
testSerializedEntry({
name: 'a',
- value: new File([], "b\nc"),
- expectedName: 'a',
- expectedValue: 'b%0D%0Ac',
+ value: new File([], "b\nc", {type: "text/plain"}),
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0Ac',
+ value: ''
+ }
+ },
description: 'Newline normalization - \\n in filename'
});
testSerializedEntry({
name: 'a',
- value: new File([], "b\rc"),
- expectedName: 'a',
- expectedValue: 'b%0D%0Ac',
+ value: new File([], "b\rc", {type: "text/plain"}),
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0Dc',
+ value: ''
+ }
+ },
description: 'Newline normalization - \\r in filename'
});
testSerializedEntry({
name: 'a',
- value: new File([], "b\r\nc"),
- expectedName: 'a',
- expectedValue: 'b%0D%0Ac',
+ value: new File([], "b\r\nc", {type: "text/plain"}),
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0D%0Ac',
+ value: ''
+ }
+ },
description: 'Newline normalization - \\r\\n in filename'
});
testSerializedEntry({
name: 'a',
- value: new File([], "b\n\rc"),
- expectedName: 'a',
- expectedValue: 'b%0D%0A%0D%0Ac',
+ value: new File([], "b\n\rc", {type: "text/plain"}),
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0A%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0A%0Dc',
+ value: ''
+ }
+ },
description: 'Newline normalization - \\n\\r in filename'
});
testSerializedEntry({
value: [['a\nb', 'c']],
- expectedName: 'a%0Ab',
- expectedValue: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
description: 'Newline normalization - \\n in FormData name'
});
testSerializedEntry({
value: [['a\rb', 'c']],
- expectedName: 'a%0Db',
- expectedValue: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
description: 'Newline normalization - \\r in FormData name'
});
testSerializedEntry({
value: [['a\r\nb', 'c']],
- expectedName: 'a%0D%0Ab',
- expectedValue: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
description: 'Newline normalization - \\r\\n in FormData name'
});
testSerializedEntry({
value: [['a\n\rb', 'c']],
- expectedName: 'a%0A%0Db',
- expectedValue: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0A%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0A%0D%0Ab',
+ value: 'c'
+ }
+ },
description: 'Newline normalization - \\n\\r in FormData name'
});
testSerializedEntry({
value: [['a', 'b\nc']],
- expectedName: 'a',
- expectedValue: 'b%0Ac',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
description: 'Newline normalization - \\n in FormData value'
});
testSerializedEntry({
value: [['a', 'b\rc']],
- expectedName: 'a',
- expectedValue: 'b%0Dc',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
description: 'Newline normalization - \\r in FormData value'
});
testSerializedEntry({
value: [['a', 'b\r\nc']],
- expectedName: 'a',
- expectedValue: 'b%0D%0Ac',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
description: 'Newline normalization - \\r\\n in FormData value'
});
testSerializedEntry({
value: [['a', 'b\n\rc']],
- expectedName: 'a',
- expectedValue: 'b%0A%0Dc',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0A%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\n\r\nc'
+ }
+ },
description: 'Newline normalization - \\n\\r in FormData value'
});
+
+testSerializedEntry({
+ value: [['a', new File([], 'b\nc', {type: "text/plain"})]],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0Ac',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\n in FormData filename'
+});
+
+testSerializedEntry({
+ value: [['a', new File([], 'b\rc', {type: "text/plain"})]],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0Dc',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\r in FormData filename'
+});
+
+testSerializedEntry({
+ value: [['a', new File([], 'b\r\nc', {type: "text/plain"})]],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0D%0Ac',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\r\\n in FormData filename'
+});
+
+testSerializedEntry({
+ value: [['a', new File([], 'b\n\rc', {type: "text/plain"})]],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0A%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0A%0Dc',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\n\\r in FormData filename'
+});
diff --git a/html/semantics/forms/form-submission-0/constructing-form-data-set.html b/html/semantics/forms/form-submission-0/constructing-form-data-set.html
index b27f1996d99cb8..a6c5e91554d28f 100644
--- a/html/semantics/forms/form-submission-0/constructing-form-data-set.html
+++ b/html/semantics/forms/form-submission-0/constructing-form-data-set.html
@@ -114,6 +114,17 @@
form.submit();
});
+test(() => {
+ const form = populateForm('');
+ form.addEventListener('formdata', e => {
+ e.formData.append('a\nb', 'c\rd');
+ });
+ const formData = new FormData(form);
+ const [name, value] = [...formData][0];
+ assert_equals(name, 'a\nb');
+ assert_equals(value, 'c\rd');
+}, 'Entries added to the "formdata" IDL attribute shouldn\'t be newline normalized in the resulting FormData');
+
test(() => {
let form = populateForm('');
let formDataInEvent = null;
diff --git a/html/semantics/forms/form-submission-0/enctypes-helper.js b/html/semantics/forms/form-submission-0/enctypes-helper.js
new file mode 100644
index 00000000000000..53b76b019545a5
--- /dev/null
+++ b/html/semantics/forms/form-submission-0/enctypes-helper.js
@@ -0,0 +1,138 @@
+(() => {
+ // Using echo-content-escaped.py rather than
+ // /fetch/api/resources/echo-content.py to work around WebKit not
+ // percent-encoding \x00, which causes the response to be detected as
+ // a binary file and served as a download.
+ const ACTION_URL = "/FileAPI/file/resources/echo-content-escaped.py";
+
+ const IFRAME_NAME = "formtargetframe";
+
+ // Undoes the escapes from /fetch/api/resources/echo-content.py
+ function unescape(str) {
+ return str
+ .replace(/\r\n?|\n/g, "\r\n")
+ .replace(
+ /\\x[0-9A-Fa-f]{2}/g,
+ (escape) => String.fromCodePoint(parseInt(escape.substring(2), 16)),
+ )
+ .replace(/\\\\/g, "\\");
+ }
+
+ // `expectedBuilder` is a function that takes in the actual form body
+ // (necessary to get the multipart/form-data payload) and returns the form
+ // body that should be expected.
+ // If `testFormData` is false, the form entry will be submitted in for
+ // controls. If it is true, it will submitted by modifying the entry list
+ // during the `formdata` event.
+ async function formSubmissionTest({
+ name,
+ value,
+ expectedBuilder,
+ enctype,
+ formEncoding,
+ testFormData = false,
+ testCase,
+ }) {
+ if (document.readyState !== "complete") {
+ await new Promise((resolve) => addEventListener("load", resolve));
+ }
+
+ const formTargetFrame = Object.assign(document.createElement("iframe"), {
+ name: IFRAME_NAME,
+ });
+ document.body.append(formTargetFrame);
+ testCase.add_cleanup(() => {
+ document.body.removeChild(formTargetFrame);
+ });
+
+ const form = Object.assign(document.createElement("form"), {
+ acceptCharset: formEncoding,
+ action: ACTION_URL,
+ method: "POST",
+ enctype,
+ target: IFRAME_NAME,
+ });
+ document.body.append(form);
+ testCase.add_cleanup(() => {
+ document.body.removeChild(form);
+ });
+
+ if (!testFormData) {
+ const input = document.createElement("input");
+ input.name = name;
+ if (value instanceof File) {
+ input.type = "file";
+ const dataTransfer = new DataTransfer();
+ dataTransfer.items.add(value);
+ input.files = dataTransfer.files;
+ } else {
+ input.type = "hidden";
+ input.value = value;
+ }
+ form.append(input);
+ } else {
+ form.addEventListener("formdata", (evt) => {
+ evt.formData.append(name, value);
+ });
+ }
+
+ await new Promise((resolve) => {
+ form.submit();
+ formTargetFrame.onload = resolve;
+ });
+
+ const serialized = unescape(
+ formTargetFrame.contentDocument.body.textContent,
+ );
+ const expected = expectedBuilder(serialized);
+ assert_equals(serialized, expected);
+ }
+
+ // This function returns a function to add individual form tests corresponding
+ // to some enctype.
+ // `expectedBuilder` is a function that takes two parameters: `expected` (the
+ // `expected` value of a test) and `serialized` (the actual form body
+ // submitted by the browser), and returns the correct form body that should
+ // have been submitted. This is necessary in order to account for th
+ // multipart/form-data boundary.
+ window.formSubmissionTemplate = (enctype, expectedBuilder) => {
+ function form({
+ name,
+ value,
+ expected,
+ formEncoding = "utf-8",
+ description,
+ }) {
+ const commonParams = {
+ name,
+ value,
+ expectedBuilder: expectedBuilder.bind(null, expected),
+ enctype,
+ formEncoding,
+ };
+
+ // Normal form
+ promise_test(
+ (testCase) =>
+ formSubmissionTest({
+ ...commonParams,
+ testCase,
+ }),
+ `${enctype}: ${description} (normal form)`,
+ );
+
+ // formdata event
+ promise_test(
+ (testCase) =>
+ formSubmissionTest({
+ ...commonParams,
+ testFormData: true,
+ testCase,
+ }),
+ `${enctype}: ${description} (formdata event)`,
+ );
+ }
+
+ return form;
+ };
+})();
diff --git a/html/semantics/forms/form-submission-0/multipart-formdata.window.js b/html/semantics/forms/form-submission-0/multipart-formdata.window.js
new file mode 100644
index 00000000000000..f26c0723dba4bf
--- /dev/null
+++ b/html/semantics/forms/form-submission-0/multipart-formdata.window.js
@@ -0,0 +1,343 @@
+// META: script=enctypes-helper.js
+
+// Form submissions in multipart/form-data are also tested in
+// /FileAPI/file/send-file*
+
+const form = formSubmissionTemplate(
+ "multipart/form-data",
+ ({ name, filename, value }, serialized) => {
+ let headers;
+ if (filename === undefined) {
+ headers = [`Content-Disposition: form-data; name="${name}"`];
+ } else {
+ headers = [
+ `Content-Disposition: form-data; name="${name}"; filename="${filename}"`,
+ "Content-Type: text/plain",
+ ];
+ }
+
+ const boundary = serialized.split("\r\n")[0];
+
+ return [
+ boundary,
+ ...headers,
+ "",
+ value,
+ boundary + "--",
+ "",
+ ].join("\r\n");
+ },
+);
+
+form({
+ name: "basic",
+ value: "test",
+ expected: {
+ name: "basic",
+ value: "test",
+ },
+ description: "Basic test",
+});
+
+form({
+ name: "basic",
+ value: new File([], "file-test.txt", { type: "text/plain" }),
+ expected: {
+ name: "basic",
+ filename: "file-test.txt",
+ value: "",
+ },
+ description: "Basic File test",
+});
+
+form({
+ name: "a\0b",
+ value: "c",
+ expected: {
+ name: "a\0b",
+ value: "c",
+ },
+ description: "0x00 in name",
+});
+
+form({
+ name: "a",
+ value: "b\0c",
+ expected: {
+ name: "a",
+ value: "b\0c",
+ },
+ description: "0x00 in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\0c", { type: "text/plain" }),
+ expected: {
+ name: "a",
+ filename: "b\0c",
+ value: "",
+ },
+ description: "0x00 in filename",
+});
+
+form({
+ name: "a\nb",
+ value: "c",
+ expected: {
+ name: "a%0D%0Ab",
+ value: "c",
+ },
+ description: "\\n in name",
+});
+
+form({
+ name: "a\rb",
+ value: "c",
+ expected: {
+ name: "a%0D%0Ab",
+ value: "c",
+ },
+ description: "\\r in name",
+});
+
+form({
+ name: "a\r\nb",
+ value: "c",
+ expected: {
+ name: "a%0D%0Ab",
+ value: "c",
+ },
+ description: "\\r\\n in name",
+});
+
+form({
+ name: "a\n\rb",
+ value: "c",
+ expected: {
+ name: "a%0D%0A%0D%0Ab",
+ value: "c",
+ },
+ description: "\\n\\r in name",
+});
+
+form({
+ name: "a",
+ value: "b\nc",
+ expected: {
+ name: "a",
+ value: "b\r\nc",
+ },
+ description: "\\n in value",
+});
+
+form({
+ name: "a",
+ value: "b\rc",
+ expected: {
+ name: "a",
+ value: "b\r\nc",
+ },
+ description: "\\r in value",
+});
+
+form({
+ name: "a",
+ value: "b\r\nc",
+ expected: {
+ name: "a",
+ value: "b\r\nc",
+ },
+ description: "\\r\\n in value",
+});
+
+form({
+ name: "a",
+ value: "b\n\rc",
+ expected: {
+ name: "a",
+ value: "b\r\n\r\nc",
+ },
+ description: "\\n\\r in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\nc", { type: "text/plain" }),
+ expected: {
+ name: "a",
+ filename: "b%0Ac",
+ value: "",
+ },
+ description: "\\n in filename",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\rc", { type: "text/plain" }),
+ expected: {
+ name: "a",
+ filename: "b%0Dc",
+ value: "",
+ },
+ description: "\\r in filename",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\r\nc", { type: "text/plain" }),
+ expected: {
+ name: "a",
+ filename: "b%0D%0Ac",
+ value: "",
+ },
+ description: "\\r\\n in filename",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\n\rc", { type: "text/plain" }),
+ expected: {
+ name: "a",
+ filename: "b%0A%0Dc",
+ value: "",
+ },
+ description: "\\n\\r in filename",
+});
+
+form({
+ name: 'a"b',
+ value: "c",
+ expected: {
+ name: "a%22b",
+ value: "c",
+ },
+ description: "double quote in name",
+});
+
+form({
+ name: "a",
+ value: 'b"c',
+ expected: {
+ name: "a",
+ value: 'b"c',
+ },
+ description: "double quote in value",
+});
+
+form({
+ name: "a",
+ value: new File([], 'b"c', { type: "text/plain" }),
+ expected: {
+ name: "a",
+ filename: "b%22c",
+ value: "",
+ },
+ description: "double quote in filename",
+});
+
+form({
+ name: "a'b",
+ value: "c",
+ expected: {
+ name: "a'b",
+ value: "c",
+ },
+ description: "single quote in name",
+});
+
+form({
+ name: "a",
+ value: "b'c",
+ expected: {
+ name: "a",
+ value: "b'c",
+ },
+ description: "single quote in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b'c", { type: "text/plain" }),
+ expected: {
+ name: "a",
+ filename: "b'c",
+ value: "",
+ },
+ description: "single quote in filename",
+});
+
+form({
+ name: "a\\b",
+ value: "c",
+ expected: {
+ name: "a\\b",
+ value: "c",
+ },
+ description: "backslash in name",
+});
+
+form({
+ name: "a",
+ value: "b\\c",
+ expected: {
+ name: "a",
+ value: "b\\c",
+ },
+ description: "backslash in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\\c", { type: "text/plain" }),
+ expected: {
+ name: "a",
+ filename: "b\\c",
+ value: "",
+ },
+ description: "backslash in filename",
+});
+
+form({
+ name: "áb",
+ value: "ç",
+ expected: {
+ name: "\xC3\xA1b",
+ value: "\xC3\xA7",
+ },
+ description: "non-ASCII in name and value",
+});
+
+form({
+ name: "a",
+ value: new File([], "ə.txt", { type: "text/plain" }),
+ expected: {
+ name: "a",
+ filename: "\xC9\x99.txt",
+ value: "",
+ },
+ description: "non-ASCII in filename",
+});
+
+form({
+ name: "aəb",
+ value: "c\uFFFDd",
+ formEncoding: "windows-1252",
+ expected: {
+ name: "aəb",
+ value: "c�d",
+ },
+ description: "characters not in encoding in name and value",
+});
+
+form({
+ name: "á",
+ value: new File([], "💩", { type: "text/plain" }),
+ formEncoding: "windows-1252",
+ expected: {
+ name: "\xE1",
+ filename: "💩",
+ value: "",
+ },
+ description: "character not in encoding in filename",
+});
diff --git a/html/semantics/forms/form-submission-0/text-plain.window.js b/html/semantics/forms/form-submission-0/text-plain.window.js
new file mode 100644
index 00000000000000..54bca9e169c062
--- /dev/null
+++ b/html/semantics/forms/form-submission-0/text-plain.window.js
@@ -0,0 +1,218 @@
+// META: script=enctypes-helper.js
+
+const form = formSubmissionTemplate(
+ "text/plain",
+ (expected) => expected,
+);
+
+form({
+ name: "basic",
+ value: "test",
+ expected: "basic=test\r\n",
+ description: "Basic test",
+});
+
+form({
+ name: "basic",
+ value: new File([], "file-test.txt"),
+ expected: "basic=file-test.txt\r\n",
+ description: "Basic File test",
+});
+
+form({
+ name: "a\0b",
+ value: "c",
+ expected: "a\0b=c\r\n",
+ description: "0x00 in name",
+});
+
+form({
+ name: "a",
+ value: "b\0c",
+ expected: "a=b\0c\r\n",
+ description: "0x00 in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\0c"),
+ expected: "a=b\0c\r\n",
+ description: "0x00 in filename",
+});
+
+form({
+ name: "a\nb",
+ value: "c",
+ expected: "a\r\nb=c\r\n",
+ description: "\\n in name",
+});
+
+form({
+ name: "a\rb",
+ value: "c",
+ expected: "a\r\nb=c\r\n",
+ description: "\\r in name",
+});
+
+form({
+ name: "a\r\nb",
+ value: "c",
+ expected: "a\r\nb=c\r\n",
+ description: "\\r\\n in name",
+});
+
+form({
+ name: "a\n\rb",
+ value: "c",
+ expected: "a\r\n\r\nb=c\r\n",
+ description: "\\n\\r in name",
+});
+
+form({
+ name: "a",
+ value: "b\nc",
+ expected: "a=b\r\nc\r\n",
+ description: "\\n in value",
+});
+
+form({
+ name: "a",
+ value: "b\rc",
+ expected: "a=b\r\nc\r\n",
+ description: "\\r in value",
+});
+
+form({
+ name: "a",
+ value: "b\r\nc",
+ expected: "a=b\r\nc\r\n",
+ description: "\\r\\n in value",
+});
+
+form({
+ name: "a",
+ value: "b\n\rc",
+ expected: "a=b\r\n\r\nc\r\n",
+ description: "\\n\\r in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\nc"),
+ expected: "a=b\r\nc\r\n",
+ description: "\\n in filename",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\rc"),
+ expected: "a=b\r\nc\r\n",
+ description: "\\r in filename",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\r\nc"),
+ expected: "a=b\r\nc\r\n",
+ description: "\\r\\n in filename",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\n\rc"),
+ expected: "a=b\r\n\r\nc\r\n",
+ description: "\\n\\r in filename",
+});
+
+form({
+ name: 'a"b',
+ value: "c",
+ expected: 'a"b=c\r\n',
+ description: "double quote in name",
+});
+
+form({
+ name: "a",
+ value: 'b"c',
+ expected: 'a=b"c\r\n',
+ description: "double quote in value",
+});
+
+form({
+ name: "a",
+ value: new File([], 'b"c'),
+ expected: 'a=b"c\r\n',
+ description: "double quote in filename",
+});
+
+form({
+ name: "a'b",
+ value: "c",
+ expected: "a'b=c\r\n",
+ description: "single quote in name",
+});
+
+form({
+ name: "a",
+ value: "b'c",
+ expected: "a=b'c\r\n",
+ description: "single quote in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b'c"),
+ expected: "a=b'c\r\n",
+ description: "single quote in filename",
+});
+
+form({
+ name: "a\\b",
+ value: "c",
+ expected: "a\\b=c\r\n",
+ description: "backslash in name",
+});
+
+form({
+ name: "a",
+ value: "b\\c",
+ expected: "a=b\\c\r\n",
+ description: "backslash in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\\c"),
+ expected: "a=b\\c\r\n",
+ description: "backslash in filename",
+});
+
+form({
+ name: "áb",
+ value: "ç",
+ expected: "\xC3\xA1b=\xC3\xA7\r\n",
+ description: "non-ASCII in name and value",
+});
+
+form({
+ name: "a",
+ value: new File([], "ə.txt"),
+ expected: "a=\xC9\x99.txt\r\n",
+ description: "non-ASCII in filename",
+});
+
+form({
+ name: "aəb",
+ value: "c\uFFFDd",
+ formEncoding: "windows-1252",
+ expected: "aəb=c�d\r\n",
+ description: "characters not in encoding in name and value",
+});
+
+form({
+ name: "á",
+ value: new File([], "💩"),
+ formEncoding: "windows-1252",
+ expected: "\xE1=💩\r\n",
+ description: "character not in encoding in filename",
+});
diff --git a/html/semantics/forms/form-submission-0/urlencoded2.window.js b/html/semantics/forms/form-submission-0/urlencoded2.window.js
new file mode 100644
index 00000000000000..df86abb093dc56
--- /dev/null
+++ b/html/semantics/forms/form-submission-0/urlencoded2.window.js
@@ -0,0 +1,218 @@
+// META: script=enctypes-helper.js
+
+const form = formSubmissionTemplate(
+ "application/x-www-form-urlencoded",
+ (expected) => expected,
+);
+
+form({
+ name: "basic",
+ value: "test",
+ expected: "basic=test",
+ description: "Basic test",
+});
+
+form({
+ name: "basic",
+ value: new File([], "file-test.txt"),
+ expected: "basic=file-test.txt",
+ description: "Basic File test",
+});
+
+form({
+ name: "a\0b",
+ value: "c",
+ expected: "a%00b=c",
+ description: "0x00 in name",
+});
+
+form({
+ name: "a",
+ value: "b\0c",
+ expected: "a=b%00c",
+ description: "0x00 in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\0c"),
+ expected: "a=b%00c",
+ description: "0x00 in filename",
+});
+
+form({
+ name: "a\nb",
+ value: "c",
+ expected: "a%0D%0Ab=c",
+ description: "\\n in name",
+});
+
+form({
+ name: "a\rb",
+ value: "c",
+ expected: "a%0D%0Ab=c",
+ description: "\\r in name",
+});
+
+form({
+ name: "a\r\nb",
+ value: "c",
+ expected: "a%0D%0Ab=c",
+ description: "\\r\\n in name",
+});
+
+form({
+ name: "a\n\rb",
+ value: "c",
+ expected: "a%0D%0A%0D%0Ab=c",
+ description: "\\n\\r in name",
+});
+
+form({
+ name: "a",
+ value: "b\nc",
+ expected: "a=b%0D%0Ac",
+ description: "\\n in value",
+});
+
+form({
+ name: "a",
+ value: "b\rc",
+ expected: "a=b%0D%0Ac",
+ description: "\\r in value",
+});
+
+form({
+ name: "a",
+ value: "b\r\nc",
+ expected: "a=b%0D%0Ac",
+ description: "\\r\\n in value",
+});
+
+form({
+ name: "a",
+ value: "b\n\rc",
+ expected: "a=b%0D%0A%0D%0Ac",
+ description: "\\n\\r in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\nc"),
+ expected: "a=b%0D%0Ac",
+ description: "\\n in filename",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\rc"),
+ expected: "a=b%0D%0Ac",
+ description: "\\r in filename",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\r\nc"),
+ expected: "a=b%0D%0Ac",
+ description: "\\r\\n in filename",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\n\rc"),
+ expected: "a=b%0D%0A%0D%0Ac",
+ description: "\\n\\r in filename",
+});
+
+form({
+ name: 'a"b',
+ value: "c",
+ expected: "a%22b=c",
+ description: "double quote in name",
+});
+
+form({
+ name: "a",
+ value: 'b"c',
+ expected: "a=b%22c",
+ description: "double quote in value",
+});
+
+form({
+ name: "a",
+ value: new File([], 'b"c'),
+ expected: "a=b%22c",
+ description: "double quote in filename",
+});
+
+form({
+ name: "a'b",
+ value: "c",
+ expected: "a%27b=c",
+ description: "single quote in name",
+});
+
+form({
+ name: "a",
+ value: "b'c",
+ expected: "a=b%27c",
+ description: "single quote in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b'c"),
+ expected: "a=b%27c",
+ description: "single quote in filename",
+});
+
+form({
+ name: "a\\b",
+ value: "c",
+ expected: "a%5Cb=c",
+ description: "backslash in name",
+});
+
+form({
+ name: "a",
+ value: "b\\c",
+ expected: "a=b%5Cc",
+ description: "backslash in value",
+});
+
+form({
+ name: "a",
+ value: new File([], "b\\c"),
+ expected: "a=b%5Cc",
+ description: "backslash in filename",
+});
+
+form({
+ name: "áb",
+ value: "ç",
+ expected: "%C3%A1b=%C3%A7",
+ description: "non-ASCII in name and value",
+});
+
+form({
+ name: "a",
+ value: new File([], "ə.txt"),
+ expected: "a=%C9%99.txt",
+ description: "non-ASCII in filename",
+});
+
+form({
+ name: "aəb",
+ value: "c\uFFFDd",
+ formEncoding: "windows-1252",
+ expected: "a%26%23601%3Bb=c%26%2365533%3Bd",
+ description: "characters not in encoding in name and value",
+});
+
+form({
+ name: "á",
+ value: new File([], "💩"),
+ formEncoding: "windows-1252",
+ expected: "%E1=%26%23128169%3B",
+ description: "character not in encoding in filename",
+});
diff --git a/url/urlsearchparams-stringifier.any.js b/url/urlsearchparams-stringifier.any.js
index bc7bedd533f58d..6187db64b1747d 100644
--- a/url/urlsearchparams-stringifier.any.js
+++ b/url/urlsearchparams-stringifier.any.js
@@ -133,3 +133,13 @@ test(() => {
assert_equals(url.toString(), 'http://www.example.com/?a=b%2Cc&x=y');
assert_equals(params.toString(), 'a=b%2Cc&x=y');
}, 'URLSearchParams connected to URL');
+
+test(() => {
+ const url = new URL('http://www.example.com/');
+ const params = url.searchParams;
+
+ params.append('a\nb', 'c\rd');
+ params.append('e\n\rf', 'g\r\nh');
+
+ assert_equals(params.toString(), "a%0Ab=c%0Dd&e%0A%0Df=g%0D%0Ah");
+}, 'URLSearchParams must not do newline normalization');