taiko
Version:
Taiko is a Node.js library for automating Chromium based browsers
272 lines (252 loc) • 7.99 kB
JavaScript
const { handleUrlRedirection, isElement, isSelector } = require("../helper");
const { eventHandler, eventRegexMap } = require("../eventBus");
const { logEvent } = require("../logger");
const { findElements } = require("../elementSearch");
const nodeURL = require("node:url");
const path = require("node:path");
const { isSameUrl } = require("../util");
let page;
let framePromises;
let frameNavigationPromise;
const createdSessionListener = async (client) => {
let resolve;
eventHandler.emit(
"handlerActingOnNewSession",
new Promise((r) => {
resolve = r;
}),
);
page = client.Page;
framePromises = {};
frameNavigationPromise = {};
await page.bringToFront();
page.frameScheduledNavigation(emitFrameNavigationEvent);
page.frameClearedScheduledNavigation(resolveFrameNavigationEvent);
page.frameNavigated(resolveFrameNavigationEvent);
page.frameStartedLoading(emitFrameEvent);
page.frameStoppedLoading(resolveFrameEvent);
page.loadEventFired((p) => {
logEvent("LoadEventFired");
eventHandler.emit("loadEventFired", p);
});
page.navigatedWithinDocument(() => {
logEvent("LoadEventFired from NavigatedWithinPage");
eventHandler.emit("loadEventFired");
// Navigating within document will always succeed
eventHandler.emit("responseReceived", {
samePageNavigation: true,
response: {
status: 200,
statusText: "",
},
});
});
await page.setLifecycleEventsEnabled({ enabled: true });
page.lifecycleEvent((p) => {
logEvent(`Lifecyle event: ${p.name}`);
eventHandler.emit(p.name, p);
});
addJavascriptDialogOpeningListener();
resolve();
};
eventHandler.on("createdSession", createdSessionListener);
const getJsDialogEventName = (message, type) => {
if (eventRegexMap.size) {
for (const [key, value] of eventRegexMap.entries()) {
if (key.startsWith(type) && value.test(message)) {
return key;
}
}
}
if (eventHandler.listenerCount(type)) {
return type;
}
};
const addJavascriptDialogOpeningListener = () => {
page.javascriptDialogOpening((data) => {
const eventName = getJsDialogEventName(data.message, data.type);
if (eventName) {
eventHandler.emit(eventName, data);
eventRegexMap.delete(eventName);
} else {
throw new Error(
`There is no handler registered for ${data.type} popup displayed on the page ${data.url}.
This might interfere with your test flow. You can use Taiko's ${data.type} API to handle this popup.
Please visit https://docs.taiko.dev/#${data.type} for more details`,
);
}
});
};
const handleJavaScriptDialog = async (accept, promptText) => {
await page.handleJavaScriptDialog({
accept,
promptText,
});
};
const resetPromises = () => {
frameNavigationPromise = {};
framePromises = {};
};
const emitFrameNavigationEvent = (p) => {
if (!frameNavigationPromise?.[p.frameId]) {
logEvent(`Frame navigation started: ${p.frameId}`);
let resolve;
eventHandler.emit(
"frameNavigationEvent",
new Promise((r) => {
resolve = r;
}),
);
frameNavigationPromise[p.frameId] = resolve;
}
};
const resolveFrameNavigationEvent = (p) => {
const frameId = p.frameId ? p.frameId : p.frame.frameId;
if (frameNavigationPromise?.[frameId]) {
logEvent(`Frame navigation resolved: ${frameId}`);
frameNavigationPromise[frameId]();
delete frameNavigationPromise[frameId];
}
};
const emitFrameEvent = (p) => {
if (!framePromises?.[p.frameId]) {
logEvent(`Frame load started: ${p.frameId}`);
let resolve;
eventHandler.emit(
"frameEvent",
new Promise((r) => {
resolve = r;
}),
);
framePromises[p.frameId] = resolve;
}
};
const resolveFrameEvent = (p) => {
if (framePromises?.[p.frameId]) {
logEvent(`Frame load resolved: ${p.frameId}`);
framePromises[p.frameId]();
delete framePromises[p.frameId];
}
};
const normalizeAndHandleRedirection = (urlString) => {
let url = nodeURL.parse(urlString);
url = handleUrlRedirection(url.href);
if (url.protocol === "file:") {
url.path = path.normalize(url.path);
}
return url.toString();
};
const handleNavigation = async (gotoUrl) => {
let resolveResponse;
let requestId;
const response = {};
const urlToNavigate = normalizeAndHandleRedirection(
nodeURL.parse(gotoUrl).href,
);
const handleRequest = (request) => {
if (
requestId &&
request.requestId === requestId &&
request.redirectResponse
) {
const redirectedResponse = {
url: request.redirectResponse.url,
status: {
code: request.redirectResponse.status,
text: request.redirectResponse.statusText,
},
};
response.redirectedResponse = response.redirectedResponse
? response.redirectedResponse.concat([redirectedResponse])
: [redirectedResponse];
}
if (!request.request || !request.request.url) {
return;
}
let requestUrl =
request.request.urlFragment !== undefined &&
request.request.urlFragment !== null
? request.request.url + request.request.urlFragment
: request.request.url;
requestUrl = normalizeAndHandleRedirection(requestUrl);
if (isSameUrl(requestUrl, urlToNavigate)) {
requestId = request.requestId;
}
};
eventHandler.addListener("requestStarted", handleRequest);
const handleResponseStatus = (response) => {
if (requestId === response.requestId) {
resolveResponse(response.response);
}
};
const responsePromise = new Promise((resolve) => {
resolveResponse = resolve;
eventHandler.addListener("responseReceived", handleResponseStatus);
});
try {
const { errorText } = await page.navigate({ url: gotoUrl });
if (errorText) {
throw new Error(
`Navigation to url ${gotoUrl} failed. REASON: ${errorText}`,
);
}
const { url, status, statusText } = await responsePromise;
response.url = url;
response.status = { code: status, text: statusText };
if (status >= 400) {
throw new Error(
`Navigation to url ${gotoUrl} failed.\n STATUS: ${status}, STATUS_TEXT: ${statusText}`,
);
}
return response;
} finally {
eventHandler.removeListener("responseReceived", handleResponseStatus);
eventHandler.removeListener("requestStarted", handleRequest);
}
};
const captureScreenshot = async (domHandler, selector, options) => {
let screenShot;
let clip;
if (isSelector(selector) || isElement(selector)) {
if (options.fullPage) {
console.warn("Ignoring fullPage screenshot as custom selector is found!");
}
const padding = options.padding || 0;
const elems = await findElements(selector);
const { x, y, width, height } = await domHandler.boundBox(elems[0]);
clip = {
x: x - padding,
y: y - padding,
width: width + padding * 2,
height: height + padding * 2,
scale: 1,
};
screenShot = await page.captureScreenshot({ clip });
} else if (options.fullPage) {
const metrics = await page.getLayoutMetrics();
const width = Math.ceil(metrics.contentSize.width);
const height = Math.ceil(metrics.contentSize.height);
clip = { x: 0, y: 0, width, height, scale: 1 };
screenShot = await page.captureScreenshot({ clip });
} else {
screenShot = await page.captureScreenshot();
}
return screenShot;
};
const closePage = async () => page.close();
const reload = async (value) => page.reload({ ignoreCache: value });
const getNavigationHistory = async () => page.getNavigationHistory();
const navigateToHistoryEntry = async (entryId) => {
await page.navigateToHistoryEntry({ entryId });
};
module.exports = {
handleNavigation,
resetPromises,
captureScreenshot,
addJavascriptDialogOpeningListener,
closePage,
reload,
navigateToHistoryEntry,
handleJavaScriptDialog,
getNavigationHistory,
};