taiko
Version:
Taiko is a Node.js library for automating Chromium based browsers
224 lines (212 loc) • 7 kB
JavaScript
const { defaultConfig } = require("./config");
const { eventHandler } = require("./eventBus");
const { isPromise } = require("./helper");
const { logEvent } = require("./logger");
const {
errorMessageForBrowserProcessCrash,
closeBrowser,
} = require("./browser/launcher");
const targetHandler = require("./handlers/targetHandler");
const pageHandler = require("./handlers/pageHandler");
const emulationHandler = require("./handlers/emulationHandler");
const cri = require("chrome-remote-interface");
const isReachable = require("is-reachable");
const numRetries = defaultConfig.criConnectionRetries;
const { pluginHooks } = require("./plugins");
let overlay;
let security;
let _client;
let page;
let network;
let dom;
const createProxyForCDPDomain = (cdpClient, cdpDomainName) => {
const cdpDomain = cdpClient[cdpDomainName];
const cdpDomainProxy = new Proxy(cdpDomain, {
get: (target, name) => {
const domainApi = target[name];
if (typeof domainApi === "function") {
return async (...args) => {
return await new Promise((resolve, reject) => {
eventHandler.removeAllListeners("browserCrashed");
eventHandler.on("browserCrashed", () => {
if (_client) {
_client.removeAllListeners();
}
_client = null;
reject();
});
const res = domainApi.apply(null, args);
if (isPromise(res)) {
res.then(resolve).catch(reject);
} else {
resolve(res);
}
}).catch((e) => {
if (e.message.match(/WebSocket is not open: readyState 3/i)) {
errorMessageForBrowserProcessCrash();
}
throw e;
});
};
}
return domainApi;
},
});
cdpClient[cdpDomainName] = cdpDomainProxy;
return cdpClient[cdpDomainName];
};
const initCRIProperties = (c) => {
page = createProxyForCDPDomain(c, "Page");
network = createProxyForCDPDomain(c, "Network");
createProxyForCDPDomain(c, "Runtime");
createProxyForCDPDomain(c, "Input");
dom = createProxyForCDPDomain(c, "DOM");
overlay = createProxyForCDPDomain(c, "Overlay");
security = createProxyForCDPDomain(c, "Security");
createProxyForCDPDomain(c, "Browser");
createProxyForCDPDomain(c, "Target");
createProxyForCDPDomain(c, "Emulation");
_client = c;
};
const initCRI = async (target, n, options = {}) => {
try {
const preConnectionResult = pluginHooks.preConnectionHook(target, options);
const c = await cri({
target: preConnectionResult.target,
host: defaultConfig.host,
port: defaultConfig.port,
useHostName: defaultConfig.useHostName,
secure: defaultConfig.secure,
alterPath: defaultConfig.alterPath,
local: defaultConfig.local,
});
const promises = [];
eventHandler.on("handlerActingOnNewSession", (promise) => {
promises.push(promise);
});
initCRIProperties(c);
const domainEnablePromises = [
network.enable(),
page.enable(),
dom.enable(),
security.enable(),
];
if (!defaultConfig.firefox) {
domainEnablePromises.push(overlay.enable());
}
await Promise.all(domainEnablePromises);
_client.on("disconnect", reconnect);
// Should be emitted after enabling all domains. All handlers can then perform any action on domains properly.
eventHandler.emit("createdSession", _client, preConnectionResult.target);
if (defaultConfig.ignoreSSLErrors) {
security.setIgnoreCertificateErrors({ ignore: true });
}
defaultConfig.device = process.env.TAIKO_EMULATE_DEVICE;
if (defaultConfig.device) {
emulationHandler.emulateDevice(defaultConfig.device);
}
await Promise.all(promises);
eventHandler.removeAllListeners("handlerActingOnNewSession");
logEvent("Session Created");
return _client;
} catch (error) {
logEvent(error);
if (n < 2) {
throw error;
}
return new Promise((r) => setTimeout(r, 100)).then(
async () => await initCRI(target, n - 1, options),
);
}
};
const connect_to_cri = async (target, options = {}) => {
const _target = defaultConfig.local ? defaultConfig.browserDebugUrl : target;
if (_client && _client._ws.readyState === 1) {
if (!defaultConfig.firefox) {
await network.setRequestInterception({
patterns: [],
});
}
_client.removeAllListeners();
}
const tgt =
_target || (await targetHandler.waitForTargetToBeCreated(numRetries));
return initCRI(tgt, numRetries, options);
};
const closeConnection = async (promisesToBeResolvedBeforeCloseBrowser) => {
if (_client) {
// remove listeners other than JS dialog for beforeUnload on client first to stop executing them when closing
await _client.removeAllListeners();
pageHandler.addJavascriptDialogOpeningListener();
if (!defaultConfig.firefox) {
await pageHandler.closePage();
await new Promise((resolve) => {
const timeout = setTimeout(() => {
resolve();
}, defaultConfig.retryTimeout);
Promise.all(promisesToBeResolvedBeforeCloseBrowser).then(() => {
clearTimeout(timeout);
resolve();
});
});
}
}
defaultConfig.connectedToRemoteBrowser
? await _client.Browser.close()
: await closeBrowser();
await _client.close();
_client = null;
};
async function reconnect() {
const response = await isReachable(
`${defaultConfig.host}:${defaultConfig.port}`,
);
if (response) {
try {
logEvent("Reconnecting");
eventHandler.emit("reconnecting");
_client.removeAllListeners();
const pages = await targetHandler.getFirstAvailablePageTarget();
await connect_to_cri(pages[0].targetId);
logEvent("Reconnected");
eventHandler.emit("reconnected");
} catch (e) {}
}
}
const cleanUpListenersOnClient = async () => {
_client.removeAllListeners();
await _client.close();
};
const validate = () => {
if (!_client) {
throw new Error(
"Browser or page not initialized. Call `openBrowser()` before using this API",
);
}
if (_client._ws.readyState > 1) {
errorMessageForBrowserProcessCrash();
throw new Error(
"Connection to browser lost. This probably isn't a problem with Taiko, inspect logs for possible causes.",
);
}
};
const getClient = () => _client;
eventHandler.addListener("targetCreated", async (newTarget) => {
const response = await isReachable(
`${defaultConfig.host}:${defaultConfig.port}`,
);
if (response) {
const pages = await targetHandler.getFirstAvailablePageTarget();
await connect_to_cri(pages[0].targetId).then(() => {
logEvent(`Target Navigated: Target id: ${newTarget.targetInfo.targetId}`);
eventHandler.emit("targetNavigated");
});
}
});
module.exports = {
connect_to_cri,
closeConnection,
cleanUpListenersOnClient,
validate,
getClient,
};