Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/mock-doc/blob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export class MockBlob implements Blob {
private readonly blobParts: BlobPart[];

readonly size: number;
readonly type: string;

constructor(blobParts: BlobPart[] = [], options: BlobPropertyBag = {}) {
this.blobParts = blobParts;
this.size = blobParts.reduce((total, part) => total + getBlobPartSize(part), 0);
this.type = options.type ? String(options.type).toLowerCase() : '';
}

async arrayBuffer() {
return new ArrayBuffer(this.size);
}

async bytes() {
return new Uint8Array(this.size);
}

slice(start = 0, end = this.size, contentType = '') {
const relativeStart = start < 0 ? Math.max(this.size + start, 0) : Math.min(start, this.size);
const relativeEnd = end < 0 ? Math.max(this.size + end, 0) : Math.min(end, this.size);
const span = Math.max(relativeEnd - relativeStart, 0);

return new MockBlob([new Uint8Array(span)], { type: contentType });
}

stream() {
return new ReadableStream<Uint8Array>({
start: (controller) => {
controller.enqueue(new Uint8Array(this.size));
controller.close();
},
});
}

async text() {
return this.blobParts.map((part) => (typeof part === 'string' ? part : '')).join('');
}

get [Symbol.toStringTag]() {
return 'Blob';
}
}

const getBlobPartSize = (part: BlobPart) => {
if (typeof part === 'string') {
return globalThis.TextEncoder ? new TextEncoder().encode(part).byteLength : part.length;
}

if (part instanceof ArrayBuffer) {
return part.byteLength;
}

if (ArrayBuffer.isView(part)) {
return part.byteLength;
}

return part.size;
};
18 changes: 18 additions & 0 deletions src/mock-doc/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MockBlob } from './blob';

export class MockFile extends MockBlob implements File {
readonly lastModified: number;
readonly name: string;
readonly webkitRelativePath = '';

constructor(fileBits: BlobPart[] = [], fileName = '', options: FilePropertyBag = {}) {
super(fileBits, options);

this.name = String(fileName);
this.lastModified = options.lastModified ?? Date.now();
}

override get [Symbol.toStringTag]() {
return 'File';
}
}
4 changes: 4 additions & 0 deletions src/mock-doc/global.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MockBlob } from './blob';
import { MockCSSStyleSheet } from './css-style-sheet';
import { MockDocumentFragment } from './document-fragment';
import {
Expand All @@ -17,6 +18,7 @@ import {
MockUListElement,
} from './element';
import { MockCustomEvent, MockEvent, MockFocusEvent, MockKeyboardEvent, MockMouseEvent } from './event';
import { MockFile } from './file';
import { MockHeaders } from './headers';
import { MockDOMParser } from './parser';
import { MockRequest, MockResponse } from './request-response';
Expand Down Expand Up @@ -158,11 +160,13 @@ const WINDOW_PROPS = [
];

const GLOBAL_CONSTRUCTORS: [string, any][] = [
['Blob', MockBlob],
['CSSStyleSheet', MockCSSStyleSheet],
['CustomEvent', MockCustomEvent],
['DocumentFragment', MockDocumentFragment],
['DOMParser', MockDOMParser],
['Event', MockEvent],
['File', MockFile],
['FocusEvent', MockFocusEvent],
['Headers', MockHeaders],
['KeyboardEvent', MockKeyboardEvent],
Expand Down
2 changes: 2 additions & 0 deletions src/mock-doc/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export { cloneAttributes, MockAttr, MockAttributeMap } from './attribute';
export { MockBlob } from './blob';
export { MockComment } from './comment-node';
export { NODE_TYPES } from './constants';
export { createDocument, createFragment, MockDocument, resetDocument } from './document';
export { MockCustomEvent, MockKeyboardEvent, MockMouseEvent } from './event';
export { MockFile } from './file';
export { patchWindow, setupGlobal, teardownGlobal } from './global';
export { MockHeaders } from './headers';
export { MockElement, MockHTMLElement, MockNode, MockTextNode } from './node';
Expand Down
26 changes: 26 additions & 0 deletions src/mock-doc/test/global.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,32 @@ describe('global', () => {
expect(Response).toBeDefined();
});

it('Blob', async () => {
const blob = new Blob(['hello'], { type: 'TEXT/PLAIN' });

expect(Blob).toBeDefined();
expect(blob.size).toBe(5);
expect(blob.type).toBe('text/plain');
expect(await blob.text()).toBe('hello');
expect(Object.prototype.toString.call(blob)).toBe('[object Blob]');
});

it('File', () => {
const file = new File(['whatever'], 'myFile.png', {
lastModified: 123,
type: 'image/png',
});

expect(File).toBeDefined();
expect(file).toBeInstanceOf(Blob);
expect(file.name).toBe('myFile.png');
expect(file.type).toBe('image/png');
expect(file.size).toBe(8);
expect(file.lastModified).toBe(123);
expect(file.webkitRelativePath).toBe('');
expect(Object.prototype.toString.call(file)).toBe('[object File]');
});

it('Parse', () => {
expect(DOMParser).toBeDefined();
});
Expand Down