diff --git a/file.js b/file.js index e324d8b..e16e299 100644 --- a/file.js +++ b/file.js @@ -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); } diff --git a/index.js b/index.js index a97500c..50e5584 100644 --- a/index.js +++ b/index.js @@ -58,18 +58,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)) { @@ -79,18 +82,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; } /** @@ -159,6 +160,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() } }) } diff --git a/test.js b/test.js index 0a9688b..d7582ed 100644 --- a/test.js +++ b/test.js @@ -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