Skip to content

fix(blob, file, tests): Minor improvements for Blob and File implementations #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 24, 2021
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