Skip to content

Commit 2c11647

Browse files
authored
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
1 parent 9022f8f commit 2c11647

File tree

3 files changed

+70
-13
lines changed

3 files changed

+70
-13
lines changed

file.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ const _File = class File extends Blob {
1717

1818
if (options === null) options = {};
1919

20-
const modified = Number(options.lastModified);
21-
this.#lastModified = Number.isNaN(modified) ? Date.now() : modified
20+
// Simulate WebIDL type casting for NaN value in lastModified option.
21+
const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified);
22+
if (!Number.isNaN(lastModified)) {
23+
this.#lastModified = lastModified;
24+
}
25+
2226
this.#name = String(fileName);
2327
}
2428

index.js

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,21 @@ const _Blob = class Blob {
5959
* @param {{ type?: string }} [options]
6060
*/
6161
constructor(blobParts = [], options = {}) {
62-
const parts = [];
63-
let size = 0;
64-
if (typeof blobParts !== 'object') {
65-
throw new TypeError(`Failed to construct 'Blob': parameter 1 is not an iterable object.`);
62+
if (typeof blobParts !== "object" || blobParts === null) {
63+
throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.');
64+
}
65+
66+
if (typeof blobParts[Symbol.iterator] !== "function") {
67+
throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.');
6668
}
6769

6870
if (typeof options !== 'object' && typeof options !== 'function') {
69-
throw new TypeError(`Failed to construct 'Blob': parameter 2 cannot convert to dictionary.`);
71+
throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.');
7072
}
7173

7274
if (options === null) options = {};
7375

76+
const encoder = new TextEncoder()
7477
for (const element of blobParts) {
7578
let part;
7679
if (ArrayBuffer.isView(element)) {
@@ -80,18 +83,16 @@ const _Blob = class Blob {
8083
} else if (element instanceof Blob) {
8184
part = element;
8285
} else {
83-
part = new TextEncoder().encode(element);
86+
part = encoder.encode(element);
8487
}
8588

86-
size += ArrayBuffer.isView(part) ? part.byteLength : part.size;
87-
parts.push(part);
89+
this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size;
90+
this.#parts.push(part);
8891
}
8992

9093
const type = options.type === undefined ? '' : String(options.type);
9194

9295
this.#type = /^[\x20-\x7E]*$/.test(type) ? type : '';
93-
this.#size = size;
94-
this.#parts = parts;
9596
}
9697

9798
/**
@@ -160,6 +161,10 @@ const _Blob = class Blob {
160161
async pull(ctrl) {
161162
const chunk = await it.next();
162163
chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value);
164+
},
165+
166+
async cancel() {
167+
await it.return()
163168
}
164169
})
165170
}

test.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,17 @@ test('Blob ctor reads blob parts from object with @@iterator', async t => {
5858
});
5959

6060
test('Blob ctor throws a string', t => {
61-
t.throws(() => new Blob('abc'));
61+
t.throws(() => new Blob('abc'), {
62+
instanceOf: TypeError,
63+
message: 'Failed to construct \'Blob\': The provided value cannot be converted to a sequence.'
64+
});
65+
});
66+
67+
test('Blob ctor throws an error for an object that does not have @@iterable method', t => {
68+
t.throws(() => new Blob({}), {
69+
instanceOf: TypeError,
70+
message: 'Failed to construct \'Blob\': The object must have a callable @@iterator property.'
71+
});
6272
});
6373

6474
test('Blob ctor threats Uint8Array as a sequence', async t => {
@@ -123,6 +133,20 @@ test('Blob stream()', async t => {
123133
}
124134
});
125135

136+
test('Blob stream() can be cancelled', async t => {
137+
const stream = new Blob(['Some content']).stream();
138+
139+
// Cancel the stream before start reading, or this will throw an error
140+
await stream.cancel();
141+
142+
const reader = stream.getReader();
143+
144+
const {done, value: chunk} = await reader.read();
145+
146+
t.true(done);
147+
t.is(chunk, undefined);
148+
});
149+
126150
test('Blob toString()', t => {
127151
const data = 'a=1';
128152
const type = 'text/plain';
@@ -355,6 +379,30 @@ test('new File(,,{lastModified: new Date()})', t => {
355379
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms
356380
});
357381

382+
test('new File(,,{lastModified: undefined})', t => {
383+
const mod = new File([], '', {lastModified: undefined}).lastModified - Date.now();
384+
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms
385+
});
386+
387+
test('new File(,,{lastModified: null})', t => {
388+
const mod = new File([], '', {lastModified: null}).lastModified;
389+
t.is(mod, 0);
390+
});
391+
392+
test('Interpretes NaN value in lastModified option as 0', t => {
393+
t.plan(3);
394+
395+
const values = ['Not a Number', [], {}];
396+
397+
// I can't really see anything about this in the spec,
398+
// but this is how browsers handle type casting for this option...
399+
for (const lastModified of values) {
400+
const file = new File(['Some content'], 'file.txt', {lastModified});
401+
402+
t.is(file.lastModified, 0);
403+
}
404+
});
405+
358406
test('new File(,,{}) sets current time', t => {
359407
const mod = new File([], '').lastModified - Date.now();
360408
t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms

0 commit comments

Comments
 (0)