Skip to content

Commit adf4e40

Browse files
committed
Draft idea of the function fetch
1 parent 715a751 commit adf4e40

1 file changed

Lines changed: 214 additions & 0 deletions

File tree

lib/api/protocol/fetch.js

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
const { Logger, relativeUrl, uriJoin } = require('../../utils');
2+
const ProtocolAction = require('./_base-action.js');
3+
4+
/**
5+
* Fetch an URL with gettnig the response HTTP headers.
6+
*
7+
* @example
8+
* describe('Fetch command demo', function() {
9+
* test('demoTest', async function(browser) {
10+
* // fetch an url:
11+
* const response = await browser.fetch('https://nightwatchjs.org', function(response) {
12+
* const data = response.value.data;
13+
* const heders = response.value.headers;
14+
* const cookies = response.value.cookies;
15+
* }
16+
* });
17+
*
18+
* test('demoTestAsync', async function(browser) {
19+
* const response = await browser.fetch('https://nightwatchjs.org');
20+
*
21+
* const data = response.data;
22+
* const heders = response.headers;
23+
* const cookies = response.cookies;
24+
* });
25+
*
26+
* });
27+
*
28+
* @method fetch
29+
* @link /#nfetch
30+
* @syntax .fetch(url, [callback])
31+
* @param {string} url The url to navigate to
32+
* @param {Function} [callback]
33+
* @api protocol.navigation
34+
* @since 3.0.0
35+
*/
36+
module.exports = class Action extends ProtocolAction {
37+
38+
async command(url, method = 'GET', body = '', callback = function (r) { return r }) {
39+
if (typeof url != 'string') {
40+
throw new Error('Missing url parameter.');
41+
}
42+
43+
if (relativeUrl(url)) {
44+
if (!this.api.baseUrl) {
45+
throw new Error(`Invalid URL ${url}. When using relative uris, you must ` +
46+
'define a "baseUrl" or "launchUrl" in your nightwatch config.');
47+
}
48+
49+
url = uriJoin(this.api.baseUrl, url);
50+
}
51+
52+
await this.api
53+
.perform(async () => {
54+
cookies = await this.api.getCookies();
55+
})
56+
// Executing the fetch in a separate perform block, because executing
57+
// in the same block leads to instant returning the cookies array instead
58+
// of the actual response. Looks like a strange behavior of the
59+
// Nightwatch.
60+
.perform(async () => {
61+
response = await ThDrupalFetchURL.fetchWithCookies(
62+
url,
63+
method,
64+
body,
65+
cookies,
66+
);
67+
})
68+
.perform(async () => {
69+
const promises = [];
70+
// We need to use the `for` loop here, because the `await` inside the
71+
// forEach loop doesn't work.
72+
// eslint-disable-next-line no-restricted-syntax
73+
for (const cookie of response.cookies) {
74+
if (cookie.maxAge === 0) {
75+
promises.push(this.api.deleteCookie(cookie.name));
76+
} else {
77+
promises.push(this.api.setCookie(cookie));
78+
}
79+
}
80+
await Promise.all(promises);
81+
});
82+
83+
const result = {
84+
status: 0,
85+
value: response,
86+
};
87+
88+
if (typeof callback === 'function') {
89+
const self = this;
90+
callback.call(self, result);
91+
}
92+
return result;
93+
}
94+
95+
static parseSetCookie(cookieString) {
96+
// Split the cookie string by ';' to get the individual pieces
97+
const parts = cookieString.split(';').map((part) => part.trim());
98+
99+
// The first part is always the key=value pair
100+
const [nameValue, ...attributes] = parts;
101+
102+
const [name, ...valueParts] = nameValue.split('=');
103+
const value = valueParts.join('=');
104+
105+
const cookieObject = {
106+
name,
107+
value,
108+
};
109+
110+
// We need to use the for loop here, because the `await` inside the
111+
// forEach loop doesn't work.
112+
// eslint-disable-next-line no-restricted-syntax
113+
for (const attribute of attributes) {
114+
const [attrName, ...attrValueParts] = attribute.split('=');
115+
const attrValue = attrValueParts.join('=');
116+
117+
// Normalize attribute names to camelCase for consistency
118+
const normalizedAttrName = attrName
119+
.replace(/-([a-z])/g, (match, p1) => p1.toUpperCase())
120+
.replace(/-/g, '')
121+
.replace(/^(.)/, (match, p1) => p1.toLowerCase());
122+
123+
// Nightwatch expects the `expiry` to be a timestamp in seconds instead of
124+
// the `expires` name.
125+
if (attrName === 'expires') {
126+
cookieObject.expiry = Math.round(new Date(attrValue).getTime() / 1000);
127+
// If the attribute is a flag (like HttpOnly, Secure, etc.), just set it to true
128+
} else if (attrValue === '') {
129+
cookieObject[normalizedAttrName] = true;
130+
} else {
131+
cookieObject[normalizedAttrName] = attrValue.trim() || null;
132+
}
133+
}
134+
135+
return cookieObject;
136+
}
137+
138+
/**
139+
* Fetches a URL with cookies.
140+
*
141+
* @param {string} url
142+
* The URL to fetch.
143+
* @param {string} [method='GET']
144+
* The HTTP method to use.
145+
* @param {string} [requestBody='']
146+
* The body of the request.
147+
* @param {Array} [cookiesObjects=[]]
148+
* An array of cookie objects.
149+
*
150+
* @return {Promise<Object>}
151+
* A promise that resolves to the response data.
152+
*/
153+
static async fetchWithCookies(
154+
url,
155+
method = 'GET',
156+
requestBody = '',
157+
cookiesObjects = [],
158+
) {
159+
// Convert the array of cookie objects to a single string for the Cookie header
160+
const cookieHeader = cookiesObjects
161+
.map((cookie) => `${cookie.name}=${cookie.value}`)
162+
.join('; ');
163+
const options = {
164+
method,
165+
headers: {
166+
Cookie: cookieHeader,
167+
},
168+
};
169+
if (method !== 'GET' && method !== 'HEAD') {
170+
options.body = requestBody;
171+
}
172+
const response = await fetch(url, options);
173+
const body = await response.text();
174+
const headers = {};
175+
response.headers.forEach((value, name) => {
176+
// Actually, Node.js function fetch() support multiple values only for
177+
// `set-cookie`,for other multiple headers overwrites the previous
178+
// value.
179+
if (name === 'set-cookie') {
180+
if (!headers[name]) {
181+
headers[name] = [];
182+
}
183+
headers[name].push(value);
184+
} else {
185+
headers[name] = value;
186+
}
187+
});
188+
189+
const cookies = [];
190+
if (headers['set-cookie']) {
191+
// We need to use the for loop here, because the `await` inside the
192+
// forEach loop doesn't work.
193+
// eslint-disable-next-line no-restricted-syntax
194+
for (const cookieString of headers['set-cookie']) {
195+
cookies.push(ThDrupalFetchURL.parseSetCookie(cookieString));
196+
}
197+
}
198+
199+
const responseData = {
200+
body,
201+
status: response.status,
202+
statusText: response.statusText,
203+
headers,
204+
cookies,
205+
config: {
206+
url,
207+
drupalPath: url.replace(process.env.DRUPAL_TEST_BASE_URL, ''),
208+
method,
209+
headers: options.headers,
210+
},
211+
};
212+
return responseData;
213+
}
214+
};

0 commit comments

Comments
 (0)