Skip to content

Commit c30dd64

Browse files
perf: speed up creating sessions
1 parent 5e9d896 commit c30dd64

8 files changed

Lines changed: 360 additions & 92 deletions

File tree

src/browser/browser.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const CUSTOM_SESSION_OPTS = [
2525
"user",
2626
"key",
2727
"region",
28-
];
28+
] as const;
2929

3030
export type BrowserOpts = {
3131
id: string;
@@ -122,7 +122,9 @@ export class Browser {
122122
});
123123
}
124124

125-
protected _getSessionOptsFromConfig(optNames = CUSTOM_SESSION_OPTS): Record<string, unknown> {
125+
protected _getSessionOptsFromConfig(
126+
optNames: ReadonlyArray<(typeof CUSTOM_SESSION_OPTS)[number]> = CUSTOM_SESSION_OPTS,
127+
): Record<string, unknown> {
126128
return optNames.reduce((options: Record<string, unknown>, optName) => {
127129
if (optName === "transformRequest") {
128130
options[optName] = (req: RequestOptions): RequestOptions => {

src/browser/existing-browser.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import { NEW_ISSUE_LINK } from "../constants/help";
1919
import type { Options } from "@testplane/wdio-types";
2020
import { runWithoutHistory } from "./history";
2121

22-
const OPTIONAL_SESSION_OPTS = ["transformRequest", "transformResponse"];
23-
2422
interface SessionOptions {
2523
sessionId: string;
2624
sessionCaps?: WebdriverIO.Capabilities;
@@ -232,7 +230,7 @@ export class ExistingBrowser extends Browser {
232230
const opts: AttachOptions = {
233231
sessionId,
234232
...sessionOpts,
235-
...this._getSessionOptsFromConfig(OPTIONAL_SESSION_OPTS),
233+
...this._getSessionOptsFromConfig(["transformRequest", "transformResponse"]),
236234
...detectedSessionEnvFlags,
237235
...this._config.sessionEnvFlags,
238236
options: sessionOpts,

src/browser/new-browser.ts

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { URLSearchParams } from "url";
22
import URI from "urijs";
33
import { isBoolean, assign, isEmpty, set } from "lodash";
4-
import { remote } from "@testplane/webdriverio";
5-
import type { Capabilities } from "@testplane/wdio-types";
4+
import { WebDriver } from "@testplane/webdriver";
5+
import { sessionEnvironmentDetector } from "@testplane/wdio-utils";
6+
import { attach, remote } from "@testplane/webdriverio";
7+
import type { Capabilities, Options } from "@testplane/wdio-types";
68

79
import { Browser, BrowserOpts } from "./browser";
810
import signalHandler from "../signal-handler";
@@ -104,9 +106,62 @@ export class NewBrowser extends Browser {
104106
}
105107

106108
protected async _createSession(): Promise<WebdriverIO.Browser> {
107-
const sessionOpts = await this._getSessionOpts();
109+
if (this._isDevtools()) {
110+
return this._getSessionOpts().then(remote);
111+
}
112+
113+
let sessionOptsException = null;
114+
const sessionOptsPromise = this._getSessionOpts() // starts installing browsers + launching driver process...
115+
.catch(exception => {
116+
sessionOptsException = exception;
117+
});
118+
119+
const isUsingThirtPartyGrid = typeof this._config.user === "string" && typeof this._config.key === "string";
120+
let thirdPartyGridCustomOptions = {};
121+
122+
if (isUsingThirtPartyGrid && !this._isLocalGridUrl()) {
123+
const gridUri = new URI(this._config.gridUrl);
124+
125+
thirdPartyGridCustomOptions = await import("./third-party-grid").then(m =>
126+
m.detectBackend({
127+
port: gridUri.port() ? parseInt(gridUri.port(), 10) : DEFAULT_PORT,
128+
hostname: this._getGridHost(gridUri),
129+
user: this._config.user,
130+
key: this._config.key,
131+
protocol: gridUri.protocol(),
132+
region: this._config.region as Options.SauceRegions | undefined,
133+
path: gridUri.path(),
134+
capabilities: this._config.desiredCapabilities, // we dont need exact caps here
135+
}),
136+
);
137+
}
108138

109-
return remote(sessionOpts);
139+
const sessionOpts = await sessionOptsPromise;
140+
141+
if (sessionOptsException) {
142+
throw sessionOptsException;
143+
}
144+
145+
const fullSessionOpts = Object.assign(sessionOpts!, thirdPartyGridCustomOptions);
146+
const session = await WebDriver.newSession(fullSessionOpts).then(res => {
147+
return res;
148+
});
149+
150+
const detectedSessionEnvFlags = sessionEnvironmentDetector({
151+
capabilities: session.capabilities,
152+
requestedCapabilities: fullSessionOpts.capabilities,
153+
});
154+
155+
return attach({
156+
sessionId: session.sessionId,
157+
...session.options,
158+
...this._getSessionOptsFromConfig(["transformRequest", "transformResponse"]),
159+
...detectedSessionEnvFlags,
160+
...this._config.sessionEnvFlags,
161+
options: session.options,
162+
capabilities: session.capabilities,
163+
requestedCapabilities: fullSessionOpts.capabilities as WebdriverIO.Capabilities,
164+
});
110165
}
111166

112167
protected async _setPageLoadTimeout(): Promise<void> {
@@ -133,6 +188,10 @@ export class NewBrowser extends Browser {
133188
return this._config.gridUrl === LOCAL_GRID_URL || getInstance().local;
134189
}
135190

191+
protected _isDevtools(): boolean {
192+
return this._config.automationProtocol === DEVTOOLS_PROTOCOL || getInstance().devtools;
193+
}
194+
136195
protected async _getSessionOpts(): Promise<Capabilities.WebdriverIOConfig> {
137196
const config = this._config;
138197

@@ -150,16 +209,14 @@ export class NewBrowser extends Browser {
150209

151210
const capabilities = await this._extendCapabilities(config);
152211

153-
const { devtools } = getInstance();
154-
155212
const options = {
156213
protocol: gridUri.protocol(),
157214
hostname: this._getGridHost(gridUri),
158215
port: gridUri.port() ? parseInt(gridUri.port(), 10) : DEFAULT_PORT,
159216
path: gridUri.path(),
160217
queryParams: this._getQueryParams(gridUri.query()),
161218
capabilities,
162-
automationProtocol: devtools ? DEVTOOLS_PROTOCOL : config.automationProtocol,
219+
automationProtocol: this._isDevtools() ? DEVTOOLS_PROTOCOL : config.automationProtocol,
163220
connectionRetryTimeout: config.sessionRequestTimeout || config.httpTimeout,
164221
connectionRetryCount: 3,
165222
baseUrl: config.baseUrl,

src/browser/third-party-grid.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// https://github.com/webdriverio/webdriverio/blob/ed92a6f2abfc55b3c4ae887a222371554a7ada61/packages/webdriverio/src/utils/driver.ts
2+
import type { Capabilities, Options } from "@testplane/wdio-types";
3+
4+
const DEFAULT_HOSTNAME = "127.0.0.1";
5+
const DEFAULT_PORT = 4444;
6+
const DEFAULT_PROTOCOL = "http";
7+
const DEFAULT_PATH = "/";
8+
const LEGACY_PATH = "/wd/hub";
9+
10+
const REGION_MAPPING = {
11+
us: "us-west-1.", // default endpoint
12+
eu: "eu-central-1.",
13+
"eu-central-1": "eu-central-1.",
14+
"us-east-4": "us-east-4.",
15+
};
16+
17+
interface BackendConfigurations {
18+
port?: number;
19+
hostname?: string;
20+
user?: string | null;
21+
key?: string | null;
22+
protocol?: string;
23+
region?: Options.SauceRegions | null;
24+
path?: string;
25+
capabilities?: Capabilities.RequestedStandaloneCapabilities | null;
26+
}
27+
28+
function getSauceEndpoint(
29+
region: keyof typeof REGION_MAPPING,
30+
{ isRDC, isVisual }: { isRDC?: boolean; isVisual?: boolean } = {},
31+
): string {
32+
const shortRegion = REGION_MAPPING[region] ? region : "us";
33+
if (isRDC) {
34+
return `${shortRegion}1.appium.testobject.com`;
35+
} else if (isVisual) {
36+
return "hub.screener.io";
37+
}
38+
39+
return `ondemand.${REGION_MAPPING[shortRegion]}saucelabs.com`;
40+
}
41+
42+
export function detectBackend({
43+
port,
44+
hostname,
45+
user,
46+
key,
47+
protocol,
48+
region,
49+
path,
50+
capabilities,
51+
}: BackendConfigurations = {}): Partial<BackendConfigurations> {
52+
/**
53+
* browserstack
54+
* e.g. zHcv9sZ39ip8ZPsxBVJ2
55+
*/
56+
if (typeof user === "string" && typeof key === "string" && key.length === 20) {
57+
return {
58+
protocol: protocol || "https",
59+
hostname: hostname || "hub-cloud.browserstack.com",
60+
port: port || 443,
61+
path: path || LEGACY_PATH,
62+
};
63+
}
64+
65+
/**
66+
* testingbot
67+
* e.g. ec337d7b677720a4dde7bd72be0bfc67
68+
*/
69+
if (typeof user === "string" && typeof key === "string" && key.length === 32) {
70+
return {
71+
protocol: protocol || "https",
72+
hostname: hostname || "hub.testingbot.com",
73+
port: port || 443,
74+
path: path || LEGACY_PATH,
75+
};
76+
}
77+
78+
/**
79+
* Sauce Labs
80+
* e.g. 50aa152c-1932-B2f0-9707-18z46q2n1mb0
81+
*
82+
* For Sauce Labs Legacy RDC we only need to determine if the sauce option has a `testobject_api_key`.
83+
* Same for Sauce Visual where an apiKey can be passed in through the capabilities (soon to be legacy too).
84+
*/
85+
const isVisual = Boolean(
86+
!Array.isArray(capabilities) &&
87+
capabilities &&
88+
(capabilities as WebdriverIO.Capabilities)["sauce:visual"]?.apiKey,
89+
);
90+
if (
91+
(typeof user === "string" && typeof key === "string" && key.length === 36) ||
92+
// Or only RDC or visual
93+
isVisual
94+
) {
95+
const sauceRegion = region as keyof typeof REGION_MAPPING;
96+
97+
return {
98+
protocol: protocol || "https",
99+
hostname: hostname || getSauceEndpoint(sauceRegion, { isVisual }),
100+
port: port || 443,
101+
path: path || LEGACY_PATH,
102+
};
103+
}
104+
105+
/**
106+
* Lambdatest
107+
* e.g. cYAjKrqGwyPjPQv41ICDF4C5OjlxzA9epZsnugVJJxqOReWRWU
108+
*/
109+
if (typeof user === "string" && typeof key === "string" && key.length === 50) {
110+
return {
111+
protocol: protocol || DEFAULT_PROTOCOL,
112+
hostname: hostname || "hub.lambdatest.com",
113+
port: port || 80,
114+
path: path || LEGACY_PATH,
115+
};
116+
}
117+
118+
if (
119+
/**
120+
* user and key are set in config
121+
*/
122+
(typeof user === "string" || typeof key === "string") &&
123+
/**
124+
* but no custom WebDriver endpoint was configured
125+
*/
126+
!hostname
127+
) {
128+
throw new Error(
129+
'A "user" or "key" was provided but could not be connected to a ' +
130+
"known cloud service (Sauce Labs, Browerstack, Testingbot or Lambdatest). " +
131+
"Please check if given user and key properties are correct!",
132+
);
133+
}
134+
135+
/**
136+
* default values if on of the WebDriver criticial options is set
137+
*/
138+
if (hostname || port || protocol || path) {
139+
return {
140+
hostname: hostname || DEFAULT_HOSTNAME,
141+
port: port || DEFAULT_PORT,
142+
protocol: protocol || DEFAULT_PROTOCOL,
143+
path: path || DEFAULT_PATH,
144+
};
145+
}
146+
147+
/**
148+
* no cloud provider detected, pass on provided params and eventually
149+
* fallback to DevTools protocol
150+
*/
151+
return { hostname, port, protocol, path };
152+
}

src/testplane.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { isRunInNodeJsEnv } from "./utils/config";
1818
import { initDevServer } from "./dev-server";
1919
import { ConfigInput } from "./config/types";
2020
import { MasterEventHandler, Test, TestResult } from "./types";
21-
import { preloadWebdriverIO } from "./utils/preload-utils";
21+
import { preloadWebdriver, preloadWebdriverIO } from "./utils/preload-utils";
2222

2323
interface RunOpts {
2424
browsers: string[];
@@ -138,7 +138,7 @@ export class Testplane extends BaseTestplane {
138138

139139
runner.init();
140140

141-
preloadWebdriverIO();
141+
preloadWebdriver().then(preloadWebdriverIO);
142142

143143
await runner.run(
144144
await this._readTests(testPaths, { browsers, sets, grep, replMode }),

src/utils/preload-utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { loadEsm } from "load-esm";
22

3+
export const preloadWebdriver = async (): Promise<void> => {
4+
await loadEsm("@testplane/webdriver").catch(() => {});
5+
};
6+
37
export const preloadWebdriverIO = async (): Promise<void> => {
48
await loadEsm("@testplane/webdriverio").catch(() => {});
59
};

test/src/browser/history/index.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"use strict";
22

3-
const webdriver = require("@testplane/webdriver");
43
const webdriverio = require("@testplane/webdriverio");
54
const proxyquire = require("proxyquire");
65
const { Callstack } = require("../../../../src/browser/history/callstack");
@@ -199,21 +198,19 @@ describe("commands-history", () => {
199198
});
200199
};
201200

202-
describe("Browser", () => {
201+
describe("ExistingBrowser", () => {
203202
let browser;
204203

205204
beforeEach(async () => {
206-
sandbox.stub(webdriverio, "remote").resolves(mkSessionStub_());
207205
sandbox.stub(webdriverio, "attach").resolves(mkSessionStub_());
208-
sandbox.stub(webdriver.WebDriver, "newSession").resolves(mkSessionStub_());
209206
const ExistingBrowser = proxyquire("src/browser/existing-browser", {
210207
"./client-bridge": {
211208
build: sandbox.stub().resolves(),
212209
},
213210
}).ExistingBrowser;
214-
browser = mkExistingBrowser_({ saveHistory: true }, void 0, ExistingBrowser);
211+
browser = mkExistingBrowser_({ saveHistory: true }, undefined, ExistingBrowser);
215212

216-
await browser.init({ sessionId: "session-id", sessionCaps: {}, sessionOpts: { capabilities: {} } });
213+
await browser.init({ sessionOpts: {}, sessionCaps: {} });
217214
});
218215

219216
mkTestsSet(() => browser, "addCommand");

0 commit comments

Comments
 (0)