Skip to content

Commit

Permalink
Proxyless: support file protocol, 'disablePageReloads' option (DevExp…
Browse files Browse the repository at this point in the history
…ress#7306)

* proxyless: support file protocol

* new line

* remove comment

* revert the function call

* fix speed option

* fix iframe test

* one more fix

* fix review issues
  • Loading branch information
miherlosev committed Sep 30, 2022
1 parent 3585b26 commit d39d061
Show file tree
Hide file tree
Showing 29 changed files with 327 additions and 242 deletions.
4 changes: 4 additions & 0 deletions gulp/constants/functional-test-globs.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ const DEBUG_GLOB_2 = [
];

const PROXYLESS_TESTS_GLOB = [
...SCREENSHOT_TESTS_GLOB,
'test/functional/fixtures/app-command/test.js',
'test/functional/fixtures/driver/test.js',
'test/functional/fixtures/page-error/test.js',
'test/functional/fixtures/page-js-errors/test.js',
'test/functional/fixtures/api/es-next/disable-reloads/test.js',
'test/functional/fixtures/quarantine/test.js',
];

module.exports = {
Expand Down
38 changes: 26 additions & 12 deletions src/browser/connection/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default class BrowserConnectionGateway {

public constructor (proxy: Proxy, options: { retryTestPages: boolean; proxyless: boolean }) {
this._remotesQueue = new RemotesQueue();
this.connectUrl = proxy.resolveRelativeServiceUrl('/browser/connect');
this.connectUrl = proxy.resolveRelativeServiceUrl(SERVICE_ROUTES.connect);
this.retryTestPages = options.retryTestPages;
this.proxyless = options.proxyless;
this.proxy = proxy;
Expand Down Expand Up @@ -59,17 +59,18 @@ export default class BrowserConnectionGateway {
serviceWorkerScript,
} = loadAssets();

this._dispatch('/browser/connect/{id}', proxy, BrowserConnectionGateway._onConnection);
this._dispatch('/browser/heartbeat/{id}', proxy, BrowserConnectionGateway._onHeartbeat, 'GET', this.proxyless);
this._dispatch('/browser/idle/{id}', proxy, BrowserConnectionGateway._onIdle);
this._dispatch('/browser/idle-forced/{id}', proxy, BrowserConnectionGateway._onIdleForced);
this._dispatch('/browser/status/{id}', proxy, BrowserConnectionGateway._onStatusRequest);
this._dispatch('/browser/status-done/{id}', proxy, BrowserConnectionGateway._onStatusRequestOnTestDone, 'GET', this.proxyless);
this._dispatch('/browser/init-script/{id}', proxy, BrowserConnectionGateway._onInitScriptRequest);
this._dispatch('/browser/init-script/{id}', proxy, BrowserConnectionGateway._onInitScriptResponse, 'POST');
this._dispatch('/browser/active-window-id/{id}', proxy, BrowserConnectionGateway._onGetActiveWindowIdRequest);
this._dispatch('/browser/active-window-id/{id}', proxy, BrowserConnectionGateway._onSetActiveWindowIdRequest, 'POST');
this._dispatch('/browser/close-window/{id}', proxy, BrowserConnectionGateway._onCloseWindowRequest, 'POST');
this._dispatch(`${SERVICE_ROUTES.connect}/{id}`, proxy, BrowserConnectionGateway._onConnection);
this._dispatch(`${SERVICE_ROUTES.heartbeat}/{id}`, proxy, BrowserConnectionGateway._onHeartbeat, 'GET', this.proxyless);
this._dispatch(`${SERVICE_ROUTES.idle}/{id}`, proxy, BrowserConnectionGateway._onIdle);
this._dispatch(`${SERVICE_ROUTES.idleForced}/{id}`, proxy, BrowserConnectionGateway._onIdleForced);
this._dispatch(`${SERVICE_ROUTES.status}/{id}`, proxy, BrowserConnectionGateway._onStatusRequest);
this._dispatch(`${SERVICE_ROUTES.statusDone}/{id}`, proxy, BrowserConnectionGateway._onStatusRequestOnTestDone, 'GET', this.proxyless);
this._dispatch(`${SERVICE_ROUTES.initScript}/{id}`, proxy, BrowserConnectionGateway._onInitScriptRequest);
this._dispatch(`${SERVICE_ROUTES.initScript}/{id}`, proxy, BrowserConnectionGateway._onInitScriptResponse, 'POST');
this._dispatch(`${SERVICE_ROUTES.activeWindowId}/{id}`, proxy, BrowserConnectionGateway._onGetActiveWindowIdRequest);
this._dispatch(`${SERVICE_ROUTES.activeWindowId}/{id}`, proxy, BrowserConnectionGateway._onSetActiveWindowIdRequest, 'POST');
this._dispatch(`${SERVICE_ROUTES.closeWindow}/{id}`, proxy, BrowserConnectionGateway._onCloseWindowRequest, 'POST');
this._dispatch(`${SERVICE_ROUTES.openFileProtocol}/{id}`, proxy, BrowserConnectionGateway._onOpenFileProtocolRequest, 'POST');

proxy.GET(SERVICE_ROUTES.connect, (req: IncomingMessage, res: ServerResponse) => this._connectNextRemoteBrowser(req, res));
proxy.GET(SERVICE_ROUTES.connectWithTrailingSlash, (req: IncomingMessage, res: ServerResponse) => this._connectNextRemoteBrowser(req, res));
Expand Down Expand Up @@ -206,6 +207,19 @@ export default class BrowserConnectionGateway {
}
}

private static _onOpenFileProtocolRequest (req: IncomingMessage, res: ServerResponse, connection: BrowserConnection): void {
if (BrowserConnectionGateway._ensureConnectionReady(res, connection)) {
BrowserConnectionGateway._fetchRequestData(req, data => {
const parsedData = JSON.parse(data);

connection.openFileProtocol(parsedData.url)
.then(() => {
respondWithJSON(res);
});
});
}
}

private async _connectNextRemoteBrowser (req: IncomingMessage, res: ServerResponse): Promise<void> {
preventCaching(res);

Expand Down
84 changes: 51 additions & 33 deletions src/browser/connection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import BrowserJob from '../../runner/browser-job';
import WarningLog from '../../notifications/warning-log';
import BrowserProvider from '../provider';
import { OSInfo } from 'get-os-info';

import SERVICE_ROUTES from './service-routes';
import {
BROWSER_RESTART_TIMEOUT,
BROWSER_CLOSE_TIMEOUT,
Expand All @@ -36,6 +36,7 @@ import TestRun from '../../test-run';
// @ts-ignore
import { TestRun as LegacyTestRun } from 'testcafe-legacy-api';
import { Proxy } from 'testcafe-hammerhead';
import { NextTestRunInfo } from '../../shared/types';

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

Expand All @@ -50,6 +51,7 @@ interface DisconnectionPromise<T> extends Promise<T> {
interface BrowserConnectionStatusResult {
cmd: string;
url: string;
testRunId: string | null;
}

interface HeartbeatStatusResult {
Expand Down Expand Up @@ -99,7 +101,7 @@ export default class BrowserConnection extends EventEmitter {
private testRunAborted: boolean;
public status: BrowserConnectionStatus;
private heartbeatTimeout: NodeJS.Timeout | null;
private pendingTestRunUrl: string | null;
private pendingTestRunInfo: NextTestRunInfo | null;
public url = '';
public idleUrl = '';
private forcedIdleUrl = '';
Expand All @@ -113,6 +115,8 @@ export default class BrowserConnection extends EventEmitter {
public statusRelativeUrl = '';
public statusDoneRelativeUrl = '';
public idleRelativeUrl = '';
public openFileProtocolRelativeUrl = '';
public openFileProtocolUrl = '';
private readonly debugLogger: debug.Debugger;
private osInfo: OSInfo | null = null;

Expand Down Expand Up @@ -156,7 +160,7 @@ export default class BrowserConnection extends EventEmitter {
this.status = BrowserConnectionStatus.uninitialized;
this.idle = true;
this.heartbeatTimeout = null;
this.pendingTestRunUrl = null;
this.pendingTestRunInfo = null;
this.disableMultipleWindows = disableMultipleWindows;
this.proxyless = proxyless;

Expand All @@ -174,21 +178,23 @@ export default class BrowserConnection extends EventEmitter {
}

private _buildCommunicationUrls (proxy: Proxy): void {
this.url = proxy.resolveRelativeServiceUrl(`/browser/connect/${this.id}`);
this.forcedIdleUrl = proxy.resolveRelativeServiceUrl(`/browser/idle-forced/${this.id}`);
this.initScriptUrl = proxy.resolveRelativeServiceUrl(`/browser/init-script/${this.id}`);
this.url = proxy.resolveRelativeServiceUrl(`${SERVICE_ROUTES.connect}/${this.id}`);
this.forcedIdleUrl = proxy.resolveRelativeServiceUrl(`${SERVICE_ROUTES.idleForced}/${this.id}`);
this.initScriptUrl = proxy.resolveRelativeServiceUrl(`${SERVICE_ROUTES.initScript}/${this.id}`);

this.heartbeatRelativeUrl = `/browser/heartbeat/${this.id}`;
this.statusRelativeUrl = `/browser/status/${this.id}`;
this.statusDoneRelativeUrl = `/browser/status-done/${this.id}`;
this.idleRelativeUrl = `/browser/idle/${this.id}`;
this.activeWindowIdUrl = `/browser/active-window-id/${this.id}`;
this.closeWindowUrl = `/browser/close-window/${this.id}`;
this.heartbeatRelativeUrl = `${SERVICE_ROUTES.heartbeat}/${this.id}`;
this.statusRelativeUrl = `${SERVICE_ROUTES.status}/${this.id}`;
this.statusDoneRelativeUrl = `${SERVICE_ROUTES.statusDone}/${this.id}`;
this.idleRelativeUrl = `${SERVICE_ROUTES.idle}/${this.id}`;
this.activeWindowIdUrl = `${SERVICE_ROUTES.activeWindowId}/${this.id}`;
this.closeWindowUrl = `${SERVICE_ROUTES.closeWindow}/${this.id}`;
this.openFileProtocolRelativeUrl = `${SERVICE_ROUTES.openFileProtocol}/${this.id}`;

this.idleUrl = proxy.resolveRelativeServiceUrl(this.idleRelativeUrl);
this.heartbeatUrl = proxy.resolveRelativeServiceUrl(this.heartbeatRelativeUrl);
this.statusUrl = proxy.resolveRelativeServiceUrl(this.statusRelativeUrl);
this.statusDoneUrl = proxy.resolveRelativeServiceUrl(this.statusDoneRelativeUrl);
this.idleUrl = proxy.resolveRelativeServiceUrl(this.idleRelativeUrl);
this.heartbeatUrl = proxy.resolveRelativeServiceUrl(this.heartbeatRelativeUrl);
this.statusUrl = proxy.resolveRelativeServiceUrl(this.statusRelativeUrl);
this.statusDoneUrl = proxy.resolveRelativeServiceUrl(this.statusDoneRelativeUrl);
this.openFileProtocolUrl = proxy.resolveRelativeServiceUrl(this.openFileProtocolRelativeUrl);
}

public set messageBus (messageBus: MessageBus) {
Expand Down Expand Up @@ -273,18 +279,18 @@ export default class BrowserConnection extends EventEmitter {
}, this.HEARTBEAT_TIMEOUT);
}

private async _getTestRunUrl (needPopNext: boolean): Promise<string> {
if (needPopNext || !this.pendingTestRunUrl)
this.pendingTestRunUrl = await this._popNextTestRunUrl();
private async _getTestRunInfo (needPopNext: boolean): Promise<NextTestRunInfo> {
if (needPopNext || !this.pendingTestRunInfo)
this.pendingTestRunInfo = await this._popNextTestRunInfo();

return this.pendingTestRunUrl as string;
return this.pendingTestRunInfo as NextTestRunInfo;
}

private async _popNextTestRunUrl (): Promise<string | null> {
private async _popNextTestRunInfo (): Promise<NextTestRunInfo | null> {
while (this.hasQueuedJobs && !this.currentJob.hasQueuedTestRuns)
this.jobQueue.shift();

return this.hasQueuedJobs ? await this.currentJob.popNextTestRunUrl(this) : null;
return this.hasQueuedJobs ? await this.currentJob.popNextTestRunInfo(this) : null;
}

public getCurrentTestRun (): LegacyTestRun | TestRun | null {
Expand Down Expand Up @@ -496,13 +502,13 @@ export default class BrowserConnection extends EventEmitter {

public renderIdlePage (): string {
return Mustache.render(IDLE_PAGE_TEMPLATE as string, {
userAgent: this.connectionInfo,
statusUrl: this.statusUrl,
heartbeatUrl: this.heartbeatUrl,
initScriptUrl: this.initScriptUrl,
retryTestPages: !!this.browserConnectionGateway.retryTestPages,
idlePageUrl: this.idleUrl,
proxyless: this.proxyless,
userAgent: this.connectionInfo,
statusUrl: this.statusUrl,
heartbeatUrl: this.heartbeatUrl,
initScriptUrl: this.initScriptUrl,
openFileProtocolUrl: this.openFileProtocolUrl,
retryTestPages: !!this.browserConnectionGateway.retryTestPages,
proxyless: this.proxyless,
});
}

Expand Down Expand Up @@ -534,18 +540,26 @@ export default class BrowserConnection extends EventEmitter {
}

if (this.status === BrowserConnectionStatus.opened) {
const testRunUrl = await this._getTestRunUrl(isTestDone || this.testRunAborted);
const nextTestRunInfo = await this._getTestRunInfo(isTestDone || this.testRunAborted);

this.testRunAborted = false;

if (testRunUrl) {
if (nextTestRunInfo) {
this.idle = false;

return { cmd: COMMAND.run, url: testRunUrl };
return {
cmd: COMMAND.run,
testRunId: nextTestRunInfo.testRunId,
url: nextTestRunInfo.url,
};
}
}

return { cmd: COMMAND.idle, url: this.idleUrl };
return {
cmd: COMMAND.idle,
url: this.idleUrl,
testRunId: null,
};
}

public get activeWindowId (): null | string {
Expand All @@ -558,6 +572,10 @@ export default class BrowserConnection extends EventEmitter {
this.provider.setActiveWindowId(this.id, val);
}

public async openFileProtocol (url: string): Promise<void> {
return this.provider.openFileProtocol(this.id, url);
}

public async canUseDefaultWindowActions (): Promise<boolean> {
return this.provider.canUseDefaultWindowActions(this.id);
}
Expand Down
9 changes: 9 additions & 0 deletions src/browser/connection/service-routes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
export default {
connect: '/browser/connect',
connectWithTrailingSlash: '/browser/connect/',
heartbeat: '/browser/heartbeat',
status: '/browser/status',
statusDone: '/browser/status-done',
initScript: '/browser/init-script',
idle: '/browser/idle',
idleForced: '/browser/idle-forced',
activeWindowId: '/browser/active-window-id',
closeWindow: '/browser/close-window',
serviceWorker: '/service-worker.js',
openFileProtocol: '/browser/open-file-protocol',

assets: {
index: '/browser/assets/index.js',
Expand Down
8 changes: 8 additions & 0 deletions src/browser/provider/built-in/dedicated/chrome/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { GET_WINDOW_DIMENSIONS_INFO_SCRIPT } from '../../../utils/client-functions';
import { BrowserClient } from './cdp-client';
import ResourceInjector from '../../../../../proxyless/resource-injector';
import { navigateTo } from '../../../../../proxyless/cdp-utils';

const MIN_AVAILABLE_DIMENSION = 50;

Expand Down Expand Up @@ -163,4 +164,11 @@ export default {
await this.resizeWindow(browserId, newWidth, newHeight, outerWidth, outerHeight);
}
},

async openFileProtocol (browserId, url) {
const { browserClient } = this.openedBrowsers[browserId];
const cdpClient = await browserClient.getActiveClient();

await navigateTo(cdpClient, url);
},
};
4 changes: 4 additions & 0 deletions src/browser/provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,10 @@ export default class BrowserProvider {
this.plugin.setActiveWindowId(browserId, val);
}

public async openFileProtocol (browserId: string, url: string): Promise<void> {
await this.plugin.openFileProtocol(browserId, url);
}

public async closeBrowserChildWindow (browserId: string): Promise<void> {
await this.plugin.closeBrowserChildWindow(browserId);
}
Expand Down
8 changes: 4 additions & 4 deletions src/client/browser/idle-page/index.html.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@

<script type="text/javascript">
var idlePage = new IdlePage({
statusUrl: '{{{statusUrl}}}',
heartbeatUrl: '{{{heartbeatUrl}}}',
initScriptUrl: '{{{initScriptUrl}}}',
idlePageUrl: '{{{idlePageUrl}}}'
statusUrl: '{{{statusUrl}}}',
heartbeatUrl: '{{{heartbeatUrl}}}',
initScriptUrl: '{{{initScriptUrl}}}',
openFileProtocolUrl: '{{{openFileProtocolUrl}}}',
},
{
retryTestPages: {{{retryTestPages}}},
Expand Down
4 changes: 2 additions & 2 deletions src/client/browser/idle-page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class IdlePage {

async _pollStatus () {
const urls = {
statusUrl: this.communicationUrls.statusUrl,
idlePageUrl: this.communicationUrls.idlePageUrl,
statusUrl: this.communicationUrls.statusUrl,
openFileProtocolUrl: this.communicationUrls.openFileProtocolUrl,
};

let { command } = await browser.checkStatus(urls, createXHR, { proxyless: this.options.proxyless });
Expand Down
37 changes: 21 additions & 16 deletions src/client/browser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const MAX_STATUS_RETRY = 5;

const SERVICE_WORKER_LOCATION = LOCATION_ORIGIN + SERVICE_ROUTES.serviceWorker;

const FILE_PROTOCOL_ORIGIN = 'file://';

let allowInitScriptExecution = false;
let heartbeatIntervalId = null;

Expand Down Expand Up @@ -56,6 +58,10 @@ function isCurrentLocation (url) {
return LOCATION_HREF.toLowerCase() === url.toLowerCase();
}

function isFileProtocol (url = '') {
return url.indexOf(FILE_PROTOCOL_ORIGIN) === 0;
}

//API
export function startHeartbeat (heartbeatUrl, createXHR) {
function heartbeat () {
Expand Down Expand Up @@ -103,36 +109,35 @@ export function stopInitScriptExecution () {
allowInitScriptExecution = false;
}

export function redirect (command) {
export function redirect (command, createXHR, openFileProtocolUrl) {
stopInitScriptExecution();
document.location = command.url;
}

function proxylessCheckRedirecting (result, idlePageUrl) {
const shouldStayOnIdle = result.cmd === COMMAND.idle
&& result.url === idlePageUrl
&& isCurrentLocation(idlePageUrl);
if (isFileProtocol(command.url))
sendXHR(openFileProtocolUrl, createXHR, { method: 'POST', data: JSON.stringify({ url: command.url }) }); //eslint-disable-line no-restricted-globals
else
document.location = command.url;
}

const shouldGoToIdle = result.cmd === COMMAND.idle
&& result.url === idlePageUrl
&& !isCurrentLocation(result.url);
function proxylessCheckRedirecting ({ result }) {
if (result.cmd === COMMAND.idle)
return regularCheckRedirecting(result);

return !shouldStayOnIdle
|| result.cmd === COMMAND.run
|| shouldGoToIdle;
// NOTE: The tested page URL can be the same for a few tests.
// So, we are forced to return true for all 'run' commands.
return true;
}

function regularCheckRedirecting (result) {
return (result.cmd === COMMAND.run || result.cmd === COMMAND.idle)
&& !isCurrentLocation(result.url);
}

async function getStatus ({ statusUrl, idlePageUrl }, createXHR, { manualRedirect, proxyless } = {}) {
async function getStatus ({ statusUrl, openFileProtocolUrl }, createXHR, { manualRedirect, proxyless } = {}) {
const result = await sendXHR(statusUrl, createXHR);
const redirecting = proxyless ? proxylessCheckRedirecting(result, idlePageUrl) : regularCheckRedirecting(result);
const redirecting = proxyless ? proxylessCheckRedirecting({ result }) : regularCheckRedirecting(result);

if (redirecting && !manualRedirect)
redirect(result);
redirect(result, createXHR, openFileProtocolUrl);

return { command: result, redirecting };
}
Expand Down

0 comments on commit d39d061

Please sign in to comment.