LibWeb: Update FileAPI File constructor to follow the latest spec steps

This fixes http://wpt.live/FileAPI/file/File-constructor.any.html
This commit is contained in:
Pavel Shliak 2024-11-23 10:51:08 +04:00
parent 8965698ce7
commit 60d0212f0b
4 changed files with 252 additions and 10 deletions

View file

@ -57,19 +57,30 @@ WebIDL::ExceptionOr<GC::Ref<File>> File::create(JS::Realm& realm, Vector<BlobPar
i64 last_modified = 0;
// 3. Process FilePropertyBag dictionary argument by running the following substeps:
if (options.has_value()) {
// FIXME: 1. If the type member is provided and is not the empty string, let t be set to the type dictionary member.
// If t contains any characters outside the range U+0020 to U+007E, then set t to the empty string and return from these substeps.
// FIXME: 2. Convert every character in t to ASCII lowercase.
// 1. If the type member is provided and is not the empty string, let t be set to the type dictionary member.
type = options->type;
// NOTE: The spec is out of date, and we are supposed to call into the MimeType parser here.
auto maybe_parsed_type = Web::MimeSniff::MimeType::parse(options->type);
if (!type.is_empty()) {
// If t contains any characters outside the range U+0020 to U+007E, then set t to the empty string and return from these substeps.
if (AK::any_of(type.bytes_as_string_view(), [](auto ch) { return ch < 0x20 || ch > 0x7E; })) {
// Assign an empty string
type = String {};
// Return from these substeps.
return realm.create<File>(realm, move(bytes), move(name), move(type), last_modified);
}
// 2. Convert every character in t to ASCII lowercase.
type = type.to_ascii_lowercase();
if (maybe_parsed_type.has_value())
type = maybe_parsed_type->serialized();
} else {
// If the type member is not provided or is the empty string, set t to the empty string.
type = String {};
}
// 3. If the lastModified member is provided, let d be set to the lastModified dictionary member. If it is not provided, set d to the current date and time represented as the number of milliseconds since the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]).
// Note: Since ECMA-262 Date objects convert to long long values representing the number of milliseconds since the Unix Epoch, the lastModified member could be a Date object [ECMA-262].
last_modified = options->last_modified.has_value() ? options->last_modified.value() : UnixDateTime::now().milliseconds_since_epoch();
// 3. If the lastModified member is provided, let d be set to the lastModified dictionary member.
// If it is not provided, set d to the current date and time represented as the number of milliseconds since the Unix Epoch.
last_modified = options->last_modified.has_value()
? options->last_modified.value()
: UnixDateTime::now().milliseconds_since_epoch();
}
// 4. Return a new File object F such that:

View file

@ -0,0 +1,61 @@
Summary
Harness status: OK
Rerun
Found 51 tests
51 Pass
Details
Result Test Name MessagePass File interface object exists
Pass Required arguments
Pass empty fileBits
Pass DOMString fileBits
Pass Unicode DOMString fileBits
Pass String object fileBits
Pass Empty Blob fileBits
Pass Blob fileBits
Pass Empty File fileBits
Pass File fileBits
Pass ArrayBuffer fileBits
Pass Typed array fileBits
Pass Various fileBits
Pass Number in fileBits
Pass Array in fileBits
Pass Object in fileBits
Pass HTMLBodyElement in fileBits
Pass Object with toString in fileBits
Pass Custom @@iterator
Pass Invalid bits argument: "hello"
Pass Invalid bits argument: 0
Pass Invalid bits argument: null
Pass Bits argument: object that throws
Pass Using fileName
Pass No replacement when using special character in fileName
Pass Using null fileName
Pass Using number fileName
Pass Using empty string fileName
Pass Using object fileName
Pass Using type in File constructor: text/plain
Pass Using type in File constructor: text/plain;charset=UTF-8
Pass Using type in File constructor: TEXT/PLAIN
Pass Using type in File constructor: <20><><EFBFBD>U+dcfd<66><64><EFBFBD>U+dcee<65><65><EFBFBD>U+dd01<30><31><EFBFBD>U+dcfd/<2F><><EFBFBD>U+dd2d<32><64><EFBFBD>U+dd29<32><39><EFBFBD>U+dd1e<31><65><EFBFBD>U+dd26<32><36><EFBFBD>U+dd2b
Pass Using type in File constructor: ascii/nonprintable
Pass Using type in File constructor: ascii/nonprintable
Pass Using type in File constructor: nonasciiî
Pass Using type in File constructor: nonasciiሴ
Pass Using type in File constructor: nonparsable
Pass Using lastModified
Pass Misusing name
Pass Unknown properties are ignored
Pass Invalid property bag: 123
Pass Invalid property bag: 123.4
Pass Invalid property bag: true
Pass Invalid property bag: "abc"
Pass Unusual but valid property bag: null
Pass Unusual but valid property bag: undefined
Pass Unusual but valid property bag: 1,2,3
Pass Unusual but valid property bag: /regex/
Pass Unusual but valid property bag: function() {}
Pass Property bag propagates exceptions

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<title>File constructor</title>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../FileAPI/file/File-constructor.any.js"></script>

View file

@ -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');