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:
Nick K
2021-10-24 14:56:15 +02:00
committed by GitHub
parent 9022f8fc70
commit 2c11647222
3 changed files with 70 additions and 13 deletions
+6 -2
View File
@@ -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);
}
+15 -10
View File
@@ -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()
}
})
}
+49 -1
View File
@@ -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