Skip to content

Commit eead2b8

Browse files
authored
feat: add all File WPTs (#1687)
1 parent 552c235 commit eead2b8

14 files changed

+566
-11
lines changed

lib/fetch/file.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,15 @@ webidl.converters.BlobPart = function (V, opts) {
202202
return webidl.converters.Blob(V, { strict: false })
203203
}
204204

205-
return webidl.converters.BufferSource(V, opts)
206-
} else {
207-
return webidl.converters.USVString(V, opts)
205+
if (
206+
ArrayBuffer.isView(V) ||
207+
types.isAnyArrayBuffer(V)
208+
) {
209+
return webidl.converters.BufferSource(V, opts)
210+
}
208211
}
212+
213+
return webidl.converters.USVString(V, opts)
209214
}
210215

211216
webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter(

lib/fetch/webidl.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,9 @@ webidl.dictionaryConverter = function (converters) {
339339
const type = webidl.util.Type(dictionary)
340340
const dict = {}
341341

342-
if (type !== 'Null' && type !== 'Undefined' && type !== 'Object') {
342+
if (type === 'Null' || type === 'Undefined') {
343+
return dict
344+
} else if (type !== 'Object') {
343345
webidl.errors.exception({
344346
header: 'Dictionary',
345347
message: `Expected ${dictionary} to be one of: Null, Undefined, Object.`

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"test:tap": "tap test/*.js test/diagnostics-channel/*.js",
5454
"test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w",
5555
"test:typescript": "tsd",
56-
"test:wpt": "node scripts/verifyVersion 18 || node test/wpt/runner/start.mjs",
56+
"test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs)",
5757
"coverage": "nyc --reporter=text --reporter=html npm run test",
5858
"coverage:ci": "nyc --reporter=lcov npm run test",
5959
"bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run",

test/wpt/runner/runner/runner.mjs

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const testPath = join(basePath, 'tests')
1010
const statusPath = join(basePath, 'status')
1111

1212
export class WPTRunner extends EventEmitter {
13+
/** @type {string} */
14+
#folderName
15+
1316
/** @type {string} */
1417
#folderPath
1518

@@ -35,6 +38,7 @@ export class WPTRunner extends EventEmitter {
3538
constructor (folder, url) {
3639
super()
3740

41+
this.#folderName = folder
3842
this.#folderPath = join(testPath, folder)
3943
this.#files.push(...WPTRunner.walk(
4044
this.#folderPath,
@@ -105,6 +109,7 @@ export class WPTRunner extends EventEmitter {
105109
this.emit('completion')
106110
const { completed, failed, success, expectedFailures } = this.#stats
107111
console.log(
112+
`[${this.#folderName}]: ` +
108113
`Completed: ${completed}, failed: ${failed}, success: ${success}, ` +
109114
`expected failures: ${expectedFailures}, ` +
110115
`unexpected failures: ${failed - expectedFailures}`

test/wpt/server/server.mjs

+13
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ const server = createServer(async (req, res) => {
111111
res.write(JSON.stringify(took))
112112
return res.end()
113113
}
114+
case '/fetch/api/resources/echo-content.py': {
115+
res.setHeader('X-Request-Method', req.method)
116+
res.setHeader('X-Request-Content-Length', req.headers['content-length'] ?? 'NO')
117+
res.setHeader('X-Request-Content-Type', req.headers['content-type'] ?? 'NO')
118+
res.setHeader('Content-Type', 'text/plain')
119+
120+
for await (const chunk of req) {
121+
res.write(chunk)
122+
}
123+
124+
res.end()
125+
break
126+
}
114127
default: {
115128
res.statusCode = 200
116129
res.end('body')

test/wpt/runner/start.mjs test/wpt/start-FileAPI.mjs

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
1-
import { WPTRunner } from './runner/runner.mjs'
1+
import { WPTRunner } from './runner/runner/runner.mjs'
22
import { join } from 'path'
33
import { fileURLToPath } from 'url'
44
import { fork } from 'child_process'
55
import { on } from 'events'
66

7-
const serverPath = fileURLToPath(join(import.meta.url, '../../server/server.mjs'))
7+
const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs'))
88

99
const child = fork(serverPath, [], {
1010
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
1111
})
1212

13-
/** @type {WPTRunner} */
14-
let runner
15-
1613
for await (const [message] of on(child, 'message')) {
1714
if (message.server) {
18-
runner = new WPTRunner('fetch', message.server)
15+
const runner = new WPTRunner('FileAPI', message.server)
1916
runner.run()
2017

2118
runner.once('completion', () => {

test/wpt/start-fetch.mjs

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { WPTRunner } from './runner/runner/runner.mjs'
2+
import { join } from 'path'
3+
import { fileURLToPath } from 'url'
4+
import { fork } from 'child_process'
5+
import { on } from 'events'
6+
7+
const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs'))
8+
9+
const child = fork(serverPath, [], {
10+
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
11+
})
12+
13+
for await (const [message] of on(child, 'message')) {
14+
if (message.server) {
15+
const runner = new WPTRunner('fetch', message.server)
16+
runner.run()
17+
18+
runner.once('completion', () => {
19+
child.send('shutdown')
20+
})
21+
} else if (message.message === 'shutdown') {
22+
process.exit()
23+
}
24+
}

test/wpt/status/FileAPI.status.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// META: title=File constructor
2+
3+
const to_string_obj = { toString: () => 'a string' };
4+
const to_string_throws = { toString: () => { throw new Error('expected'); } };
5+
6+
test(function() {
7+
assert_true("File" in globalThis, "globalThis should have a File property.");
8+
}, "File interface object exists");
9+
10+
test(t => {
11+
assert_throws_js(TypeError, () => new File(),
12+
'Bits argument is required');
13+
assert_throws_js(TypeError, () => new File([]),
14+
'Name argument is required');
15+
}, 'Required arguments');
16+
17+
function test_first_argument(arg1, expectedSize, testName) {
18+
test(function() {
19+
var file = new File(arg1, "dummy");
20+
assert_true(file instanceof File);
21+
assert_equals(file.name, "dummy");
22+
assert_equals(file.size, expectedSize);
23+
assert_equals(file.type, "");
24+
// assert_false(file.isClosed); XXX: File.isClosed doesn't seem to be implemented
25+
assert_not_equals(file.lastModified, "");
26+
}, testName);
27+
}
28+
29+
test_first_argument([], 0, "empty fileBits");
30+
test_first_argument(["bits"], 4, "DOMString fileBits");
31+
test_first_argument(["𝓽𝓮𝔁𝓽"], 16, "Unicode DOMString fileBits");
32+
test_first_argument([new String('string object')], 13, "String object fileBits");
33+
test_first_argument([new Blob()], 0, "Empty Blob fileBits");
34+
test_first_argument([new Blob(["bits"])], 4, "Blob fileBits");
35+
test_first_argument([new File([], 'world.txt')], 0, "Empty File fileBits");
36+
test_first_argument([new File(["bits"], 'world.txt')], 4, "File fileBits");
37+
test_first_argument([new ArrayBuffer(8)], 8, "ArrayBuffer fileBits");
38+
test_first_argument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4, "Typed array fileBits");
39+
test_first_argument(["bits", new Blob(["bits"]), new Blob(), new Uint8Array([0x50, 0x41]),
40+
new Uint16Array([0x5353]), new Uint32Array([0x53534150])], 16, "Various fileBits");
41+
test_first_argument([12], 2, "Number in fileBits");
42+
test_first_argument([[1,2,3]], 5, "Array in fileBits");
43+
test_first_argument([{}], 15, "Object in fileBits"); // "[object Object]"
44+
if (globalThis.document !== undefined) {
45+
test_first_argument([document.body], 24, "HTMLBodyElement in fileBits"); // "[object HTMLBodyElement]"
46+
}
47+
test_first_argument([to_string_obj], 8, "Object with toString in fileBits");
48+
test_first_argument({[Symbol.iterator]() {
49+
let i = 0;
50+
return {next: () => [
51+
{done:false, value:'ab'},
52+
{done:false, value:'cde'},
53+
{done:true}
54+
][i++]};
55+
}}, 5, 'Custom @@iterator');
56+
57+
[
58+
'hello',
59+
0,
60+
null
61+
].forEach(arg => {
62+
test(t => {
63+
assert_throws_js(TypeError, () => new File(arg, 'world.html'),
64+
'Constructor should throw for invalid bits argument');
65+
}, `Invalid bits argument: ${JSON.stringify(arg)}`);
66+
});
67+
68+
test(t => {
69+
assert_throws_js(Error, () => new File([to_string_throws], 'name.txt'),
70+
'Constructor should propagate exceptions');
71+
}, 'Bits argument: object that throws');
72+
73+
74+
function test_second_argument(arg2, expectedFileName, testName) {
75+
test(function() {
76+
var file = new File(["bits"], arg2);
77+
assert_true(file instanceof File);
78+
assert_equals(file.name, expectedFileName);
79+
}, testName);
80+
}
81+
82+
test_second_argument("dummy", "dummy", "Using fileName");
83+
test_second_argument("dummy/foo", "dummy/foo",
84+
"No replacement when using special character in fileName");
85+
test_second_argument(null, "null", "Using null fileName");
86+
test_second_argument(1, "1", "Using number fileName");
87+
test_second_argument('', '', "Using empty string fileName");
88+
if (globalThis.document !== undefined) {
89+
test_second_argument(document.body, '[object HTMLBodyElement]', "Using object fileName");
90+
}
91+
92+
// testing the third argument
93+
[
94+
{type: 'text/plain', expected: 'text/plain'},
95+
{type: 'text/plain;charset=UTF-8', expected: 'text/plain;charset=utf-8'},
96+
{type: 'TEXT/PLAIN', expected: 'text/plain'},
97+
{type: '𝓽𝓮𝔁𝓽/𝔭𝔩𝔞𝔦𝔫', expected: ''},
98+
{type: 'ascii/nonprintable\u001F', expected: ''},
99+
{type: 'ascii/nonprintable\u007F', expected: ''},
100+
{type: 'nonascii\u00EE', expected: ''},
101+
{type: 'nonascii\u1234', expected: ''},
102+
{type: 'nonparsable', expected: 'nonparsable'}
103+
].forEach(testCase => {
104+
test(t => {
105+
var file = new File(["bits"], "dummy", { type: testCase.type});
106+
assert_true(file instanceof File);
107+
assert_equals(file.type, testCase.expected);
108+
}, `Using type in File constructor: ${testCase.type}`);
109+
});
110+
test(function() {
111+
var file = new File(["bits"], "dummy", { lastModified: 42 });
112+
assert_true(file instanceof File);
113+
assert_equals(file.lastModified, 42);
114+
}, "Using lastModified");
115+
test(function() {
116+
var file = new File(["bits"], "dummy", { name: "foo" });
117+
assert_true(file instanceof File);
118+
assert_equals(file.name, "dummy");
119+
}, "Misusing name");
120+
test(function() {
121+
var file = new File(["bits"], "dummy", { unknownKey: "value" });
122+
assert_true(file instanceof File);
123+
assert_equals(file.name, "dummy");
124+
}, "Unknown properties are ignored");
125+
126+
[
127+
123,
128+
123.4,
129+
true,
130+
'abc'
131+
].forEach(arg => {
132+
test(t => {
133+
assert_throws_js(TypeError, () => new File(['bits'], 'name.txt', arg),
134+
'Constructor should throw for invalid property bag type');
135+
}, `Invalid property bag: ${JSON.stringify(arg)}`);
136+
});
137+
138+
[
139+
null,
140+
undefined,
141+
[1,2,3],
142+
/regex/,
143+
function() {}
144+
].forEach(arg => {
145+
test(t => {
146+
assert_equals(new File(['bits'], 'name.txt', arg).size, 4,
147+
'Constructor should accept object-ish property bag type');
148+
}, `Unusual but valid property bag: ${arg}`);
149+
});
150+
151+
test(t => {
152+
assert_throws_js(Error,
153+
() => new File(['bits'], 'name.txt', {type: to_string_throws}),
154+
'Constructor should propagate exceptions');
155+
}, 'Property bag propagates exceptions');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// META: title=FormData: FormData: Upload files named using controls
2+
// META: script=../support/send-file-formdata-helper.js
3+
"use strict";
4+
5+
formDataPostFileUploadTest({
6+
fileNameSource: "ASCII",
7+
fileBaseName: "file-for-upload-in-form-NUL-[\0].txt",
8+
});
9+
10+
formDataPostFileUploadTest({
11+
fileNameSource: "ASCII",
12+
fileBaseName: "file-for-upload-in-form-BS-[\b].txt",
13+
});
14+
15+
formDataPostFileUploadTest({
16+
fileNameSource: "ASCII",
17+
fileBaseName: "file-for-upload-in-form-VT-[\v].txt",
18+
});
19+
20+
// These have characters that undergo processing in name=,
21+
// filename=, and/or value; formDataPostFileUploadTest postprocesses
22+
// expectedEncodedBaseName for these internally.
23+
24+
formDataPostFileUploadTest({
25+
fileNameSource: "ASCII",
26+
fileBaseName: "file-for-upload-in-form-LF-[\n].txt",
27+
});
28+
29+
formDataPostFileUploadTest({
30+
fileNameSource: "ASCII",
31+
fileBaseName: "file-for-upload-in-form-LF-CR-[\n\r].txt",
32+
});
33+
34+
formDataPostFileUploadTest({
35+
fileNameSource: "ASCII",
36+
fileBaseName: "file-for-upload-in-form-CR-[\r].txt",
37+
});
38+
39+
formDataPostFileUploadTest({
40+
fileNameSource: "ASCII",
41+
fileBaseName: "file-for-upload-in-form-CR-LF-[\r\n].txt",
42+
});
43+
44+
formDataPostFileUploadTest({
45+
fileNameSource: "ASCII",
46+
fileBaseName: "file-for-upload-in-form-HT-[\t].txt",
47+
});
48+
49+
formDataPostFileUploadTest({
50+
fileNameSource: "ASCII",
51+
fileBaseName: "file-for-upload-in-form-FF-[\f].txt",
52+
});
53+
54+
formDataPostFileUploadTest({
55+
fileNameSource: "ASCII",
56+
fileBaseName: "file-for-upload-in-form-DEL-[\x7F].txt",
57+
});
58+
59+
// The rest should be passed through unmodified:
60+
61+
formDataPostFileUploadTest({
62+
fileNameSource: "ASCII",
63+
fileBaseName: "file-for-upload-in-form-ESC-[\x1B].txt",
64+
});
65+
66+
formDataPostFileUploadTest({
67+
fileNameSource: "ASCII",
68+
fileBaseName: "file-for-upload-in-form-SPACE-[ ].txt",
69+
});

0 commit comments

Comments
 (0)