Skip to content

Commit

Permalink
capricorn86#463@trivial: Continues on XMLHttpRequest implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Dec 7, 2022
1 parent 7bf4bd5 commit 2e45e3e
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 81 deletions.
40 changes: 34 additions & 6 deletions packages/happy-dom/src/fetch/FetchHandler.ts
Expand Up @@ -4,6 +4,9 @@ import IDocument from '../nodes/document/IDocument';
import IResponse from './IResponse';
import Response from './Response';
import NodeFetch from 'node-fetch';
import Request from './Request';
import RequestInfo from './RequestInfo';
import { URL } from 'url';

/**
* Helper class for performing fetch.
Expand All @@ -17,22 +20,47 @@ export default class FetchHandler {
* @param [init] Init.
* @returns Response.
*/
public static fetch(document: IDocument, url: string, init?: IRequestInit): Promise<IResponse> {
// We want to only load NodeFetch when it is needed to improve performance and not have direct dependencies to server side packages.
public static fetch(
document: IDocument,
url: RequestInfo,
init?: IRequestInit
): Promise<IResponse> {
const taskManager = document.defaultView.happyDOM.asyncTaskManager;
const requestInit = { ...init, headers: { ...init?.headers } };
const cookie = document.defaultView.document.cookie;
const referer = document.defaultView.location.origin;

requestInit.headers['user-agent'] = document.defaultView.navigator.userAgent;

// We need set referer to solve anti-hotlinking.
// And the browser will set the referer to the origin of the page.
requestInit.headers['referer'] = document.defaultView.location.origin;
// Referer is "null" when the URL is set to "about:blank".
// This is also how the browser behaves.
if (referer !== 'null') {
requestInit.headers['referer'] = referer;
}

requestInit.headers['user-agent'] = document.defaultView.navigator.userAgent;
requestInit.headers['cookie'] = document.defaultView.document.cookie;
if (cookie) {
requestInit.headers['set-cookie'] = cookie;
}

let request;

if (typeof url === 'string') {
request = new Request(RelativeURL.getAbsoluteURL(document.defaultView.location, url));
} else if (url instanceof URL) {
// URLs are always absolute, no need for getAbsoluteURL.
request = new Request(url);
} else {
request = new Request(RelativeURL.getAbsoluteURL(document.defaultView.location, url.url), {
...url
});
}

return new Promise((resolve, reject) => {
const taskID = taskManager.startTask();

NodeFetch(RelativeURL.getAbsoluteURL(document.defaultView.location, url).href, requestInit)
NodeFetch(request, requestInit)
.then((response) => {
if (taskManager.getTaskCount() === 0) {
reject(new Error('Failed to complete fetch request. Task was canceled.'));
Expand Down
6 changes: 6 additions & 0 deletions packages/happy-dom/src/fetch/RequestInfo.ts
@@ -0,0 +1,6 @@
import { URL } from 'url';
import IRequest from './IRequest';

type RequestInfo = IRequest | string | URL;

export default RequestInfo;
3 changes: 2 additions & 1 deletion packages/happy-dom/src/window/IWindow.ts
Expand Up @@ -103,6 +103,7 @@ import { Performance } from 'perf_hooks';
import IElement from '../nodes/element/IElement';
import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction';
import IHappyDOMSettings from './IHappyDOMSettings';
import RequestInfo from '../fetch/RequestInfo';

/**
* Window without dependencies to server side specific packages.
Expand Down Expand Up @@ -342,7 +343,7 @@ export default interface IWindow extends IEventTarget, NodeJS.Global {
* @param [init] Init.
* @returns Promise.
*/
fetch(url: string, init?: IRequestInit): Promise<IResponse>;
fetch(url: RequestInfo, init?: IRequestInit): Promise<IResponse>;

/**
* Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data).
Expand Down
3 changes: 2 additions & 1 deletion packages/happy-dom/src/window/Window.ts
Expand Up @@ -114,6 +114,7 @@ import NamedNodeMap from '../named-node-map/NamedNodeMap';
import IElement from '../nodes/element/IElement';
import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction';
import IHappyDOMSettings from './IHappyDOMSettings';
import RequestInfo from '../fetch/RequestInfo';

const ORIGINAL_SET_TIMEOUT = setTimeout;
const ORIGINAL_CLEAR_TIMEOUT = clearTimeout;
Expand Down Expand Up @@ -659,7 +660,7 @@ export default class Window extends EventTarget implements IWindow {
* @param [init] Init.
* @returns Promise.
*/
public async fetch(url: string, init?: IRequestInit): Promise<IResponse> {
public async fetch(url: RequestInfo, init?: IRequestInit): Promise<IResponse> {
return await FetchHandler.fetch(this.document, url, init);
}

Expand Down
@@ -1,52 +1,52 @@
// SSL certificate generated for Happy DOM to be able to perform HTTPS requests
export default {
cert: `-----BEGIN CERTIFICATE-----
MIIDYzCCAkugAwIBAgIUJRKB/H66hpet1VfUlm0CiXqePA4wDQYJKoZIhvcNAQEL
BQAwQTELMAkGA1UEBhMCU0UxDjAMBgNVBAgMBVNrYW5lMQ4wDAYDVQQHDAVNYWxt
bzESMBAGA1UECgwJSGFwcHkgRE9NMB4XDTIyMTAxMTIyMDM0OVoXDTMyMTAwODIy
MDM0OVowQTELMAkGA1UEBhMCU0UxDjAMBgNVBAgMBVNrYW5lMQ4wDAYDVQQHDAVN
YWxtbzESMBAGA1UECgwJSGFwcHkgRE9NMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAqerQSQEg/SxVxRiwlItithr5e5EMZo1nsqt/xOxagbmpW3IEmt0j
bpbH7iEF4DDEo7KAOwUCOwVWeFxRoag8lG2ax48wrgjlCna45XDn0Xeg1ARajL04
gs46HZ0VrzIloVGfln0zgt/Vum5BNqs9Oc5fQoBmoP3cAn3dn4ZVcP0AKthtcyPl
q2DuNRN0PV0D2RtMSiAy9l1Ko6N5x+sAeClDyOL+sTDLngZBVeZyOKt9Id15S8Zt
XtA6VMgHnnF3jChn7pag77rsd/y5iANAVNZYqRl+Eg7xaDcsvbgH46UBOrBcB39Q
tTh5Mtjoxep5e3ZDFG+kQ1HUE+iz5O5n0wIDAQABo1MwUTAdBgNVHQ4EFgQU69s9
YSobG/m2SN4L/7zTaF7iDbwwHwYDVR0jBBgwFoAU69s9YSobG/m2SN4L/7zTaF7i
DbwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAi/WUXx2oal8L
YnPlIuKfh49n/K18wXSYG//oFYwxfVxqpYH8hUiXVm/GUcXCxS++hUkaKLqXmH9q
MKJiCrZr3vS+2nsBKopkICu/TLdROl0sAI9lByfnEbfSAzjxe1IWJdK8NdY0y5m5
9pEr/URVIAp/CxrneyASb4q0Jg5To3FR7vYc+2X6wZn0MundKMg6Dp9/A37jiF3l
Tt/EJp299YZcsUzh+LnRuggRjnoOVu1aLcLFlaUiwZfy9m8mLG6B/mdW/qNzNMh9
Oqvg1zfGdpz/4D/2UUUBn6pq1vbsoAaF3OesoA3mfDcegDf/H9woJlpT0Wql+e68
Y3FblSokcA==
-----END CERTIFICATE-----`,
MIIDYzCCAkugAwIBAgIUJRKB/H66hpet1VfUlm0CiXqePA4wDQYJKoZIhvcNAQEL
BQAwQTELMAkGA1UEBhMCU0UxDjAMBgNVBAgMBVNrYW5lMQ4wDAYDVQQHDAVNYWxt
bzESMBAGA1UECgwJSGFwcHkgRE9NMB4XDTIyMTAxMTIyMDM0OVoXDTMyMTAwODIy
MDM0OVowQTELMAkGA1UEBhMCU0UxDjAMBgNVBAgMBVNrYW5lMQ4wDAYDVQQHDAVN
YWxtbzESMBAGA1UECgwJSGFwcHkgRE9NMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAqerQSQEg/SxVxRiwlItithr5e5EMZo1nsqt/xOxagbmpW3IEmt0j
bpbH7iEF4DDEo7KAOwUCOwVWeFxRoag8lG2ax48wrgjlCna45XDn0Xeg1ARajL04
gs46HZ0VrzIloVGfln0zgt/Vum5BNqs9Oc5fQoBmoP3cAn3dn4ZVcP0AKthtcyPl
q2DuNRN0PV0D2RtMSiAy9l1Ko6N5x+sAeClDyOL+sTDLngZBVeZyOKt9Id15S8Zt
XtA6VMgHnnF3jChn7pag77rsd/y5iANAVNZYqRl+Eg7xaDcsvbgH46UBOrBcB39Q
tTh5Mtjoxep5e3ZDFG+kQ1HUE+iz5O5n0wIDAQABo1MwUTAdBgNVHQ4EFgQU69s9
YSobG/m2SN4L/7zTaF7iDbwwHwYDVR0jBBgwFoAU69s9YSobG/m2SN4L/7zTaF7i
DbwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAi/WUXx2oal8L
YnPlIuKfh49n/K18wXSYG//oFYwxfVxqpYH8hUiXVm/GUcXCxS++hUkaKLqXmH9q
MKJiCrZr3vS+2nsBKopkICu/TLdROl0sAI9lByfnEbfSAzjxe1IWJdK8NdY0y5m5
9pEr/URVIAp/CxrneyASb4q0Jg5To3FR7vYc+2X6wZn0MundKMg6Dp9/A37jiF3l
Tt/EJp299YZcsUzh+LnRuggRjnoOVu1aLcLFlaUiwZfy9m8mLG6B/mdW/qNzNMh9
Oqvg1zfGdpz/4D/2UUUBn6pq1vbsoAaF3OesoA3mfDcegDf/H9woJlpT0Wql+e68
Y3FblSokcA==
-----END CERTIFICATE-----`,
key: `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCp6tBJASD9LFXF
GLCUi2K2Gvl7kQxmjWeyq3/E7FqBualbcgSa3SNulsfuIQXgMMSjsoA7BQI7BVZ4
XFGhqDyUbZrHjzCuCOUKdrjlcOfRd6DUBFqMvTiCzjodnRWvMiWhUZ+WfTOC39W6
bkE2qz05zl9CgGag/dwCfd2fhlVw/QAq2G1zI+WrYO41E3Q9XQPZG0xKIDL2XUqj
o3nH6wB4KUPI4v6xMMueBkFV5nI4q30h3XlLxm1e0DpUyAeecXeMKGfulqDvuux3
/LmIA0BU1lipGX4SDvFoNyy9uAfjpQE6sFwHf1C1OHky2OjF6nl7dkMUb6RDUdQT
6LPk7mfTAgMBAAECggEAKkwTkTjAt4UjzK56tl+EMQTB+ep/hb/JgoaChci4Nva6
m9LkJpDJ0yuhlTuPNOGu8XjrxsVWas7HWarRf0Zb3i7yip6wZYI9Ub+AA015x4DZ
/i0fRU2NFbK0cM67qSL4jxG8gj+kZP3HPGNZxHwX/53JxMolwgmvjMc8NgvAlSFd
NnV9h4xtbhUh1NGS5zmP3iU2rwnE8JrIEzwy6axLom7nekAgkdcbAr0UoBs8gcgH
aYNhU4Gz3tGcZZ0IXAfT/bJIH1Ko8AGv4pssWc3BXcmmNdm/+kzvHIxEIV7Qegmo
XG1ZyZCyD/0b4/3e8ySDBEDqwR+HeyTW2isWG2agAQKBgQDp44aTwr3dkIXY30xv
FPfUOipg/B49dWnffYJ9MWc1FT9ijNPAngWSk0EIiEQIazICcUBI4Yji6/KeyqLJ
GdLpDi1CkKqtyh73mjELinYp3EUQgEa77aQogGa2+nMOVfu+O5CtloUrv/A18jX3
+VEyaEASK0fWmnSI0OdlxQHIAQKBgQC5+xOls2F3MlKASvWRLlnW1wHqlDTtVoYg
5Nh8syZH4Ci2UH8tON3A5/7SWNM0t1cgV6Cw4zW8Z2spgIT/W0iYYrQ4hHL1xdCu
+CxL1km4Gy8Uwpsd+KdFahFqF/XTmLzW0HXLxWSK0fTwmdV0SFrKF3MXfTCU2AeZ
jJoMFb6P0wKBgQC3Odw6s0vkYAzLGhuZxfZkVvDOK5RRF0NKpttr0iEFL9EJFkPo
2KKK8jr3QTDy229BBJGUxsJi6u6VwS8HlehpVQbV59kd7oKV/EBBx0XMg1fDlopT
PNbmN7i/zbIG4AsoOyebJZjL7kBzMn1e9vzKHWtcEHXlw/hZGja8vjooAQKBgAeg
xK2HLfg1mCyq5meN/yFQsENu0LzrT5UJzddPgcJw7zqLEqxIKNBAs7Ls8by3yFsL
PQwERa/0jfCl1M6kb9XQNpQa2pw6ANUsWKTDpUJn2wZ+9N3F1RaDwzMWyH5lRVmK
M0qoTfdjpSg5Jwgd75taWt4bxGJWeflSSv8z5R0BAoGAWL8c527AbeBvx2tOYKkD
2TFranvANNcoMrbeviZSkkGvMNDP3p8b6juJwXOIeWNr8q4vFgCzLmq6d1/9gYm2
3XJwwyD0LKlqzkBrrKU47qrnmMosUrIRlrAzd3HbShOptxc6Iz2apSaUDKGKXkaw
gl5OpEjeliU7Mus0BVS858g=
-----END PRIVATE KEY-----`
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCp6tBJASD9LFXF
GLCUi2K2Gvl7kQxmjWeyq3/E7FqBualbcgSa3SNulsfuIQXgMMSjsoA7BQI7BVZ4
XFGhqDyUbZrHjzCuCOUKdrjlcOfRd6DUBFqMvTiCzjodnRWvMiWhUZ+WfTOC39W6
bkE2qz05zl9CgGag/dwCfd2fhlVw/QAq2G1zI+WrYO41E3Q9XQPZG0xKIDL2XUqj
o3nH6wB4KUPI4v6xMMueBkFV5nI4q30h3XlLxm1e0DpUyAeecXeMKGfulqDvuux3
/LmIA0BU1lipGX4SDvFoNyy9uAfjpQE6sFwHf1C1OHky2OjF6nl7dkMUb6RDUdQT
6LPk7mfTAgMBAAECggEAKkwTkTjAt4UjzK56tl+EMQTB+ep/hb/JgoaChci4Nva6
m9LkJpDJ0yuhlTuPNOGu8XjrxsVWas7HWarRf0Zb3i7yip6wZYI9Ub+AA015x4DZ
/i0fRU2NFbK0cM67qSL4jxG8gj+kZP3HPGNZxHwX/53JxMolwgmvjMc8NgvAlSFd
NnV9h4xtbhUh1NGS5zmP3iU2rwnE8JrIEzwy6axLom7nekAgkdcbAr0UoBs8gcgH
aYNhU4Gz3tGcZZ0IXAfT/bJIH1Ko8AGv4pssWc3BXcmmNdm/+kzvHIxEIV7Qegmo
XG1ZyZCyD/0b4/3e8ySDBEDqwR+HeyTW2isWG2agAQKBgQDp44aTwr3dkIXY30xv
FPfUOipg/B49dWnffYJ9MWc1FT9ijNPAngWSk0EIiEQIazICcUBI4Yji6/KeyqLJ
GdLpDi1CkKqtyh73mjELinYp3EUQgEa77aQogGa2+nMOVfu+O5CtloUrv/A18jX3
+VEyaEASK0fWmnSI0OdlxQHIAQKBgQC5+xOls2F3MlKASvWRLlnW1wHqlDTtVoYg
5Nh8syZH4Ci2UH8tON3A5/7SWNM0t1cgV6Cw4zW8Z2spgIT/W0iYYrQ4hHL1xdCu
+CxL1km4Gy8Uwpsd+KdFahFqF/XTmLzW0HXLxWSK0fTwmdV0SFrKF3MXfTCU2AeZ
jJoMFb6P0wKBgQC3Odw6s0vkYAzLGhuZxfZkVvDOK5RRF0NKpttr0iEFL9EJFkPo
2KKK8jr3QTDy229BBJGUxsJi6u6VwS8HlehpVQbV59kd7oKV/EBBx0XMg1fDlopT
PNbmN7i/zbIG4AsoOyebJZjL7kBzMn1e9vzKHWtcEHXlw/hZGja8vjooAQKBgAeg
xK2HLfg1mCyq5meN/yFQsENu0LzrT5UJzddPgcJw7zqLEqxIKNBAs7Ls8by3yFsL
PQwERa/0jfCl1M6kb9XQNpQa2pw6ANUsWKTDpUJn2wZ+9N3F1RaDwzMWyH5lRVmK
M0qoTfdjpSg5Jwgd75taWt4bxGJWeflSSv8z5R0BAoGAWL8c527AbeBvx2tOYKkD
2TFranvANNcoMrbeviZSkkGvMNDP3p8b6juJwXOIeWNr8q4vFgCzLmq6d1/9gYm2
3XJwwyD0LKlqzkBrrKU47qrnmMosUrIRlrAzd3HbShOptxc6Iz2apSaUDKGKXkaw
gl5OpEjeliU7Mus0BVS858g=
-----END PRIVATE KEY-----`
};
12 changes: 9 additions & 3 deletions packages/happy-dom/test/setup.js
Expand Up @@ -115,13 +115,19 @@ class NodeFetchResponse {
}
}

class NodeFetchRequest extends NodeFetchResponse {}
class NodeFetchRequest extends NodeFetchResponse {
constructor(url) {
super();
this.url = url;
}
}

class NodeFetchHeaders {}

jest.mock('node-fetch', () => {
return Object.assign(
(url, options) => {
global.mockedModules.modules['node-fetch'].parameters.url = url;
(request, options) => {
global.mockedModules.modules['node-fetch'].parameters.url = request.url.href;
global.mockedModules.modules['node-fetch'].parameters.init = options;
if (global.mockedModules.modules['node-fetch'].error) {
return Promise.reject(global.mockedModules.modules['node-fetch'].returnValue.error);
Expand Down
85 changes: 62 additions & 23 deletions packages/happy-dom/test/window/Window.test.ts
Expand Up @@ -12,6 +12,7 @@ import Selection from '../../src/selection/Selection';
import DOMException from '../../src/exception/DOMException';
import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum';
import CustomElement from '../../test/CustomElement';
import { URL } from 'url';

describe('Window', () => {
let window: IWindow;
Expand Down Expand Up @@ -457,48 +458,85 @@ describe('Window', () => {
describe('fetch()', () => {
for (const method of ['arrayBuffer', 'blob', 'buffer', 'json', 'text', 'textConverted']) {
it(`Handles successful "${method}" request.`, async () => {
window.location.href = 'https://localhost:8080';
document.cookie = 'name1=value1';
document.cookie = 'name2=value2';

const expectedUrl = 'https://localhost:8080/path/';
const expectedOptions = {};
const expectedOptions = {
method: 'PUT',
headers: {
'test-header': 'test-value'
}
};
const response = await window.fetch(expectedUrl, expectedOptions);
const result = await response[method]();

expect(mockedModules.modules['node-fetch'].parameters.init).toEqual({
...expectedOptions,
headers: {
...expectedOptions.headers,
'user-agent': window.navigator.userAgent,
'set-cookie': 'name1=value1; name2=value2',
referer: window.location.origin
}
});
expect(mockedModules.modules['node-fetch'].parameters.url).toBe(expectedUrl);

expect(mockedModules.modules['node-fetch'].parameters.init.headers['user-agent']).toBe(
window.navigator.userAgent
);
expect(mockedModules.modules['node-fetch'].parameters.init.headers['cookie']).toBe(
window.document.cookie
);
expect(mockedModules.modules['node-fetch'].parameters.init.headers['referer']).toBe(
window.location.origin
);

expect(result).toEqual(mockedModules.modules['node-fetch'].returnValue.response[method]);
});
}

it('Handles relative URL.', async () => {
const expectedPath = '/path/';
const expectedOptions = {};

window.location.href = 'https://localhost:8080';

const response = await window.fetch(expectedPath, expectedOptions);
const response = await window.fetch(expectedPath);
const textResponse = await response.text();

expect(mockedModules.modules['node-fetch'].parameters.url).toBe(
'https://localhost:8080' + expectedPath
);

expect(mockedModules.modules['node-fetch'].parameters.init.headers['user-agent']).toBe(
window.navigator.userAgent
);
expect(mockedModules.modules['node-fetch'].parameters.init.headers['cookie']).toBe(
window.document.cookie
);
expect(mockedModules.modules['node-fetch'].parameters.init.headers['referer']).toBe(
window.location.origin
expect(textResponse).toEqual(mockedModules.modules['node-fetch'].returnValue.response.text);
});

it('Handles URL object.', async () => {
const expectedURL = 'https://localhost:8080/path/';

window.location.href = 'https://localhost:8080';

const response = await window.fetch(new URL(expectedURL));
const textResponse = await response.text();

expect(mockedModules.modules['node-fetch'].parameters.url).toBe(expectedURL);

expect(textResponse).toEqual(mockedModules.modules['node-fetch'].returnValue.response.text);
});

it('Handles Request object with absolute URL.', async () => {
const expectedURL = 'https://localhost:8080/path/';

window.location.href = 'https://localhost:8080';

const response = await window.fetch(new window.Request(expectedURL));
const textResponse = await response.text();

expect(mockedModules.modules['node-fetch'].parameters.url).toBe(expectedURL);

expect(textResponse).toEqual(mockedModules.modules['node-fetch'].returnValue.response.text);
});

it('Handles Request object with relative URL.', async () => {
const expectedPath = '/path/';

window.location.href = 'https://localhost:8080';

const response = await window.fetch(new window.Request(expectedPath));
const textResponse = await response.text();

expect(mockedModules.modules['node-fetch'].parameters.url).toBe(
'https://localhost:8080' + expectedPath
);

expect(textResponse).toEqual(mockedModules.modules['node-fetch'].returnValue.response.text);
Expand All @@ -507,8 +545,9 @@ describe('Window', () => {
it('Handles error JSON request.', async () => {
mockedModules.modules['node-fetch'].returnValue.error = new Error('error');
window.location.href = 'https://localhost:8080';

try {
await window.fetch('/url/', {});
await window.fetch('/url/');
} catch (error) {
expect(error).toBe(mockedModules.modules['node-fetch'].returnValue.error);
}
Expand Down

0 comments on commit 2e45e3e

Please sign in to comment.