Skip to content

Commit 9b348de

Browse files
authored
chore: inject resources with CDP (#7186)
* proxyless: inject resources using cdp * fix tests * small refactoring * small refactoring * update hammerhead * stub proxyless tests * fix task script creation * fix proxyless run * revert refactoring
1 parent bc8bac1 commit 9b348de

File tree

17 files changed

+219
-66
lines changed

17 files changed

+219
-66
lines changed

Gulpfile.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const {
3636
MULTIPLE_WINDOWS_TESTS_GLOB,
3737
DEBUG_GLOB_1,
3838
DEBUG_GLOB_2,
39+
PROXYLESS_TESTS_GLOB,
3940
} = require('./gulp/constants/functional-test-globs');
4041

4142
const {
@@ -424,7 +425,7 @@ gulp.task('test-functional-local-debug-1', gulp.series('prepare-tests', 'test-fu
424425
gulp.task('test-functional-local-debug-2', gulp.series('prepare-tests', 'test-functional-local-debug-run-2'));
425426

426427
gulp.step('test-functional-local-proxyless-run', () => {
427-
return testFunctional(TESTS_GLOB, functionalTestConfig.testingEnvironmentNames.localHeadlessChrome, { isProxyless: true });
428+
return testFunctional(PROXYLESS_TESTS_GLOB, functionalTestConfig.testingEnvironmentNames.localHeadlessChrome, { isProxyless: true });
428429
});
429430

430431
gulp.task('test-functional-local-proxyless', gulp.series('prepare-tests', 'test-functional-local-proxyless-run'));

gulp/constants/functional-test-globs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const DEBUG_GLOB_2 = [
2727
...SCREENSHOT_TESTS_GLOB.map(glob => `!${glob}`),
2828
];
2929

30+
const PROXYLESS_TESTS_GLOB = [];
31+
3032
module.exports = {
3133
TESTS_GLOB,
3234
LEGACY_TESTS_GLOB,
@@ -36,4 +38,5 @@ module.exports = {
3638
DEBUG_GLOB_1,
3739
DEBUG_GLOB_2,
3840
SCREENSHOT_TESTS_GLOB,
41+
PROXYLESS_TESTS_GLOB,
3942
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@
137137
"source-map-support": "^0.5.16",
138138
"strip-bom": "^2.0.0",
139139
"testcafe-browser-tools": "2.0.23",
140-
"testcafe-hammerhead": "24.5.24",
140+
"testcafe-hammerhead": "24.6.0",
141141
"testcafe-legacy-api": "5.1.4",
142142
"testcafe-reporter-dashboard": "1.0.0-rc.3",
143143
"testcafe-reporter-json": "^2.1.0",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import BrowserConnection from './index';
2+
3+
class BrowserConnectionTracker {
4+
public activeBrowserConnections: { [id: string]: BrowserConnection };
5+
6+
public constructor () {
7+
this.activeBrowserConnections = {};
8+
}
9+
10+
public add (connection: BrowserConnection): void {
11+
this.activeBrowserConnections[connection.id] = connection;
12+
}
13+
14+
public remove (connection: BrowserConnection): void {
15+
delete this.activeBrowserConnections[connection.id];
16+
}
17+
}
18+
19+
export default new BrowserConnectionTracker();

src/browser/connection/gateway.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,15 @@ import SERVICE_ROUTES from './service-routes';
1818
export default class BrowserConnectionGateway {
1919
private _connections: Dictionary<BrowserConnection> = {};
2020
private _remotesQueue: RemotesQueue;
21-
public readonly domain: string;
2221
public readonly connectUrl: string;
2322
public retryTestPages: boolean;
23+
public readonly proxy: Proxy;
2424

2525
public constructor (proxy: Proxy, options: { retryTestPages: boolean }) {
2626
this._remotesQueue = new RemotesQueue();
27-
// @ts-ignore Need to improve typings of the 'testcafe-hammerhead' module
28-
this.domain = (proxy as any).server1Info.domain;
29-
this.connectUrl = `${this.domain}/browser/connect`;
27+
this.connectUrl = proxy.resolveRelativeServiceUrl('/browser/connect');
3028
this.retryTestPages = options.retryTestPages;
29+
this.proxy = proxy;
3130

3231
this._registerRoutes(proxy);
3332
}

src/browser/connection/index.ts

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import BrowserConnectionStatus from './status';
1717
import HeartbeatStatus from './heartbeat-status';
1818
import { GeneralError, TimeoutError } from '../../errors/runtime';
1919
import { RUNTIME_ERRORS } from '../../errors/types';
20-
import { Dictionary } from '../../configuration/interfaces';
2120
import BrowserConnectionGateway from './gateway';
2221
import BrowserJob from '../../runner/browser-job';
2322
import WarningLog from '../../notifications/warning-log';
@@ -32,11 +31,16 @@ import {
3231
REMOTE_BROWSER_INIT_TIMEOUT,
3332
} from '../../utils/browser-connection-timeouts';
3433
import MessageBus from '../../utils/message-bus';
34+
import BrowserConnectionTracker from './browser-connection-tracker';
35+
import TestRun from '../../test-run';
36+
// @ts-ignore
37+
import { TestRun as LegacyTestRun } from 'testcafe-legacy-api';
38+
import { Proxy } from 'testcafe-hammerhead';
3539

3640
const getBrowserConnectionDebugScope = (id: string): string => `testcafe:browser:connection:${id}`;
3741

38-
const IDLE_PAGE_TEMPLATE = read('../../client/browser/idle-page/index.html.mustache');
39-
const connections: Dictionary<BrowserConnection> = {};
42+
const IDLE_PAGE_TEMPLATE = read('../../client/browser/idle-page/index.html.mustache');
43+
4044

4145
interface DisconnectionPromise<T> extends Promise<T> {
4246
resolve: Function;
@@ -83,31 +87,31 @@ export default class BrowserConnection extends EventEmitter {
8387
public permanent: boolean;
8488
public previousActiveWindowId: string | null;
8589
private readonly disableMultipleWindows: boolean;
86-
private readonly proxyless: boolean;
90+
public readonly proxyless: boolean;
8791
private readonly HEARTBEAT_TIMEOUT: number;
8892
private readonly BROWSER_CLOSE_TIMEOUT: number;
8993
private readonly BROWSER_RESTART_TIMEOUT: number;
9094
public readonly id: string;
9195
private readonly jobQueue: BrowserJob[];
9296
private readonly initScriptsQueue: InitScriptTask[];
93-
private browserConnectionGateway: BrowserConnectionGateway;
97+
public browserConnectionGateway: BrowserConnectionGateway;
9498
private disconnectionPromise: DisconnectionPromise<void> | null;
9599
private testRunAborted: boolean;
96100
public status: BrowserConnectionStatus;
97101
private heartbeatTimeout: NodeJS.Timeout | null;
98102
private pendingTestRunUrl: string | null;
99-
public readonly url: string;
100-
public readonly idleUrl: string;
101-
private forcedIdleUrl: string;
102-
private readonly initScriptUrl: string;
103-
public readonly heartbeatRelativeUrl: string;
104-
public readonly statusRelativeUrl: string;
105-
public readonly statusDoneRelativeUrl: string;
106-
private readonly heartbeatUrl: string;
107-
private readonly statusUrl: string;
108-
public readonly activeWindowIdUrl: string;
109-
public readonly closeWindowUrl: string;
110-
private statusDoneUrl: string;
103+
public url = '';
104+
public idleUrl = '';
105+
private forcedIdleUrl = '';
106+
private initScriptUrl = '';
107+
public heartbeatUrl = '';
108+
public statusUrl = '';
109+
public activeWindowIdUrl = '';
110+
public closeWindowUrl = '';
111+
public statusDoneUrl = '';
112+
public heartbeatRelativeUrl = '';
113+
public statusRelativeUrl = '';
114+
public statusDoneRelativeUrl = '';
111115
private readonly debugLogger: debug.Debugger;
112116
private osInfo: OSInfo | null = null;
113117

@@ -155,24 +159,10 @@ export default class BrowserConnection extends EventEmitter {
155159
this.disableMultipleWindows = disableMultipleWindows;
156160
this.proxyless = proxyless;
157161

158-
this.url = `${gateway.domain}/browser/connect/${this.id}`;
159-
this.idleUrl = `${gateway.domain}/browser/idle/${this.id}`;
160-
this.forcedIdleUrl = `${gateway.domain}/browser/idle-forced/${this.id}`;
161-
this.initScriptUrl = `${gateway.domain}/browser/init-script/${this.id}`;
162-
163-
this.heartbeatRelativeUrl = `/browser/heartbeat/${this.id}`;
164-
this.statusRelativeUrl = `/browser/status/${this.id}`;
165-
this.statusDoneRelativeUrl = `/browser/status-done/${this.id}`;
166-
this.activeWindowIdUrl = `/browser/active-window-id/${this.id}`;
167-
this.closeWindowUrl = `/browser/close-window/${this.id}`;
168-
169-
this.heartbeatUrl = `${gateway.domain}${this.heartbeatRelativeUrl}`;
170-
this.statusUrl = `${gateway.domain}${this.statusRelativeUrl}`;
171-
this.statusDoneUrl = `${gateway.domain}${this.statusDoneRelativeUrl}`;
172-
162+
this._buildCommunicationUrls(gateway.proxy);
173163
this._setEventHandlers();
174164

175-
connections[this.id] = this;
165+
BrowserConnectionTracker.add(this);
176166

177167
this.previousActiveWindowId = null;
178168

@@ -182,6 +172,24 @@ export default class BrowserConnection extends EventEmitter {
182172
process.nextTick(() => this._runBrowser());
183173
}
184174

175+
private _buildCommunicationUrls (proxy: Proxy): void {
176+
this.url = proxy.resolveRelativeServiceUrl(`/browser/connect/${this.id}`);
177+
this.idleUrl = proxy.resolveRelativeServiceUrl(`/browser/idle/${this.id}`);
178+
this.forcedIdleUrl = proxy.resolveRelativeServiceUrl(`/browser/idle-forced/${this.id}`);
179+
this.initScriptUrl = proxy.resolveRelativeServiceUrl(`/browser/init-script/${this.id}`);
180+
181+
this.heartbeatRelativeUrl = `/browser/heartbeat/${this.id}`;
182+
this.statusRelativeUrl = `/browser/status/${this.id}`;
183+
this.statusDoneRelativeUrl = `/browser/status-done/${this.id}`;
184+
this.activeWindowIdUrl = `/browser/active-window-id/${this.id}`;
185+
this.closeWindowUrl = `/browser/close-window/${this.id}`;
186+
187+
this.heartbeatUrl = proxy.resolveRelativeServiceUrl(this.heartbeatRelativeUrl);
188+
this.statusUrl = proxy.resolveRelativeServiceUrl(this.statusRelativeUrl);
189+
this.statusDoneUrl = proxy.resolveRelativeServiceUrl(this.statusDoneRelativeUrl);
190+
191+
}
192+
185193
public set messageBus (messageBus: MessageBus) {
186194
this._messageBus = messageBus;
187195
this.warningLog.callback = WarningLog.createAddWarningCallback(this._messageBus);
@@ -278,8 +286,12 @@ export default class BrowserConnection extends EventEmitter {
278286
return this.hasQueuedJobs ? await this.currentJob.popNextTestRunUrl(this) : null;
279287
}
280288

289+
public getCurrentTestRun (): LegacyTestRun | TestRun | null {
290+
return this.currentJob ? this.currentJob.currentTestRun : null;
291+
}
292+
281293
public static getById (id: string): BrowserConnection | null {
282-
return connections[id] || null;
294+
return BrowserConnectionTracker.activeBrowserConnections[id] || null;
283295
}
284296

285297
private async _restartBrowser (): Promise<void> {
@@ -454,7 +466,7 @@ export default class BrowserConnection extends EventEmitter {
454466
if (this.heartbeatTimeout)
455467
clearTimeout(this.heartbeatTimeout);
456468

457-
delete connections[this.id];
469+
BrowserConnectionTracker.remove(this);
458470

459471
this.status = BrowserConnectionStatus.closed;
460472
this.emit(BrowserConnectionStatus.closed);

src/browser/provider/built-in/dedicated/chrome/cdp-client/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,14 @@ interface VideoFrameData {
4949
export class BrowserClient {
5050
private _clients: Dictionary<ProtocolApiInfo> = {};
5151
private readonly _runtimeInfo: RuntimeInfo;
52-
private readonly _proxyless: boolean;
5352
private _parentTarget?: remoteChrome.TargetInfo;
5453
private readonly debugLogger: debug.Debugger;
5554
private readonly _videoFramesBuffer: VideoFrameData[];
5655
private _lastFrame: VideoFrameData | null;
5756

58-
public constructor (runtimeInfo: RuntimeInfo, proxyless: boolean) {
57+
public constructor (runtimeInfo: RuntimeInfo) {
5958
this._runtimeInfo = runtimeInfo;
6059
this.debugLogger = debug(DEBUG_SCOPE(runtimeInfo.browserId));
61-
this._proxyless = proxyless;
6260

6361
runtimeInfo.browserClient = this;
6462

src/browser/provider/built-in/dedicated/chrome/index.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from './local-chrome';
1111
import { GET_WINDOW_DIMENSIONS_INFO_SCRIPT } from '../../../utils/client-functions';
1212
import { BrowserClient } from './cdp-client';
13+
import RequestsInterceptor from './requests-interceptor';
1314

1415
const MIN_AVAILABLE_DIMENSION = 50;
1516

@@ -43,6 +44,13 @@ export default {
4344
this.setUserAgentMetaInfo(browserId, metaInfo, options);
4445
},
4546

47+
async _setupProxyless (browserId, browserClient) {
48+
const requestsInterceptor = new RequestsInterceptor(browserId);
49+
const cdpClient = await browserClient.getActiveClient();
50+
51+
await requestsInterceptor.setup(cdpClient);
52+
},
53+
4654
async openBrowser (browserId, pageUrl, config, disableMultipleWindows, proxyless) {
4755
const parsedPageUrl = parseUrl(pageUrl);
4856
const runtimeInfo = await this._createRunTimeInfo(parsedPageUrl.hostname, config, disableMultipleWindows);
@@ -70,7 +78,7 @@ export default {
7078
if (!disableMultipleWindows)
7179
runtimeInfo.activeWindowId = this.calculateWindowId();
7280

73-
const browserClient = new BrowserClient(runtimeInfo, proxyless);
81+
const browserClient = new BrowserClient(runtimeInfo);
7482

7583
this.openedBrowsers[browserId] = runtimeInfo;
7684

@@ -79,6 +87,9 @@ export default {
7987
await this._ensureWindowIsExpanded(browserId, runtimeInfo.viewportSize);
8088

8189
this._setUserAgentMetaInfoForEmulatingDevice(browserId, runtimeInfo.config);
90+
91+
if (proxyless)
92+
await this._setupProxyless(browserId, browserClient);
8293
},
8394

8495
async closeBrowser (browserId, closingInfo = {}) {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { ProtocolApi } from 'chrome-remote-interface';
2+
import Protocol from 'devtools-protocol';
3+
import RequestPausedEvent = Protocol.Fetch.RequestPausedEvent;
4+
import RequestPattern = Protocol.Fetch.RequestPattern;
5+
import GetResponseBodyResponse = Protocol.Fetch.GetResponseBodyResponse;
6+
import {
7+
injectResources,
8+
PageInjectableResources,
9+
INJECTABLE_SCRIPTS as HAMMERHEAD_INJECTABLE_SCRIPTS,
10+
} from 'testcafe-hammerhead';
11+
import BrowserConnection from '../../../../connection';
12+
import { SCRIPTS, TESTCAFE_UI_STYLES } from '../../../../../assets/injectables';
13+
14+
const HTTP_STATUS_OK = 200;
15+
16+
export default class RequestsInterceptor {
17+
private readonly _browserId: string;
18+
19+
public constructor (browserId: string) {
20+
this._browserId = browserId;
21+
}
22+
23+
private _getResponseAsString (response: GetResponseBodyResponse): string {
24+
return response.base64Encoded
25+
? Buffer.from(response.body, 'base64').toString()
26+
: response.body;
27+
}
28+
29+
private async _prepareInjectableResources (): Promise<PageInjectableResources> {
30+
const browserConnection = BrowserConnection.getById(this._browserId) as BrowserConnection;
31+
const proxy = browserConnection.browserConnectionGateway.proxy;
32+
const windowId = browserConnection.activeWindowId;
33+
34+
const taskScript = await browserConnection.currentJob.currentTestRun.session.getTaskScript({
35+
referer: '',
36+
cookieUrl: '',
37+
isIframe: false,
38+
withPayload: true,
39+
serverInfo: proxy.server1Info,
40+
windowId,
41+
});
42+
43+
const injectableResources = {
44+
stylesheets: [
45+
TESTCAFE_UI_STYLES,
46+
],
47+
scripts: [
48+
...HAMMERHEAD_INJECTABLE_SCRIPTS,
49+
...SCRIPTS,
50+
],
51+
embeddedScripts: [taskScript],
52+
};
53+
54+
injectableResources.scripts = injectableResources.scripts.map(script => proxy.resolveRelativeServiceUrl(script));
55+
injectableResources.stylesheets = injectableResources.stylesheets.map(style => proxy.resolveRelativeServiceUrl(style));
56+
57+
return injectableResources;
58+
}
59+
60+
public async setup (client: ProtocolApi): Promise<void> {
61+
const fetchAllDocumentsPattern = {
62+
urlPattern: '*',
63+
resourceType: 'Document',
64+
requestStage: 'Response',
65+
} as RequestPattern;
66+
67+
await client.Fetch.enable({ patterns: [fetchAllDocumentsPattern] });
68+
69+
client.Fetch.on('requestPaused', async (params: RequestPausedEvent) => {
70+
const {
71+
requestId,
72+
responseHeaders,
73+
responseStatusCode,
74+
} = params;
75+
76+
const responseObj = await client.Fetch.getResponseBody({ requestId });
77+
const responseStr = this._getResponseAsString(responseObj);
78+
const injectableResources = await this._prepareInjectableResources();
79+
const updatedResponseStr = injectResources(responseStr, injectableResources);
80+
81+
await client.Fetch.fulfillRequest({
82+
requestId,
83+
responseCode: responseStatusCode || HTTP_STATUS_OK,
84+
responseHeaders: responseHeaders || [],
85+
body: Buffer.from(updatedResponseStr).toString('base64'),
86+
});
87+
});
88+
}
89+
}

src/client/test-run/index.js.mustache

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
var fixtureName = {{{fixtureName}}};
2727
var testName = {{{testName}}};
2828
var canUseDefaultWindowActions = {{{canUseDefaultWindowActions}}};
29+
var proxyless = {{{proxyless}}};
2930
3031
var ClientDriver = window['%testCafeDriver%'];
3132
var driver = new ClientDriver(testRunId,
@@ -39,7 +40,8 @@
3940
dialogHandler: dialogHandler,
4041
retryTestPages: retryTestPages,
4142
speed: speed,
42-
canUseDefaultWindowActions: canUseDefaultWindowActions
43+
canUseDefaultWindowActions: canUseDefaultWindowActions,
44+
proxyless: proxyless
4345
}
4446
);
4547

0 commit comments

Comments
 (0)