Repository: capricorn86/happy-dom (ts)

Issue:
TypeError: request.clone(...).formData is not a function
**Describe the bug**
The method `formData()` is missing at Request interface and its implementation.

https://developer.mozilla.org/en-US/docs/Web/API/Request/formData

**To Reproduce**
```typescript
it('should work with form data', async () => {
  const formData = new FormData();
  formData.append('some', 'test');
  const request = new Request('http://foo.bar/baz', { method: 'post', body: formData });
  const formDataFromRequest = await request.formData();
  expect(formDataFromRequest).toBe(formDataFromRequest);
});
```

**Actual Behavior**
```
TypeError: request.formData is not a function
```

**Expected behavior**
test should succeed

**Additional context**
We start to migrate a project to remix and to implement the action/loader unit tests where we need to construct `Request`s manually and pass them the form data.


Pull request description:
#892@patch: Implement `Request.formData()`.
fixes #892 

Patch:
diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts
index 50f5c456..271aaf4a 100644
--- a/packages/happy-dom/src/fetch/Request.ts
+++ b/packages/happy-dom/src/fetch/Request.ts
@@ -18,6 +18,8 @@ import IRequestRedirect from './types/IRequestRedirect';
 import FetchRequestReferrerUtility from './utilities/FetchRequestReferrerUtility';
 import FetchRequestHeaderUtility from './utilities/FetchRequestHeaderUtility';
 import IRequestCredentials from './types/IRequestCredentials';
+import FormData from '../form-data/FormData';
+import MultipartFormDataParser from './multipart/MultipartFormDataParser';
 
 /**
  * Fetch request.
@@ -280,6 +282,38 @@ export default class Request implements IRequest {
 		return JSON.parse(text);
 	}
 
+	/**
+	 * Returns FormData.
+	 *
+	 * @returns FormData.
+	 */
+	public async formData(): Promise<FormData> {
+		if (this.bodyUsed) {
+			throw new DOMException(
+				`Body has already been used for "${this.url}".`,
+				DOMExceptionNameEnum.invalidStateError
+			);
+		}
+
+		(<boolean>this.bodyUsed) = true;
+
+		const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager;
+		const taskID = taskManager.startTask(() => this.signal._abort());
+		let formData: FormData;
+
+		try {
+			const type = this._contentType;
+			formData = await MultipartFormDataParser.streamToFormData(this.body, type);
+		} catch (error) {
+			taskManager.endTask(taskID);
+			throw error;
+		}
+
+		taskManager.endTask(taskID);
+
+		return formData;
+	}
+
 	/**
 	 * Clones request.
 	 *
diff --git a/packages/happy-dom/src/fetch/types/IRequest.ts b/packages/happy-dom/src/fetch/types/IRequest.ts
index 7505e1a3..dd30b5ee 100644
--- a/packages/happy-dom/src/fetch/types/IRequest.ts
+++ b/packages/happy-dom/src/fetch/types/IRequest.ts
@@ -5,6 +5,7 @@ import Stream from 'stream';
 import IRequestReferrerPolicy from './IRequestReferrerPolicy';
 import IRequestRedirect from './IRequestRedirect';
 import IRequestCredentials from './IRequestCredentials';
+import FormData from '../../form-data/FormData';
 
 /**
  * Fetch request.
@@ -56,6 +57,13 @@ export default interface IRequest {
 	 */
 	json(): Promise<string>;
 
+	/**
+	 * Returns FormData.
+	 *
+	 * @returns FormData.
+	 */
+	formData(): Promise<FormData>;
+
 	/**
 	 * Clones request.
 	 *


Test patch:
diff --git a/packages/happy-dom/test/fetch/Request.test.ts b/packages/happy-dom/test/fetch/Request.test.ts
index 6271ea94..bd0dda78 100644
--- a/packages/happy-dom/test/fetch/Request.test.ts
+++ b/packages/happy-dom/test/fetch/Request.test.ts
@@ -11,6 +11,8 @@ import IRequestReferrerPolicy from '../../src/fetch/types/IRequestReferrerPolicy
 import IRequestRedirect from '../../src/fetch/types/IRequestRedirect';
 import FetchBodyUtility from '../../src/fetch/utilities/FetchBodyUtility';
 import Blob from '../../src/file/Blob';
+import { FormData } from '../../src';
+import MultipartFormDataParser from '../../src/fetch/multipart/MultipartFormDataParser';
 
 const TEST_URL = 'https://example.com/';
 
@@ -628,6 +630,42 @@ describe('Request', () => {
 		});
 	});
 
+	describe('formData()', () => {
+		it('Returns FormData', async () => {
+			const formData = new FormData();
+			formData.append('some', 'test');
+			const request = new Request(TEST_URL, { method: 'POST', body: formData });
+			const requestFormData = await request.formData();
+
+			expect(requestFormData).toEqual(formData);
+		});
+
+		it('Supports window.happyDOM.whenAsyncComplete().', (done) => {
+			const formData = new FormData();
+			formData.append('some', 'test');
+			const request = new Request(TEST_URL, { method: 'POST', body: formData });
+			let isAsyncComplete = false;
+
+			jest
+				.spyOn(MultipartFormDataParser, 'streamToFormData')
+				.mockImplementation(
+					() => new Promise((resolve) => setTimeout(() => resolve(formData), 10))
+				);
+
+			window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true));
+			request.formData();
+
+			setTimeout(() => {
+				expect(isAsyncComplete).toBe(false);
+			}, 2);
+
+			setTimeout(() => {
+				expect(isAsyncComplete).toBe(true);
+				done();
+			}, 12);
+		});
+	});
+
 	describe('clone()', () => {
 		it('Returns a clone.', async () => {
 			window.happyDOM.setURL('https://example.com/other/path/');