Skip to content
Merged
8 changes: 6 additions & 2 deletions file.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
25 changes: 15 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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()
}
})
}
Expand Down
50 changes: 49 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down