Skip to content

Commit

Permalink
feat: add all File WPTs (#1687)
Browse files Browse the repository at this point in the history
  • Loading branch information
KhafraDev committed Oct 7, 2022
1 parent 552c235 commit eead2b8
Show file tree
Hide file tree
Showing 14 changed files with 566 additions and 11 deletions.
11 changes: 8 additions & 3 deletions lib/fetch/file.js
Expand Up @@ -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<BlobPart>'] = webidl.sequenceConverter(
Expand Down
4 changes: 3 additions & 1 deletion lib/fetch/webidl.js
Expand Up @@ -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.`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions test/wpt/runner/runner/runner.mjs
Expand Up @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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}`
Expand Down
13 changes: 13 additions & 0 deletions test/wpt/server/server.mjs
Expand Up @@ -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')
Expand Down
9 changes: 3 additions & 6 deletions test/wpt/runner/start.mjs → 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', () => {
Expand Down
24 changes: 24 additions & 0 deletions 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()
}
}
1 change: 1 addition & 0 deletions test/wpt/status/FileAPI.status.json
@@ -0,0 +1 @@
{}
155 changes: 155 additions & 0 deletions 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');
69 changes: 69 additions & 0 deletions 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",
});

0 comments on commit eead2b8

Please sign in to comment.