diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index b5ab746d..0852a1d3 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -304,29 +304,50 @@ export default class Request implements Request { * @returns FormData. */ public async formData(): Promise { - if (this.bodyUsed) { - throw new DOMException( - `Body has already been used for "${this.url}".`, - DOMExceptionNameEnum.invalidStateError - ); - } + const contentType = this[PropertySymbol.contentType]; + const asyncTaskManager = this.#asyncTaskManager; - (this.bodyUsed) = true; + if (/multipart/i.test(contentType)) { + if (this.bodyUsed) { + throw new DOMException( + `Body has already been used for "${this.url}".`, + DOMExceptionNameEnum.invalidStateError + ); + } - const taskID = this.#asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); - let formData: FormData; + (this.bodyUsed) = true; - try { - const type = this[PropertySymbol.contentType]; - formData = (await MultipartFormDataParser.streamToFormData(this.body, type)).formData; - } catch (error) { - this.#asyncTaskManager.endTask(taskID); - throw error; + const taskID = asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); + let formData: FormData; + + try { + const result = await MultipartFormDataParser.streamToFormData(this.body, contentType); + formData = result.formData; + } catch (error) { + asyncTaskManager.endTask(taskID); + throw error; + } + + asyncTaskManager.endTask(taskID); + + return formData; } - this.#asyncTaskManager.endTask(taskID); + if (contentType?.startsWith('application/x-www-form-urlencoded')) { + const parameters = new URLSearchParams(await this.text()); + const formData = new FormData(); + + for (const [key, value] of parameters) { + formData.append(key, value); + } - return formData; + return formData; + } + + throw new DOMException( + `Failed to construct FormData object: The "content-type" header is neither "application/x-www-form-urlencoded" nor "multipart/form-data".`, + DOMExceptionNameEnum.invalidStateError + ); } /** diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index cfdc9974..ee915208 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -225,37 +225,52 @@ export default class Response implements Response { */ public async formData(): Promise { const contentType = this.headers.get('Content-Type'); + const asyncTaskManager = this.#browserFrame[PropertySymbol.asyncTaskManager]; + + if (/multipart/i.test(contentType)) { + if (this.bodyUsed) { + throw new DOMException( + `Body has already been used for "${this.url}".`, + DOMExceptionNameEnum.invalidStateError + ); + } - if (contentType?.startsWith('application/x-www-form-urlencoded')) { - const formData = new FormData(); - const text = await this.text(); - const parameters = new URLSearchParams(text); + (this.bodyUsed) = true; - for (const [name, value] of parameters) { - formData.append(name, value); + const taskID = asyncTaskManager.startTask(); + let formData: FormData; + let buffer: Buffer; + + try { + const result = await MultipartFormDataParser.streamToFormData(this.body, contentType); + formData = result.formData; + buffer = result.buffer; + } catch (error) { + asyncTaskManager.endTask(taskID); + throw error; } + this.#storeBodyInCache(buffer); + asyncTaskManager.endTask(taskID); + return formData; } - const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(); - let formData: FormData; - let buffer: Buffer; - - try { - const result = await MultipartFormDataParser.streamToFormData(this.body, contentType); - formData = result.formData; - buffer = result.buffer; - } catch (error) { - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); - throw error; - } + if (contentType?.startsWith('application/x-www-form-urlencoded')) { + const parameters = new URLSearchParams(await this.text()); + const formData = new FormData(); - this.#storeBodyInCache(buffer); + for (const [key, value] of parameters) { + formData.append(key, value); + } - this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); + return formData; + } - return formData; + throw new DOMException( + `Failed to build FormData object: The "content-type" header is neither "application/x-www-form-urlencoded" nor "multipart/form-data".`, + DOMExceptionNameEnum.invalidStateError + ); } /** diff --git a/packages/happy-dom/test/fetch/Request.test.ts b/packages/happy-dom/test/fetch/Request.test.ts index 8d598464..e7378cfb 100644 --- a/packages/happy-dom/test/fetch/Request.test.ts +++ b/packages/happy-dom/test/fetch/Request.test.ts @@ -15,6 +15,9 @@ import MultipartFormDataParser from '../../src/fetch/multipart/MultipartFormData import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest'; import { ReadableStream } from 'stream/web'; import * as PropertySymbol from '../../src/PropertySymbol.js'; +import File from '../../src/file/File.js'; +import Path from 'path'; +import FS from 'fs'; const TEST_URL = 'https://example.com/'; @@ -666,13 +669,106 @@ describe('Request', () => { }); describe('formData()', () => { - it('Returns FormData', async () => { + it('Returns FormData for FormData object (multipart)', async () => { const formData = new FormData(); formData.append('some', 'test'); const request = new window.Request(TEST_URL, { method: 'POST', body: formData }); - const requestFormData = await request.formData(); + const formDataResponse = await request.formData(); - expect(requestFormData).toEqual(formData); + expect(formDataResponse).toEqual(formData); + }); + + it('Returns FormData for URLSearchParams object (application/x-www-form-urlencoded)', async () => { + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('some', 'test'); + const request = new window.Request(TEST_URL, { method: 'POST', body: urlSearchParams }); + const formDataResponse = await request.formData(); + + expect(formDataResponse instanceof FormData).toBe(true); + expect(formDataResponse.get('some')).toBe('test'); + }); + + it('Returns FormData for "application/x-www-form-urlencoded" content.', async () => { + const urlSearchParams = new URLSearchParams(); + + urlSearchParams.set('key1', 'value1'); + urlSearchParams.set('key2', 'value2'); + urlSearchParams.set('key3', 'value3'); + + const request = new window.Request(TEST_URL, { method: 'POST', body: urlSearchParams }); + const formDataResponse = await request.formData(); + let size = 0; + + for (const _entry of formDataResponse) { + size++; + } + + expect(formDataResponse.get('key1')).toBe('value1'); + expect(formDataResponse.get('key2')).toBe('value2'); + expect(formDataResponse.get('key3')).toBe('value3'); + expect(size).toBe(3); + }); + + it('Returns FormData for multipart text fields.', async () => { + const formData = new FormData(); + + vi.spyOn(Math, 'random').mockImplementation(() => 0.8); + + formData.set('key1', 'value1'); + formData.set('key2', 'value2'); + formData.set('key3', 'value3'); + + const request = new window.Request(TEST_URL, { method: 'POST', body: formData }); + const formDataResponse = await request.formData(); + let size = 0; + + for (const _entry of formDataResponse) { + size++; + } + + expect(formDataResponse.get('key1')).toBe('value1'); + expect(formDataResponse.get('key2')).toBe('value2'); + expect(formDataResponse.get('key3')).toBe('value3'); + expect(size).toBe(3); + }); + + it('Returns FormData for multipart files.', async () => { + const formData = new FormData(); + const imageBuffer = await FS.promises.readFile( + Path.join(__dirname, 'data', 'test-image.jpg') + ); + + vi.spyOn(Math, 'random').mockImplementation(() => 0.8); + + formData.set('key1', 'value1'); + formData.set('file1', new File([imageBuffer], 'test-image-1.jpg', { type: 'image/jpeg' })); + formData.set('key2', 'value2'); + formData.set('file2', new File([imageBuffer], 'test-image-2.jpg', { type: 'image/jpeg' })); + + const request = new window.Request(TEST_URL, { method: 'POST', body: formData }); + const formDataResponse = await request.formData(); + let size = 0; + + for (const _entry of formDataResponse) { + size++; + } + + expect(formDataResponse.get('key1')).toBe('value1'); + expect(formDataResponse.get('key2')).toBe('value2'); + expect(size).toBe(4); + + const file1 = formDataResponse.get('file1'); + const file2 = formDataResponse.get('file2'); + + expect(file1.name).toBe('test-image-1.jpg'); + expect(file1.type).toBe('image/jpeg'); + expect(file1.size).toBe(imageBuffer.length); + expect(await file1.arrayBuffer()).toEqual(imageBuffer.buffer); + + expect(file2.name).toBe('test-image-2.jpg'); + expect(file2.type).toBe('image/jpeg'); + expect(file2.size).toBe(imageBuffer.length); + expect(await file2.arrayBuffer()).toEqual(imageBuffer.buffer); }); it('Supports window.happyDOM?.waitUntilComplete().', async () => { diff --git a/packages/happy-dom/test/fetch/Response.test.ts b/packages/happy-dom/test/fetch/Response.test.ts index 10a46d16..9c8e523a 100644 --- a/packages/happy-dom/test/fetch/Response.test.ts +++ b/packages/happy-dom/test/fetch/Response.test.ts @@ -321,6 +321,25 @@ describe('Response', () => { }); describe('formData()', () => { + it('Returns FormData for FormData object (multipart)', async () => { + const formData = new FormData(); + formData.append('some', 'test'); + const response = new window.Response(formData); + const formDataResponse = await response.formData(); + + expect(formDataResponse).toEqual(formData); + }); + + it('Returns FormData for URLSearchParams object (application/x-www-form-urlencoded)', async () => { + const urlSearchParams = new URLSearchParams(); + urlSearchParams.append('some', 'test'); + const response = new window.Response(urlSearchParams); + const formDataResponse = await response.formData(); + + expect(formDataResponse instanceof FormData).toBe(true); + expect(formDataResponse.get('some')).toBe('test'); + }); + it('Returns FormData for "application/x-www-form-urlencoded" content.', async () => { const urlSearchParams = new URLSearchParams();