From 4bd35f5a798702631ebbca5743bb5079b0206fe2 Mon Sep 17 00:00:00 2001 From: xonaib Date: Sat, 23 May 2026 15:58:21 +0500 Subject: [PATCH] fix(mock-doc): add File and Blob constructors --- src/mock-doc/blob.ts | 61 ++++++++++++++++++++++++++++++++ src/mock-doc/file.ts | 18 ++++++++++ src/mock-doc/global.ts | 4 +++ src/mock-doc/index.ts | 2 ++ src/mock-doc/test/global.spec.ts | 26 ++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 src/mock-doc/blob.ts create mode 100644 src/mock-doc/file.ts diff --git a/src/mock-doc/blob.ts b/src/mock-doc/blob.ts new file mode 100644 index 00000000000..2c192273288 --- /dev/null +++ b/src/mock-doc/blob.ts @@ -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({ + 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; +}; diff --git a/src/mock-doc/file.ts b/src/mock-doc/file.ts new file mode 100644 index 00000000000..227eec77bed --- /dev/null +++ b/src/mock-doc/file.ts @@ -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'; + } +} diff --git a/src/mock-doc/global.ts b/src/mock-doc/global.ts index 605348cf2c0..0f9653e8371 100644 --- a/src/mock-doc/global.ts +++ b/src/mock-doc/global.ts @@ -1,3 +1,4 @@ +import { MockBlob } from './blob'; import { MockCSSStyleSheet } from './css-style-sheet'; import { MockDocumentFragment } from './document-fragment'; import { @@ -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'; @@ -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], diff --git a/src/mock-doc/index.ts b/src/mock-doc/index.ts index f911dafc7ef..a70ffb1ac5b 100644 --- a/src/mock-doc/index.ts +++ b/src/mock-doc/index.ts @@ -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'; diff --git a/src/mock-doc/test/global.spec.ts b/src/mock-doc/test/global.spec.ts index fa15c95ac93..ed146cb0cc0 100644 --- a/src/mock-doc/test/global.spec.ts +++ b/src/mock-doc/test/global.spec.ts @@ -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(); });