fix(blob, file, tests): Minor improvements for Blob and File implementations (#120)
* Use private class fields in parts initialization directly. * Move encoder initialization outside the loop * Improve errors reported by Blob constructor. * Simulate WebIDL type casting for lastModified in File constructor. * Add cancel method for Blob.stream() underlying source. * Fix indentation inconsistency. * Use getReader in blob stream cancellation test instead of Symbol.asyncIterator
This commit is contained in:
@@ -17,8 +17,12 @@ const _File = class File extends Blob {
|
||||
|
||||
if (options === null) options = {};
|
||||
|
||||
const modified = Number(options.lastModified);
|
||||
this.#lastModified = Number.isNaN(modified) ? Date.now() : modified
|
||||
// Simulate WebIDL type casting for NaN value in lastModified option.
|
||||
const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified);
|
||||
if (!Number.isNaN(lastModified)) {
|
||||
this.#lastModified = lastModified;
|
||||
}
|
||||
|
||||
this.#name = String(fileName);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,18 +59,21 @@ const _Blob = class Blob {
|
||||
* @param {{ type?: string }} [options]
|
||||
*/
|
||||
constructor(blobParts = [], options = {}) {
|
||||
const parts = [];
|
||||
let size = 0;
|
||||
if (typeof blobParts !== 'object') {
|
||||
throw new TypeError(`Failed to construct 'Blob': parameter 1 is not an iterable object.`);
|
||||
if (typeof blobParts !== "object" || blobParts === null) {
|
||||
throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.');
|
||||
}
|
||||
|
||||
if (typeof blobParts[Symbol.iterator] !== "function") {
|
||||
throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.');
|
||||
}
|
||||
|
||||
if (typeof options !== 'object' && typeof options !== 'function') {
|
||||
throw new TypeError(`Failed to construct 'Blob': parameter 2 cannot convert to dictionary.`);
|
||||
throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.');
|
||||
}
|
||||
|
||||
if (options === null) options = {};
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
for (const element of blobParts) {
|
||||
let part;
|
||||
if (ArrayBuffer.isView(element)) {
|
||||
@@ -80,18 +83,16 @@ const _Blob = class Blob {
|
||||
} else if (element instanceof Blob) {
|
||||
part = element;
|
||||
} else {
|
||||
part = new TextEncoder().encode(element);
|
||||
part = encoder.encode(element);
|
||||
}
|
||||
|
||||
size += ArrayBuffer.isView(part) ? part.byteLength : part.size;
|
||||
parts.push(part);
|
||||
this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size;
|
||||
this.#parts.push(part);
|
||||
}
|
||||
|
||||
const type = options.type === undefined ? '' : String(options.type);
|
||||
|
||||
this.#type = /^[\x20-\x7E]*$/.test(type) ? type : '';
|
||||
this.#size = size;
|
||||
this.#parts = parts;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,6 +161,10 @@ const _Blob = class Blob {
|
||||
async pull(ctrl) {
|
||||
const chunk = await it.next();
|
||||
chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value);
|
||||
},
|
||||
|
||||
async cancel() {
|
||||
await it.return()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -58,7 +58,17 @@ test('Blob ctor reads blob parts from object with @@iterator', async t => {
|
||||
});
|
||||
|
||||
test('Blob ctor throws a string', t => {
|
||||
t.throws(() => new Blob('abc'));
|
||||
t.throws(() => new Blob('abc'), {
|
||||
instanceOf: TypeError,
|
||||
message: 'Failed to construct \'Blob\': The provided value cannot be converted to a sequence.'
|
||||
});
|
||||
});
|
||||
|
||||
test('Blob ctor throws an error for an object that does not have @@iterable method', t => {
|
||||
t.throws(() => new Blob({}), {
|
||||
instanceOf: TypeError,
|
||||
message: 'Failed to construct \'Blob\': The object must have a callable @@iterator property.'
|
||||
});
|
||||
});
|
||||
|
||||
test('Blob ctor threats Uint8Array as a sequence', async t => {
|
||||
@@ -123,6 +133,20 @@ test('Blob stream()', async t => {
|
||||
}
|
||||
});
|
||||
|
||||
test('Blob stream() can be cancelled', async t => {
|
||||
const stream = new Blob(['Some content']).stream();
|
||||
|
||||
// Cancel the stream before start reading, or this will throw an error
|
||||
await stream.cancel();
|
||||
|
||||
const reader = stream.getReader();
|
||||
|
||||
const {done, value: chunk} = await reader.read();
|
||||
|
||||
t.true(done);
|
||||
t.is(chunk, undefined);
|
||||
});
|
||||
|
||||
test('Blob toString()', t => {
|
||||
const data = 'a=1';
|
||||
const type = 'text/plain';
|
||||
@@ -355,6 +379,30 @@ test('new File(,,{lastModified: new Date()})', t => {
|
||||
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms
|
||||
});
|
||||
|
||||
test('new File(,,{lastModified: undefined})', t => {
|
||||
const mod = new File([], '', {lastModified: undefined}).lastModified - Date.now();
|
||||
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms
|
||||
});
|
||||
|
||||
test('new File(,,{lastModified: null})', t => {
|
||||
const mod = new File([], '', {lastModified: null}).lastModified;
|
||||
t.is(mod, 0);
|
||||
});
|
||||
|
||||
test('Interpretes NaN value in lastModified option as 0', t => {
|
||||
t.plan(3);
|
||||
|
||||
const values = ['Not a Number', [], {}];
|
||||
|
||||
// I can't really see anything about this in the spec,
|
||||
// but this is how browsers handle type casting for this option...
|
||||
for (const lastModified of values) {
|
||||
const file = new File(['Some content'], 'file.txt', {lastModified});
|
||||
|
||||
t.is(file.lastModified, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('new File(,,{}) sets current time', t => {
|
||||
const mod = new File([], '').lastModified - Date.now();
|
||||
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms
|
||||
|
||||
Reference in New Issue
Block a user