From f3d6a755a388f686027d2d766958496ffcb179a3 Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Fri, 7 Oct 2022 15:26:26 -0400 Subject: [PATCH] feat: add all File WPTs (#1687) --- lib/fetch/file.js | 11 +- lib/fetch/webidl.js | 4 +- package.json | 2 +- test/wpt/runner/runner/runner.mjs | 5 + test/wpt/server/server.mjs | 13 ++ .../{runner/start.mjs => start-FileAPI.mjs} | 9 +- test/wpt/start-fetch.mjs | 24 +++ test/wpt/status/FileAPI.status.json | 1 + .../FileAPI/file/File-constructor.any.js | 155 ++++++++++++++++++ .../file/send-file-formdata-controls.any.js | 69 ++++++++ .../send-file-formdata-punctuation.any.js | 144 ++++++++++++++++ .../file/send-file-formdata-utf-8.any.js | 33 ++++ .../FileAPI/file/send-file-formdata.any.js | 8 + .../support/send-file-formdata-helper.js | 99 +++++++++++ 14 files changed, 566 insertions(+), 11 deletions(-) rename test/wpt/{runner/start.mjs => start-FileAPI.mjs} (67%) create mode 100644 test/wpt/start-fetch.mjs create mode 100644 test/wpt/status/FileAPI.status.json create mode 100644 test/wpt/tests/FileAPI/file/File-constructor.any.js create mode 100644 test/wpt/tests/FileAPI/file/send-file-formdata-controls.any.js create mode 100644 test/wpt/tests/FileAPI/file/send-file-formdata-punctuation.any.js create mode 100644 test/wpt/tests/FileAPI/file/send-file-formdata-utf-8.any.js create mode 100644 test/wpt/tests/FileAPI/file/send-file-formdata.any.js create mode 100644 test/wpt/tests/FileAPI/support/send-file-formdata-helper.js diff --git a/lib/fetch/file.js b/lib/fetch/file.js index 2720831c180..2a96fa50047 100644 --- a/lib/fetch/file.js +++ b/lib/fetch/file.js @@ -202,10 +202,15 @@ webidl.converters.BlobPart = function (V, opts) { return webidl.converters.Blob(V, { strict: false }) } - return webidl.converters.BufferSource(V, opts) - } else { - return webidl.converters.USVString(V, opts) + if ( + ArrayBuffer.isView(V) || + types.isAnyArrayBuffer(V) + ) { + return webidl.converters.BufferSource(V, opts) + } } + + return webidl.converters.USVString(V, opts) } webidl.converters['sequence'] = webidl.sequenceConverter( diff --git a/lib/fetch/webidl.js b/lib/fetch/webidl.js index b05bd62b0db..7c6c147e277 100644 --- a/lib/fetch/webidl.js +++ b/lib/fetch/webidl.js @@ -339,7 +339,9 @@ webidl.dictionaryConverter = function (converters) { const type = webidl.util.Type(dictionary) const dict = {} - if (type !== 'Null' && type !== 'Undefined' && type !== 'Object') { + if (type === 'Null' || type === 'Undefined') { + return dict + } else if (type !== 'Object') { webidl.errors.exception({ header: 'Dictionary', message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` diff --git a/package.json b/package.json index 823c48b8d3e..8490063ab09 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "test:tap": "tap test/*.js test/diagnostics-channel/*.js", "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", "test:typescript": "tsd", - "test:wpt": "node scripts/verifyVersion 18 || node test/wpt/runner/start.mjs", + "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs)", "coverage": "nyc --reporter=text --reporter=html npm run test", "coverage:ci": "nyc --reporter=lcov npm run test", "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run", diff --git a/test/wpt/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index 8ed5921b978..7bf77d6e019 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -10,6 +10,9 @@ const testPath = join(basePath, 'tests') const statusPath = join(basePath, 'status') export class WPTRunner extends EventEmitter { + /** @type {string} */ + #folderName + /** @type {string} */ #folderPath @@ -35,6 +38,7 @@ export class WPTRunner extends EventEmitter { constructor (folder, url) { super() + this.#folderName = folder this.#folderPath = join(testPath, folder) this.#files.push(...WPTRunner.walk( this.#folderPath, @@ -105,6 +109,7 @@ export class WPTRunner extends EventEmitter { this.emit('completion') const { completed, failed, success, expectedFailures } = this.#stats console.log( + `[${this.#folderName}]: ` + `Completed: ${completed}, failed: ${failed}, success: ${success}, ` + `expected failures: ${expectedFailures}, ` + `unexpected failures: ${failed - expectedFailures}` diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index 3e71b71cf33..3774465edac 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -111,6 +111,19 @@ const server = createServer(async (req, res) => { res.write(JSON.stringify(took)) return res.end() } + case '/fetch/api/resources/echo-content.py': { + res.setHeader('X-Request-Method', req.method) + res.setHeader('X-Request-Content-Length', req.headers['content-length'] ?? 'NO') + res.setHeader('X-Request-Content-Type', req.headers['content-type'] ?? 'NO') + res.setHeader('Content-Type', 'text/plain') + + for await (const chunk of req) { + res.write(chunk) + } + + res.end() + break + } default: { res.statusCode = 200 res.end('body') diff --git a/test/wpt/runner/start.mjs b/test/wpt/start-FileAPI.mjs similarity index 67% rename from test/wpt/runner/start.mjs rename to test/wpt/start-FileAPI.mjs index 9ce32f6e8b8..e601afc1120 100644 --- a/test/wpt/runner/start.mjs +++ b/test/wpt/start-FileAPI.mjs @@ -1,21 +1,18 @@ -import { WPTRunner } from './runner/runner.mjs' +import { WPTRunner } from './runner/runner/runner.mjs' import { join } from 'path' import { fileURLToPath } from 'url' import { fork } from 'child_process' import { on } from 'events' -const serverPath = fileURLToPath(join(import.meta.url, '../../server/server.mjs')) +const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs')) const child = fork(serverPath, [], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }) -/** @type {WPTRunner} */ -let runner - for await (const [message] of on(child, 'message')) { if (message.server) { - runner = new WPTRunner('fetch', message.server) + const runner = new WPTRunner('FileAPI', message.server) runner.run() runner.once('completion', () => { diff --git a/test/wpt/start-fetch.mjs b/test/wpt/start-fetch.mjs new file mode 100644 index 00000000000..55d3f2e53ae --- /dev/null +++ b/test/wpt/start-fetch.mjs @@ -0,0 +1,24 @@ +import { WPTRunner } from './runner/runner/runner.mjs' +import { join } from 'path' +import { fileURLToPath } from 'url' +import { fork } from 'child_process' +import { on } from 'events' + +const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs')) + +const child = fork(serverPath, [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] +}) + +for await (const [message] of on(child, 'message')) { + if (message.server) { + const runner = new WPTRunner('fetch', message.server) + runner.run() + + runner.once('completion', () => { + child.send('shutdown') + }) + } else if (message.message === 'shutdown') { + process.exit() + } +} diff --git a/test/wpt/status/FileAPI.status.json b/test/wpt/status/FileAPI.status.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/test/wpt/status/FileAPI.status.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/wpt/tests/FileAPI/file/File-constructor.any.js b/test/wpt/tests/FileAPI/file/File-constructor.any.js new file mode 100644 index 00000000000..0b0185c40bf --- /dev/null +++ b/test/wpt/tests/FileAPI/file/File-constructor.any.js @@ -0,0 +1,155 @@ +// META: title=File constructor + +const to_string_obj = { toString: () => 'a string' }; +const to_string_throws = { toString: () => { throw new Error('expected'); } }; + +test(function() { + assert_true("File" in globalThis, "globalThis should have a File property."); +}, "File interface object exists"); + +test(t => { + assert_throws_js(TypeError, () => new File(), + 'Bits argument is required'); + assert_throws_js(TypeError, () => new File([]), + 'Name argument is required'); +}, 'Required arguments'); + +function test_first_argument(arg1, expectedSize, testName) { + test(function() { + var file = new File(arg1, "dummy"); + assert_true(file instanceof File); + assert_equals(file.name, "dummy"); + assert_equals(file.size, expectedSize); + assert_equals(file.type, ""); + // assert_false(file.isClosed); XXX: File.isClosed doesn't seem to be implemented + assert_not_equals(file.lastModified, ""); + }, testName); +} + +test_first_argument([], 0, "empty fileBits"); +test_first_argument(["bits"], 4, "DOMString fileBits"); +test_first_argument(["๐“ฝ๐“ฎ๐”๐“ฝ"], 16, "Unicode DOMString fileBits"); +test_first_argument([new String('string object')], 13, "String object fileBits"); +test_first_argument([new Blob()], 0, "Empty Blob fileBits"); +test_first_argument([new Blob(["bits"])], 4, "Blob fileBits"); +test_first_argument([new File([], 'world.txt')], 0, "Empty File fileBits"); +test_first_argument([new File(["bits"], 'world.txt')], 4, "File fileBits"); +test_first_argument([new ArrayBuffer(8)], 8, "ArrayBuffer fileBits"); +test_first_argument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4, "Typed array fileBits"); +test_first_argument(["bits", new Blob(["bits"]), new Blob(), new Uint8Array([0x50, 0x41]), + new Uint16Array([0x5353]), new Uint32Array([0x53534150])], 16, "Various fileBits"); +test_first_argument([12], 2, "Number in fileBits"); +test_first_argument([[1,2,3]], 5, "Array in fileBits"); +test_first_argument([{}], 15, "Object in fileBits"); // "[object Object]" +if (globalThis.document !== undefined) { + test_first_argument([document.body], 24, "HTMLBodyElement in fileBits"); // "[object HTMLBodyElement]" +} +test_first_argument([to_string_obj], 8, "Object with toString in fileBits"); +test_first_argument({[Symbol.iterator]() { + let i = 0; + return {next: () => [ + {done:false, value:'ab'}, + {done:false, value:'cde'}, + {done:true} + ][i++]}; +}}, 5, 'Custom @@iterator'); + +[ + 'hello', + 0, + null +].forEach(arg => { + test(t => { + assert_throws_js(TypeError, () => new File(arg, 'world.html'), + 'Constructor should throw for invalid bits argument'); + }, `Invalid bits argument: ${JSON.stringify(arg)}`); +}); + +test(t => { + assert_throws_js(Error, () => new File([to_string_throws], 'name.txt'), + 'Constructor should propagate exceptions'); +}, 'Bits argument: object that throws'); + + +function test_second_argument(arg2, expectedFileName, testName) { + test(function() { + var file = new File(["bits"], arg2); + assert_true(file instanceof File); + assert_equals(file.name, expectedFileName); + }, testName); +} + +test_second_argument("dummy", "dummy", "Using fileName"); +test_second_argument("dummy/foo", "dummy/foo", + "No replacement when using special character in fileName"); +test_second_argument(null, "null", "Using null fileName"); +test_second_argument(1, "1", "Using number fileName"); +test_second_argument('', '', "Using empty string fileName"); +if (globalThis.document !== undefined) { + test_second_argument(document.body, '[object HTMLBodyElement]', "Using object fileName"); +} + +// testing the third argument +[ + {type: 'text/plain', expected: 'text/plain'}, + {type: 'text/plain;charset=UTF-8', expected: 'text/plain;charset=utf-8'}, + {type: 'TEXT/PLAIN', expected: 'text/plain'}, + {type: '๐“ฝ๐“ฎ๐”๐“ฝ/๐”ญ๐”ฉ๐”ž๐”ฆ๐”ซ', expected: ''}, + {type: 'ascii/nonprintable\u001F', expected: ''}, + {type: 'ascii/nonprintable\u007F', expected: ''}, + {type: 'nonascii\u00EE', expected: ''}, + {type: 'nonascii\u1234', expected: ''}, + {type: 'nonparsable', expected: 'nonparsable'} +].forEach(testCase => { + test(t => { + var file = new File(["bits"], "dummy", { type: testCase.type}); + assert_true(file instanceof File); + assert_equals(file.type, testCase.expected); + }, `Using type in File constructor: ${testCase.type}`); +}); +test(function() { + var file = new File(["bits"], "dummy", { lastModified: 42 }); + assert_true(file instanceof File); + assert_equals(file.lastModified, 42); +}, "Using lastModified"); +test(function() { + var file = new File(["bits"], "dummy", { name: "foo" }); + assert_true(file instanceof File); + assert_equals(file.name, "dummy"); +}, "Misusing name"); +test(function() { + var file = new File(["bits"], "dummy", { unknownKey: "value" }); + assert_true(file instanceof File); + assert_equals(file.name, "dummy"); +}, "Unknown properties are ignored"); + +[ + 123, + 123.4, + true, + 'abc' +].forEach(arg => { + test(t => { + assert_throws_js(TypeError, () => new File(['bits'], 'name.txt', arg), + 'Constructor should throw for invalid property bag type'); + }, `Invalid property bag: ${JSON.stringify(arg)}`); +}); + +[ + null, + undefined, + [1,2,3], + /regex/, + function() {} +].forEach(arg => { + test(t => { + assert_equals(new File(['bits'], 'name.txt', arg).size, 4, + 'Constructor should accept object-ish property bag type'); + }, `Unusual but valid property bag: ${arg}`); +}); + +test(t => { + assert_throws_js(Error, + () => new File(['bits'], 'name.txt', {type: to_string_throws}), + 'Constructor should propagate exceptions'); +}, 'Property bag propagates exceptions'); diff --git a/test/wpt/tests/FileAPI/file/send-file-formdata-controls.any.js b/test/wpt/tests/FileAPI/file/send-file-formdata-controls.any.js new file mode 100644 index 00000000000..e95d3aada44 --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-formdata-controls.any.js @@ -0,0 +1,69 @@ +// META: title=FormData: FormData: Upload files named using controls +// META: script=../support/send-file-formdata-helper.js + "use strict"; + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-NUL-[\0].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-BS-[\b].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-VT-[\v].txt", + }); + + // These have characters that undergo processing in name=, + // filename=, and/or value; formDataPostFileUploadTest postprocesses + // expectedEncodedBaseName for these internally. + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LF-[\n].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LF-CR-[\n\r].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-CR-[\r].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-CR-LF-[\r\n].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-HT-[\t].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-FF-[\f].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-DEL-[\x7F].txt", + }); + + // The rest should be passed through unmodified: + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-ESC-[\x1B].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-SPACE-[ ].txt", + }); diff --git a/test/wpt/tests/FileAPI/file/send-file-formdata-punctuation.any.js b/test/wpt/tests/FileAPI/file/send-file-formdata-punctuation.any.js new file mode 100644 index 00000000000..987dba39aff --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-formdata-punctuation.any.js @@ -0,0 +1,144 @@ +// META: title=FormData: FormData: Upload files named using punctuation +// META: script=../support/send-file-formdata-helper.js + "use strict"; + + // These have characters that undergo processing in name=, + // filename=, and/or value; formDataPostFileUploadTest postprocesses + // expectedEncodedBaseName for these internally. + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-QUOTATION-MARK-[\x22].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: '"file-for-upload-in-form-double-quoted.txt"', + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-REVERSE-SOLIDUS-[\\].txt", + }); + + // The rest should be passed through unmodified: + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-EXCLAMATION-MARK-[!].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-DOLLAR-SIGN-[$].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-PERCENT-SIGN-[%].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-AMPERSAND-[&].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-APOSTROPHE-['].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LEFT-PARENTHESIS-[(].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-RIGHT-PARENTHESIS-[)].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-ASTERISK-[*].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-PLUS-SIGN-[+].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-COMMA-[,].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-FULL-STOP-[.].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-SOLIDUS-[/].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-COLON-[:].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-SEMICOLON-[;].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-EQUALS-SIGN-[=].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-QUESTION-MARK-[?].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-CIRCUMFLEX-ACCENT-[^].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LEFT-SQUARE-BRACKET-[[].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-RIGHT-SQUARE-BRACKET-[]].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-LEFT-CURLY-BRACKET-[{].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-VERTICAL-LINE-[|].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-RIGHT-CURLY-BRACKET-[}].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form-TILDE-[~].txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "'file-for-upload-in-form-single-quoted.txt'", + }); diff --git a/test/wpt/tests/FileAPI/file/send-file-formdata-utf-8.any.js b/test/wpt/tests/FileAPI/file/send-file-formdata-utf-8.any.js new file mode 100644 index 00000000000..b8bd74c717a --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-formdata-utf-8.any.js @@ -0,0 +1,33 @@ +// META: title=FormData: FormData: Upload files in UTF-8 fetch() +// META: script=../support/send-file-formdata-helper.js + "use strict"; + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form.txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "x-user-defined", + fileBaseName: "file-for-upload-in-form-\uF7F0\uF793\uF783\uF7A0.txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "windows-1252", + fileBaseName: "file-for-upload-in-form-รขหœยบรฐลธหœโ€š.txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "JIS X 0201 and JIS X 0208", + fileBaseName: "file-for-upload-in-form-โ˜…ๆ˜Ÿโ˜….txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "Unicode", + fileBaseName: "file-for-upload-in-form-โ˜บ๐Ÿ˜‚.txt", + }); + + formDataPostFileUploadTest({ + fileNameSource: "Unicode", + fileBaseName: `file-for-upload-in-form-${kTestChars}.txt`, + }); diff --git a/test/wpt/tests/FileAPI/file/send-file-formdata.any.js b/test/wpt/tests/FileAPI/file/send-file-formdata.any.js new file mode 100644 index 00000000000..e13a34828a0 --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-formdata.any.js @@ -0,0 +1,8 @@ +// META: title=FormData: Upload ASCII-named file in UTF-8 form +// META: script=../support/send-file-formdata-helper.js + "use strict"; + + formDataPostFileUploadTest({ + fileNameSource: "ASCII", + fileBaseName: "file-for-upload-in-form.txt", + }); diff --git a/test/wpt/tests/FileAPI/support/send-file-formdata-helper.js b/test/wpt/tests/FileAPI/support/send-file-formdata-helper.js new file mode 100644 index 00000000000..53c8cca7e09 --- /dev/null +++ b/test/wpt/tests/FileAPI/support/send-file-formdata-helper.js @@ -0,0 +1,99 @@ +"use strict"; + +const kTestChars = "ABC~โ€พยฅโ‰ˆยค๏ฝฅใƒปโ€ขโˆ™ยทโ˜ผโ˜…ๆ˜Ÿ๐ŸŒŸๆ˜Ÿโ˜…โ˜ผยทโˆ™โ€ขใƒป๏ฝฅยคโ‰ˆยฅโ€พ~XYZ"; + +// formDataPostFileUploadTest - verifies multipart upload structure and +// numeric character reference replacement for filenames, field names, +// and field values using FormData and fetch(). +// +// Uses /fetch/api/resources/echo-content.py to echo the upload +// POST (unlike in send-file-form-helper.js, here we expect all +// multipart/form-data request bodies to be UTF-8, so we don't need to +// escape controls and non-ASCII bytes). +// +// Fields in the parameter object: +// +// - fileNameSource: purely explanatory and gives a clue about which +// character encoding is the source for the non-7-bit-ASCII parts of +// the fileBaseName, or Unicode if no smaller-than-Unicode source +// contains all the characters. Used in the test name. +// - fileBaseName: the not-necessarily-just-7-bit-ASCII file basename +// used for the constructed test file. Used in the test name. +const formDataPostFileUploadTest = ({ + fileNameSource, + fileBaseName, +}) => { + promise_test(async (testCase) => { + const formData = new FormData(); + let file = new Blob([kTestChars], { type: "text/plain" }); + try { + // Switch to File in browsers that allow this + file = new File([file], fileBaseName, { type: file.type }); + } catch (ignoredException) { + } + + // Used to verify that the browser agrees with the test about + // field value replacement and encoding independently of file system + // idiosyncracies. + formData.append("filename", fileBaseName); + + // Same, but with name and value reversed to ensure field names + // get the same treatment. + formData.append(fileBaseName, "filename"); + + formData.append("file", file, fileBaseName); + + const formDataText = await (await fetch( + `/fetch/api/resources/echo-content.py`, + { + method: "POST", + body: formData, + }, + )).text(); + const formDataLines = formDataText.split("\r\n"); + if (formDataLines.length && !formDataLines[formDataLines.length - 1]) { + --formDataLines.length; + } + assert_greater_than( + formDataLines.length, + 2, + `${fileBaseName}: multipart form data must have at least 3 lines: ${ + JSON.stringify(formDataText) + }`, + ); + const boundary = formDataLines[0]; + assert_equals( + formDataLines[formDataLines.length - 1], + boundary + "--", + `${fileBaseName}: multipart form data must end with ${boundary}--: ${ + JSON.stringify(formDataText) + }`, + ); + + const asValue = fileBaseName.replace(/\r\n?|\n/g, "\r\n"); + const asName = asValue.replace(/[\r\n"]/g, encodeURIComponent); + const asFilename = fileBaseName.replace(/[\r\n"]/g, encodeURIComponent); + const expectedText = [ + boundary, + 'Content-Disposition: form-data; name="filename"', + "", + asValue, + boundary, + `Content-Disposition: form-data; name="${asName}"`, + "", + "filename", + boundary, + `Content-Disposition: form-data; name="file"; ` + + `filename="${asFilename}"`, + "Content-Type: text/plain", + "", + kTestChars, + boundary + "--", + ].join("\r\n"); + + assert_true( + formDataText.startsWith(expectedText), + `Unexpected multipart-shaped form data received:\n${formDataText}\nExpected:\n${expectedText}`, + ); + }, `Upload ${fileBaseName} (${fileNameSource}) in fetch with FormData`); +};