Skip to content

Commit 8aa6eee

Browse files
authored
fix: decode page in native automation mode (#8099)
<!-- Thank you for your contribution. Before making a PR, please read our contributing guidelines at https://github.com/DevExpress/testcafe/blob/master/CONTRIBUTING.md#code-contribution We recommend creating a *draft* PR, so that you can mark it as 'ready for review' when you are done. --> ## Purpose Incorrect decoding page in native automation mode. ## Approach The methods for decoding a page to a string have been modified. [PR-hammerhead](DevExpress/testcafe-hammerhead#2984) ## References _Provide a link to the existing issue(s), if any._ ## Pre-Merge TODO - [x] Write tests for your proposed changes - [x] Make sure that existing tests do not fail
1 parent 4a00f1b commit 8aa6eee

File tree

9 files changed

+77
-30
lines changed

9 files changed

+77
-30
lines changed

package-lock.json

Lines changed: 8 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
"source-map-support": "^0.5.16",
143143
"strip-bom": "^2.0.0",
144144
"testcafe-browser-tools": "2.0.26",
145-
"testcafe-hammerhead": "31.7.0",
145+
"testcafe-hammerhead": "31.7.1",
146146
"testcafe-legacy-api": "5.1.6",
147147
"testcafe-reporter-json": "^2.1.0",
148148
"testcafe-reporter-list": "^2.2.0",

src/native-automation/request-pipeline/index.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import CertificateErrorEvent = Protocol.Security.CertificateErrorEvent;
1212
import HeaderEntry = Protocol.Fetch.HeaderEntry;
1313
import NativeAutomationRequestHookEventProvider from '../request-hooks/event-provider';
1414
import ResourceInjector, { ResourceInjectorOptions } from '../resource-injector';
15-
import { convertToHeaderEntries } from '../utils/headers';
15+
import { convertToHeaderEntries, getHeaderEntry } from '../utils/headers';
16+
import httpHeaders from '../../utils/http-headers';
1617

1718
import {
1819
createRequestPausedEventForResponse,
@@ -157,12 +158,13 @@ export default class NativeAutomationRequestPipeline extends NativeAutomationApi
157158
await this._resourceInjector.processNonProxiedContent(fulfillInfo, this._client, sessionId);
158159
else {
159160
const userScripts = await this._getUserScripts(event);
161+
const contentType = getHeaderEntry(event.responseHeaders, httpHeaders.contentType)?.value;
160162

161163
await this._resourceInjector.processHTMLPageContent(fulfillInfo, {
162164
isIframe: false,
163165
contextStorage: this.contextStorage,
164166
userScripts,
165-
}, this._client, sessionId);
167+
}, this._client, sessionId, contentType);
166168
}
167169

168170
requestPipelineMockLogger(`sent mocked response for the ${event.requestId}`);
@@ -201,7 +203,8 @@ export default class NativeAutomationRequestPipeline extends NativeAutomationApi
201203
return;
202204
}
203205

204-
const resourceInfo = await this._resourceInjector.getDocumentResourceInfo(event, this._client);
206+
const contentType = getHeaderEntry(event.responseHeaders, httpHeaders.contentType)?.value;
207+
const resourceInfo = await this._resourceInjector.getDocumentResourceInfo(event, this._client, contentType);
205208

206209
if (resourceInfo.error) {
207210
if (this._shouldRedirectToErrorPage(event)) {
@@ -242,7 +245,7 @@ export default class NativeAutomationRequestPipeline extends NativeAutomationApi
242245
contextStorage: this.contextStorage,
243246
userScripts,
244247
},
245-
this._client, sessionId);
248+
this._client, sessionId, contentType);
246249

247250
this._contextInfo.dispose(getRequestId(event));
248251

@@ -258,9 +261,7 @@ export default class NativeAutomationRequestPipeline extends NativeAutomationApi
258261
}
259262

260263
private static _isPage (responseHeaders: HeaderEntry[] | undefined): boolean {
261-
const contentType = responseHeaders
262-
?.find(header => header.name.toLowerCase() === 'content-type')
263-
?.value;
264+
const contentType = getHeaderEntry(responseHeaders, httpHeaders.contentType)?.value;
264265

265266
if (contentType)
266267
return contentTypeUtils.isPage(contentType);
@@ -502,7 +503,7 @@ export default class NativeAutomationRequestPipeline extends NativeAutomationApi
502503
const headers = this._formatHeadersForContinueResponse(event.request.headers);
503504

504505
for (const changedHeader of reqOpts._changedHeaders) {
505-
const targetHeader = headers.find(header => header.name.toLowerCase() === changedHeader.name) as HeaderEntry;
506+
const targetHeader = getHeaderEntry(headers, changedHeader.name);
506507

507508
if (targetHeader)
508509
targetHeader.value = changedHeader.value;

src/native-automation/resource-injector.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,13 @@ export default class ResourceInjector {
137137
return stringifyHeaderValues(headers);
138138
}
139139

140-
private async _fulfillRequest (client: ProtocolApi, fulfillRequestInfo: FulfillRequestRequest, body: string, sessionId: SessionId): Promise<void> {
140+
private async _fulfillRequest (client: ProtocolApi, fulfillRequestInfo: FulfillRequestRequest, body: string, sessionId: SessionId, contentType?: string): Promise<void> {
141141
await safeFulfillRequest(client, {
142142
requestId: fulfillRequestInfo.requestId,
143143
responseCode: fulfillRequestInfo.responseCode || StatusCodes.OK,
144144
responsePhrase: fulfillRequestInfo.responsePhrase,
145145
responseHeaders: this._processResponseHeaders(fulfillRequestInfo.responseHeaders),
146-
body: toBase64String(body),
146+
body: toBase64String(body, contentType),
147147
}, sessionId);
148148
}
149149

@@ -158,7 +158,7 @@ export default class ResourceInjector {
158158
await navigateTo(client, this._options.specialServiceRoutes.errorPage1);
159159
}
160160

161-
public async getDocumentResourceInfo (event: RequestPausedEvent, client: ProtocolApi): Promise<DocumentResourceInfo> {
161+
public async getDocumentResourceInfo (event: RequestPausedEvent, client: ProtocolApi, contentType?: string): Promise<DocumentResourceInfo> {
162162
const {
163163
requestId,
164164
request,
@@ -184,7 +184,7 @@ export default class ResourceInjector {
184184
}
185185

186186
const responseObj = await client.Fetch.getResponseBody({ requestId });
187-
const responseStr = getResponseAsString(responseObj);
187+
const responseStr = getResponseAsString(responseObj, contentType);
188188

189189
return {
190190
error: null,
@@ -213,7 +213,7 @@ export default class ResourceInjector {
213213
});
214214
}
215215

216-
public async processHTMLPageContent (fulfillRequestInfo: FulfillRequestRequest, injectableResourcesOptions: InjectableResourcesOptions, client: ProtocolApi, sessionId: SessionId): Promise<void> {
216+
public async processHTMLPageContent (fulfillRequestInfo: FulfillRequestRequest, injectableResourcesOptions: InjectableResourcesOptions, client: ProtocolApi, sessionId: SessionId, contentType?: string): Promise<void> {
217217
const injectableResources = await this._prepareInjectableResources(injectableResourcesOptions);
218218

219219
// NOTE: an unhandled exception interrupts the test execution,
@@ -227,7 +227,7 @@ export default class ResourceInjector {
227227
this._getPageInjectableResourcesOptions(injectableResourcesOptions),
228228
);
229229

230-
await this._fulfillRequest(client, fulfillRequestInfo, updatedResponseStr, sessionId);
230+
await this._fulfillRequest(client, fulfillRequestInfo, updatedResponseStr, sessionId, contentType);
231231
}
232232
}
233233

src/native-automation/utils/headers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ export function convertToOutgoingHttpHeaders (headers: HeaderEntry[] | undefined
2525
}, {});
2626
}
2727

28+
export function getHeaderEntry (headers: HeaderEntry[] | undefined, headerName: string): HeaderEntry | undefined {
29+
return headers?.find(header => header.name.toLowerCase() === headerName);
30+
}
31+

src/native-automation/utils/string.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import Protocol from 'devtools-protocol';
22
import GetResponseBodyResponse = Protocol.Network.GetResponseBodyResponse;
33
import HeaderEntry = Protocol.Fetch.HeaderEntry;
4+
import { decodeBufferToString, encodeStringToBuffer } from 'testcafe-hammerhead';
45

5-
export function getResponseAsString (response: GetResponseBodyResponse): string {
6-
return response.base64Encoded
7-
? Buffer.from(response.body, 'base64').toString()
8-
: response.body;
6+
export function getResponseAsString (response: GetResponseBodyResponse, contentType?: string): string {
7+
if (!contentType)
8+
return response.base64Encoded ? Buffer.from(response.body, 'base64').toString() : response.body;
9+
10+
const bufferBody = getResponseAsBuffer(response);
11+
12+
return decodeBufferToString(bufferBody, contentType);
913
}
1014

1115
export function getResponseAsBuffer (response: GetResponseBodyResponse): Buffer {
@@ -14,8 +18,10 @@ export function getResponseAsBuffer (response: GetResponseBodyResponse): Buffer
1418
: Buffer.from(response.body);
1519
}
1620

17-
export function toBase64String (str: string): string {
18-
return Buffer.from(str).toString('base64');
21+
export function toBase64String (str: string, contentType?: string): string {
22+
const bufferBody = contentType ? encodeStringToBuffer(str, contentType) : Buffer.from(str);
23+
24+
return bufferBody.toString('base64');
1925
}
2026

2127
export function fromBase64String (str: string): Buffer {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { onlyDescribeInNativeAutomation } = require('../../../utils/skip-in');
2+
3+
onlyDescribeInNativeAutomation('[Regression](GH-7529)', function () {
4+
it('In Native Automation mode, the page should be decoded as in proxy mode', function () {
5+
return runTests('./testcafe-fixtures/index.js');
6+
});
7+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Selector } from 'testcafe';
2+
3+
fixture `Regression GH-7529`
4+
.page `http://localhost:3000/fixtures/regression/gh-7529/`;
5+
6+
test('Decode page in native automation mode', async t => {
7+
const title = await Selector('h1').textContent;
8+
9+
await t.expect(title).eql('codage réussi');
10+
});

test/functional/site/server.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,26 @@ Server.prototype._setupRoutes = function (apiRouter) {
149149
`);
150150
});
151151

152+
this.app.get('/fixtures/regression/gh-7529/', function (req, res) {
153+
const html = `
154+
<!DOCTYPE html>
155+
<html lang="fr">
156+
<head>
157+
<meta charset="ISO-8859-15">
158+
<title>GH-7529</title>
159+
</head>
160+
<body>
161+
<h1>codage réussi</h1>
162+
</body>
163+
</html>
164+
`;
165+
166+
const content = Buffer.from(html, 'latin1');
167+
168+
res.setHeader('content-type', 'text/html; charset=iso-8859-15');
169+
res.send(content);
170+
});
171+
152172
this.app.get('*', function (req, res) {
153173
const reqPath = req.params[0] || '';
154174
const resourcePath = path.join(server.basePath, reqPath);

0 commit comments

Comments
 (0)