@progress/kendo-e2e
Version:
Kendo UI end-to-end test utilities.
3,143 lines • 129 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res, err) => function __init() {
if (err) throw err[0];
try {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
} catch (e) {
throw err = [e], e;
}
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/settings/settings.ts
var Settings;
var init_settings = __esm({
"src/settings/settings.ts"() {
Settings = class {
static get browserName() {
return process.env["BROWSER_NAME"] || "chrome";
}
static get browserWidth() {
return +process.env["BROWSER_WIDTH"] || 1366;
}
static get browserHeight() {
return +process.env["BROWSER_HEIGHT"] || 768;
}
static get headless() {
const value = process.env["HEADLESS"];
return value !== void 0 && value.toLocaleLowerCase() === "true";
}
static get baseUrl() {
return process.env["BASE_URL"] || "";
}
static get timeout() {
return +process.env["TIMEOUT"] || 3e4;
}
static get chromeArguments() {
const args = process.env["CHROME_ARGUMENTS"];
return args ? args.split(";").map((arg) => arg.trim()).filter((arg) => arg.length > 0) : [];
}
};
}
});
// src/selenium/driver-manager.ts
var import_selenium_webdriver, import_chrome, import_edge, import_firefox, import_safari, DriverManager;
var init_driver_manager = __esm({
"src/selenium/driver-manager.ts"() {
import_selenium_webdriver = require("selenium-webdriver");
import_chrome = require("selenium-webdriver/chrome");
import_edge = require("selenium-webdriver/edge");
import_firefox = require("selenium-webdriver/firefox");
import_safari = require("selenium-webdriver/safari");
init_settings();
DriverManager = class {
constructor() {
/**
* Default command-line arguments for Chromium-based browsers (Chrome, Edge).
*
* These options ensure consistent test behavior:
* - Fixed window size and scale factor for consistent screenshots
* - Disabled extensions and notifications to avoid interference
* - Reduced logging noise
* - Certificate error handling
* - Disabled search engine choice screen
*/
this.DEFAULT_CHROMIUM_OPTIONS = [
`--window-size=${Settings.browserWidth},${Settings.browserHeight}`,
"--force-device-scale-factor=1",
"--log-level=1",
"--disable-extensions",
"--disable-notifications",
"--disable-search-engine-choice-screen",
"--ignore-certificate-errors"
];
}
/**
* Creates a WebDriver instance based on Settings.browserName.
*
* Automatically selects the appropriate browser driver based on configuration.
* Supports mobile emulation and BiDi protocol for Chrome.
*
* @param options - Optional driver configuration
* @param options.mobileEmulation - Mobile device emulation settings (Chrome only)
* @param options.enableBidi - Enable BiDi protocol for advanced features (Chrome only)
* @returns Configured WebDriver instance
*
* @example
* ```typescript
* const manager = new DriverManager();
*
* // Basic driver
* const driver = manager.getDriver();
*
* // With mobile emulation
* const mobile = manager.getDriver({
* mobileEmulation: { deviceName: 'Pixel 5' }
* });
*
* // With BiDi for CDP features
* const advanced = manager.getDriver({ enableBidi: true });
* ```
*/
getDriver(options = {}) {
switch (Settings.browserName) {
case import_selenium_webdriver.Browser.CHROME: {
return this.getChromeDriver(options);
}
case import_selenium_webdriver.Browser.EDGE: {
return this.getEdgeDriver();
}
case import_selenium_webdriver.Browser.FIREFOX: {
return this.getFirefoxDriver();
}
case import_selenium_webdriver.Browser.SAFARI: {
return this.getSafariDriver();
}
default: {
throw new Error(`Unsupported browser: ${Settings.browserName}`);
}
}
}
/**
* Creates Chrome-specific options with custom arguments and settings.
*
* Configures Chrome with optimal settings for testing, including headless mode
* support, Docker compatibility, mobile emulation, and BiDi protocol.
*
* @param args - Command-line arguments for Chrome (default: DEFAULT_CHROMIUM_OPTIONS)
* @param options - Driver configuration options
* @returns Configured ChromeOptions instance
*
* @example
* ```typescript
* const manager = new DriverManager();
*
* // Get default options
* const options = manager.getChromeOptions();
*
* // Custom arguments
* const customOptions = manager.getChromeOptions([
* '--window-size=1920,1080',
* '--disable-gpu'
* ]);
*
* // With mobile emulation
* const mobileOptions = manager.getChromeOptions(
* manager.DEFAULT_CHROMIUM_OPTIONS,
* { mobileEmulation: { deviceName: 'iPhone 12' } }
* );
* ```
*/
getChromeOptions(args = this.DEFAULT_CHROMIUM_OPTIONS, options = {}) {
const chromeOptions = new import_chrome.Options();
const argumentsToUse = options.chromeArguments ?? [...args, ...Settings.chromeArguments];
argumentsToUse.forEach((argument) => {
chromeOptions.addArguments(argument);
});
if (Settings.headless) {
chromeOptions.addArguments("--headless=new");
}
if (process.env["CI_CONTAINER"]) {
chromeOptions.addArguments("--no-sandbox");
chromeOptions.addArguments("--disable-setuid-sandbox");
}
if (options.mobileEmulation) {
chromeOptions.setMobileEmulation(options.mobileEmulation);
}
if (options.enableBidi) {
chromeOptions.enableBidi();
}
return chromeOptions;
}
/**
* Creates a Chrome WebDriver instance.
*
* @param options - Either pre-configured ChromeOptions or DriverOptions
* @returns Chrome WebDriver instance
*
* @example
* ```typescript
* const manager = new DriverManager();
*
* // Basic Chrome driver
* const driver = manager.getChromeDriver();
*
* // With custom options
* const options = manager.getChromeOptions();
* options.addArguments('--start-maximized');
* const driver = manager.getChromeDriver(options);
* ```
*/
getChromeDriver(options) {
let chromeOptions;
if (options instanceof import_chrome.Options) {
chromeOptions = options;
} else {
chromeOptions = this.getChromeOptions(this.DEFAULT_CHROMIUM_OPTIONS, options);
}
return new import_selenium_webdriver.Builder().forBrowser(import_selenium_webdriver.Browser.CHROME).setChromeOptions(chromeOptions).build();
}
getEdgeOptions(args = this.DEFAULT_CHROMIUM_OPTIONS) {
const options = new import_edge.Options();
args.forEach((argument) => {
options.addArguments(argument);
});
if (Settings.headless) {
options.addArguments("--headless");
}
return options;
}
getEdgeDriver(options = this.getEdgeOptions()) {
return new import_selenium_webdriver.Builder().forBrowser(import_selenium_webdriver.Browser.EDGE).setEdgeOptions(options).build();
}
getFirefoxOptions() {
const options = new import_firefox.Options();
options.addArguments(`--width=${Settings.browserWidth}`);
options.addArguments(`--height=${Settings.browserHeight}`);
options.setLoggingPrefs(import_selenium_webdriver.logging.Level.SEVERE);
if (Settings.headless) {
options.addArguments("--headless");
}
return options;
}
getFirefoxDriver(options = this.getFirefoxOptions()) {
const prefs = new import_selenium_webdriver.logging.Preferences();
prefs.setLevel(import_selenium_webdriver.logging.Type.BROWSER, import_selenium_webdriver.logging.Level.ALL);
const service = new import_firefox.ServiceBuilder().setStdio("inherit");
return new import_selenium_webdriver.Builder().forBrowser(import_selenium_webdriver.Browser.FIREFOX).setLoggingPrefs(prefs).setFirefoxOptions(options).setFirefoxService(service).build();
}
getSafariOptions() {
const options = new import_safari.Options();
return options;
}
getSafariDriver(options = this.getSafariOptions()) {
const driver = new import_selenium_webdriver.Builder().forBrowser(import_selenium_webdriver.Browser.SAFARI).setSafariOptions(options).build();
const size = { width: Settings.browserWidth, height: Settings.browserHeight, x: 0, y: 0 };
driver.manage().window().setRect(size);
return driver;
}
};
}
});
// src/selenium/conditions.ts
var import_selenium_webdriver2, VIEWPORT_SCRIPT, EC;
var init_conditions = __esm({
"src/selenium/conditions.ts"() {
import_selenium_webdriver2 = require("selenium-webdriver");
VIEWPORT_SCRIPT = function(element) {
const box = element.getBoundingClientRect();
const cx = box.left + box.width / 2;
const cy = box.top + box.height / 2;
let target = document.elementFromPoint(cx, cy);
while (target) {
if (target === element) {
return true;
}
target = target.parentElement;
}
return false;
};
EC = class {
/**
* Creates a condition that waits for an element to have specific text.
*
* Compares the element's visible text content (via getText()) with the expected text.
* Match must be exact.
*
* @param element - WebElement to check text of
* @param text - Exact text to wait for
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* const message = await app.find('#message');
* await app.wait(EC.hasText(message, 'Operation completed'));
*
* // Or check without waiting
* const hasCorrectText = await app.hasText('#status', 'Active');
* ```
*/
static hasText(element, text) {
return () => element.getText().then((result) => {
return result === text;
});
}
/**
* Creates a condition that waits for an input element to have a specific value.
*
* Checks the 'value' attribute of form inputs. Perfect for validating input fields
* after auto-fill, dynamic updates, or user interaction.
*
* @param element - Input WebElement to check
* @param value - Expected value
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* const input = await app.find('#username');
* await app.type(input, 'testuser');
* await app.wait(EC.hasValue(input, 'testuser'));
*
* // Wait for auto-filled value
* await app.wait(EC.hasValue(await app.find('#email'), 'user@example.com'));
* ```
*/
static hasValue(element, value) {
return () => element.getAttribute("value").then((result) => {
return result === value;
});
}
/**
* Creates a condition that waits for an element to have keyboard focus.
*
* Checks if the element is the currently active (focused) element in the document.
* Useful for testing keyboard navigation and focus management.
*
* @param element - WebElement to check for focus
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* const input = await app.find('#search');
* await app.click(input);
* await app.wait(EC.hasFocus(input));
*
* // Verify focus moved after Tab key
* await app.sendKey(Key.TAB);
* const nextInput = await app.find('#next-field');
* await app.wait(EC.hasFocus(nextInput));
* ```
*/
static hasFocus(element) {
return async (driver) => {
const focused = await driver.switchTo().activeElement();
return await element.getId() === await focused.getId();
};
}
/**
* Creates a condition that waits for an element to lose keyboard focus.
*
* Opposite of {@link hasFocus}. Useful for testing blur events and focus movement.
*
* @param element - WebElement to check for lack of focus
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* const input = await app.find('#field');
* await app.focus(input);
* await app.sendKey(Key.TAB); // Move focus away
* await app.wait(EC.hasNoFocus(input));
* ```
*/
static hasNoFocus(element) {
return async (driver) => {
const focused = await driver.switchTo().activeElement();
return await element.getId() !== await focused.getId();
};
}
/**
* Creates a condition that waits for an element to have at least one child matching a locator.
*
* Checks if the parent element contains any child elements matching the selector.
* Useful for waiting for dynamic content to load within a container.
*
* @param element - Parent WebElement to search within
* @param locator - Child element selector (By locator or CSS selector string)
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* const list = await app.find('ul#results');
* await app.wait(EC.hasChild(list, 'li'));
*
* // Wait for specific child
* const table = await app.find('#data-table');
* await app.wait(EC.hasChild(table, '.loaded-row'));
* ```
*/
static hasChild(element, locator) {
return async () => {
return locator instanceof import_selenium_webdriver2.By ? element.findElements(locator).then((result) => {
return result.length > 0;
}) : element.findElements(import_selenium_webdriver2.By.css(locator)).then((result) => {
return result.length > 0;
});
};
}
/**
* Creates a condition that waits for an element to have a specific attribute value.
*
* Can check for exact match or partial match (contains). Useful for validating
* data attributes, ARIA attributes, disabled state, etc.
*
* @param element - WebElement to check
* @param attribute - Attribute name (e.g., 'disabled', 'data-id', 'aria-label')
* @param value - Expected value
* @param exactMatch - If true, value must match exactly; if false, attribute must contain value (default: true)
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* const button = await app.find('#submit');
*
* // Wait for button to become disabled
* await app.wait(EC.hasAttribute(button, 'disabled', 'true'));
*
* // Wait for data attribute (partial match)
* await app.wait(EC.hasAttribute(button, 'data-state', 'loading', false));
*
* // Check ARIA label
* await app.wait(EC.hasAttribute(button, 'aria-label', 'Submit form'));
* ```
*/
static hasAttribute(element, attribute, value, exactMatch = true) {
return () => element.getAttribute(attribute).then((result) => {
if (exactMatch) {
return result === value;
} else {
return result.includes(value);
}
});
}
/**
* Creates a condition that waits for an element to have a specific CSS class.
*
* Convenience method that checks the 'class' attribute. Can do exact or partial match.
* Perfect for waiting for state changes reflected in CSS classes.
*
* @param element - WebElement to check
* @param value - Class name to wait for
* @param exactMatch - If true, class attribute must match exactly; if false, must contain the class (default: false)
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* const button = await app.find('#submit');
*
* // Wait for class to be added (partial match)
* await app.wait(EC.hasClass(button, 'active'));
*
* // Wait for exact class attribute
* await app.wait(EC.hasClass(button, 'btn btn-primary', true));
*
* // Wait for loading class
* await app.wait(EC.hasClass(await app.find('.spinner'), 'loading'));
* ```
*/
static hasClass(element, value, exactMatch = false) {
return this.hasAttribute(element, "class", value, exactMatch);
}
/**
* Creates a condition that waits for an element to be visible.
*
* Element must be present in DOM and have display style that makes it visible.
* Accepts WebElement, By locator, or CSS selector string.
*
* @param element - Element to check visibility of (WebElement, By locator, or CSS selector)
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* // Wait for modal to appear
* await app.wait(EC.isVisible('#modal'));
*
* // Wait for element after click
* await app.click('#show-details');
* await app.wait(EC.isVisible('.details-panel'));
*
* // With By locator
* await app.wait(EC.isVisible(By.css('[data-test="banner"]')));
* ```
*/
static isVisible(element) {
return async (driver) => {
try {
if (!(element instanceof import_selenium_webdriver2.WebElement)) {
element = element instanceof import_selenium_webdriver2.By ? await driver.findElement(element) : await driver.findElement(import_selenium_webdriver2.By.css(element));
}
return await element.isDisplayed();
} catch {
return false;
}
};
}
/**
* Creates a condition that waits for an element to become hidden or not present.
*
* Element is considered not visible if it's either not in DOM or has display:none or similar.
* Opposite of {@link isVisible}.
*
* @param element - Element to check (WebElement, By locator, or CSS selector)
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* // Wait for loading spinner to disappear
* await app.wait(EC.notVisible('.spinner'));
*
* // Wait for modal to close
* await app.click('.modal .close');
* await app.wait(EC.notVisible('.modal'));
* ```
*/
static notVisible(element) {
return async (driver) => {
try {
if (!(element instanceof import_selenium_webdriver2.WebElement)) {
element = element instanceof import_selenium_webdriver2.By ? await driver.findElement(element) : await driver.findElement(import_selenium_webdriver2.By.css(element));
}
return !await element.isDisplayed();
} catch {
return true;
}
};
}
/**
* Creates a condition that waits for an element to be in the visible viewport.
*
* Checks if the center point of the element is actually visible in the viewport and not
* covered by other elements. Stricter than {@link isVisible} - element must be scrolled into view.
*
* @param element - Element to check (WebElement, By locator, or CSS selector)
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* // Wait for element to scroll into view
* await app.wait(EC.isInViewport('.footer-content'));
*
* // Verify element is actually visible to user
* await app.scrollIntoView('#target');
* await app.wait(EC.isInViewport('#target'));
* ```
*/
static isInViewport(element) {
return async (driver) => {
try {
if (!(element instanceof import_selenium_webdriver2.WebElement)) {
element = element instanceof import_selenium_webdriver2.By ? await driver.findElement(element) : await driver.findElement(import_selenium_webdriver2.By.css(element));
}
const result = await driver.executeScript(VIEWPORT_SCRIPT, element);
return result + "" === "true";
} catch {
return false;
}
};
}
/**
* Creates a condition that waits for an element to be outside the visible viewport.
*
* Checks if the element is scrolled out of view or covered. Opposite of {@link isInViewport}.
*
* @param element - Element to check (WebElement, By locator, or CSS selector)
* @returns Condition function for use with wait()
*
* @example
* ```typescript
* // Wait for element to scroll out of view
* await app.scrollIntoView('#bottom-element');
* await app.wait(EC.notInViewport('#top-element'));
* ```
*/
static notInViewport(element) {
return async (driver) => {
try {
if (!(element instanceof import_selenium_webdriver2.WebElement)) {
element = element instanceof import_selenium_webdriver2.By ? await driver.findElement(element) : await driver.findElement(import_selenium_webdriver2.By.css(element));
}
const result = await driver.executeScript(VIEWPORT_SCRIPT, element);
return result + "" === "false";
} catch {
return true;
}
};
}
};
}
});
// src/selenium/expect.ts
function expectSelector(driver, by) {
const DEFAULT_TIMEOUT = 3e3;
const DEFAULT_POLL_INTERVAL = 25;
function fail(message) {
throw new Error(message);
}
const getLocatorString = () => {
return by.toString();
};
const toHaveText = async function toHaveText2(expected, opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
let lastText = "";
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const text = (await element.getText())?.trim?.() ?? "";
lastText = text;
if (expected instanceof RegExp) {
if (expected.test(text)) return;
} else if (text === expected) {
return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const exp = expected instanceof RegExp ? `/${expected.source}/` : `"${expected}"`;
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to have text ${exp} but got "${lastText}"` : `Expected element ${locatorStr} to have text ${exp} but got "${lastText}"`;
fail(errorMessage);
};
const toHaveValue = async function toHaveValue2(expected, opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
let lastValue = "";
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const value = await element.getAttribute("value") ?? "";
lastValue = value;
if (value === expected) {
return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to have value "${expected}" but got "${lastValue}"` : `Expected element ${locatorStr} to have value "${expected}" but got "${lastValue}"`;
fail(errorMessage);
};
const toHaveFocus = async function toHaveFocus2(opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const activeElement = await driver.switchTo().activeElement();
const elementId = await element.getId();
const activeId = await activeElement.getId();
if (elementId === activeId) {
return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to have focus within ${timeout}ms` : `Expected element ${locatorStr} to have focus within ${timeout}ms`;
fail(errorMessage);
};
const toHaveNoFocus = async function toHaveNoFocus2(opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const activeElement = await driver.switchTo().activeElement();
const elementId = await element.getId();
const activeId = await activeElement.getId();
if (elementId !== activeId) {
return;
}
} catch {
return;
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to not have focus within ${timeout}ms` : `Expected element ${locatorStr} to not have focus within ${timeout}ms`;
fail(errorMessage);
};
const toHaveAttribute = async function toHaveAttribute2(attribute, expected, opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const exactMatch = opts?.exactMatch ?? true;
const deadline = Date.now() + timeout;
let lastValue = "";
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const value = await element.getAttribute(attribute) ?? "";
lastValue = value;
if (exactMatch) {
if (value === expected) return;
} else {
if (value.includes(expected)) return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const matchType = exactMatch ? "exactly" : "to contain";
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} attribute "${attribute}" ${matchType} "${expected}" but got "${lastValue}"` : `Expected element ${locatorStr} attribute "${attribute}" ${matchType} "${expected}" but got "${lastValue}"`;
fail(errorMessage);
};
const toHaveClass = async function toHaveClass2(expected, opts) {
return toHaveAttribute("class", expected, opts);
};
const toBeVisible = async function toBeVisible2(opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
if (await element.isDisplayed()) {
return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to be visible within ${timeout}ms` : `Expected element ${locatorStr} to be visible within ${timeout}ms`;
fail(errorMessage);
};
const toBeNotVisible = async function toBeNotVisible2(opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
if (!await element.isDisplayed()) {
return;
}
} catch {
return;
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to become hidden within ${timeout}ms` : `Expected element ${locatorStr} to become hidden within ${timeout}ms`;
fail(errorMessage);
};
const toBeEnabled = async function toBeEnabled2(opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
if (await element.isEnabled()) {
return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to be enabled within ${timeout}ms` : `Expected element ${locatorStr} to be enabled within ${timeout}ms`;
fail(errorMessage);
};
const toBeDisabled = async function toBeDisabled2(opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
if (!await element.isEnabled()) {
return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to be disabled within ${timeout}ms` : `Expected element ${locatorStr} to be disabled within ${timeout}ms`;
fail(errorMessage);
};
const toBeChecked = async function toBeChecked2(opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
if (await element.isSelected()) {
return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to be checked within ${timeout}ms` : `Expected element ${locatorStr} to be checked within ${timeout}ms`;
fail(errorMessage);
};
const toBeNotChecked = async function toBeNotChecked2(opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
if (!await element.isSelected()) {
return;
}
} catch {
return;
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to not be checked within ${timeout}ms` : `Expected element ${locatorStr} to not be checked within ${timeout}ms`;
fail(errorMessage);
};
const toContainText = async function toContainText2(expected, opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
let lastText = "";
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const text = (await element.getText())?.trim?.() ?? "";
lastText = text;
if (text.includes(expected)) {
return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to contain text "${expected}" but got "${lastText}"` : `Expected element ${locatorStr} to contain text "${expected}" but got "${lastText}"`;
fail(errorMessage);
};
const toNotContainText = async function toNotContainText2(expected, opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
let lastText = "";
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const text = (await element.getText())?.trim?.() ?? "";
lastText = text;
if (!text.includes(expected)) {
return;
}
} catch {
return;
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to not contain text "${expected}" but got "${lastText}"` : `Expected element ${locatorStr} to not contain text "${expected}" but got "${lastText}"`;
fail(errorMessage);
};
const toHaveCount = async function toHaveCount2(expected, opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
let lastCount = 0;
while (Date.now() < deadline) {
try {
const elements = await driver.findElements(by);
lastCount = elements.length;
if (lastCount === expected) {
return;
}
} catch {
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected ${locatorStr} to have count ${expected} but found ${lastCount}` : `Expected ${locatorStr} to have count ${expected} but found ${lastCount}`;
fail(errorMessage);
};
const toNotHaveText = async function toNotHaveText2(expected, opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
let lastText = "";
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const text = (await element.getText())?.trim?.() ?? "";
lastText = text;
if (expected instanceof RegExp) {
if (!expected.test(text)) return;
} else if (text !== expected) {
return;
}
} catch {
return;
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const exp = expected instanceof RegExp ? `/${expected.source}/` : `"${expected}"`;
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to not have text ${exp} but got "${lastText}"` : `Expected element ${locatorStr} to not have text ${exp} but got "${lastText}"`;
fail(errorMessage);
};
const toNotHaveValue = async function toNotHaveValue2(expected, opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const deadline = Date.now() + timeout;
let lastValue = "";
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const value = await element.getAttribute("value") ?? "";
lastValue = value;
if (value !== expected) {
return;
}
} catch {
return;
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} to not have value "${expected}" but got "${lastValue}"` : `Expected element ${locatorStr} to not have value "${expected}" but got "${lastValue}"`;
fail(errorMessage);
};
const toNotHaveAttribute = async function toNotHaveAttribute2(attribute, expected, opts) {
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
const pollInterval = opts?.pollInterval ?? DEFAULT_POLL_INTERVAL;
const exactMatch = opts?.exactMatch ?? true;
const deadline = Date.now() + timeout;
let lastValue = "";
while (Date.now() < deadline) {
try {
const element = await driver.findElement(by);
const value = await element.getAttribute(attribute) ?? "";
lastValue = value;
if (exactMatch) {
if (value !== expected) return;
} else {
if (!value.includes(expected)) return;
}
} catch {
return;
}
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
}
const locatorStr = getLocatorString();
const matchType = exactMatch ? "exactly" : "to contain";
const customMessage = opts?.message;
const errorMessage = customMessage ? `${customMessage} Expected element ${locatorStr} attribute "${attribute}" to not match ${matchType} "${expected}" but got "${lastValue}"` : `Expected element ${locatorStr} attribute "${attribute}" to not match ${matchType} "${expected}" but got "${lastValue}"`;
fail(errorMessage);
};
const toNotHaveClass = async function toNotHaveClass2(expected, opts) {
return toNotHaveAttribute("class", expected, opts);
};
return {
toHaveText,
toBeVisible,
toHaveValue,
toHaveFocus,
toHaveAttribute,
toHaveClass,
toBeEnabled,
toBeDisabled,
toBeChecked,
toContainText,
toHaveCount,
not: {
toBeVisible: toBeNotVisible,
toHaveFocus: toHaveNoFocus,
toHaveText: toNotHaveText,
toHaveValue: toNotHaveValue,
toHaveAttribute: toNotHaveAttribute,
toHaveClass: toNotHaveClass,
toBeChecked: toBeNotChecked,
toContainText: toNotContainText,
toBeEnabled: toBeDisabled,
toBeDisabled: toBeEnabled
}
};
}
var init_expect = __esm({
"src/selenium/expect.ts"() {
}
});
// src/utils/rgb-to-hex.ts
function rgbToHex(color) {
const match = color.match(/\d+/g);
if (!match || match.length < 3) return color;
const [r, g, b] = match.map(Number);
return "#" + [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("");
}
var init_rgb_to_hex = __esm({
"src/utils/rgb-to-hex.ts"() {
}
});
// src/selenium/web-app.ts
var import_selenium_webdriver3, WebApp;
var init_web_app = __esm({
"src/selenium/web-app.ts"() {
import_selenium_webdriver3 = require("selenium-webdriver");
init_conditions();
init_expect();
init_rgb_to_hex();
WebApp = class {
/**
* Creates a WebApp instance wrapping a Selenium WebDriver.
*
* @param driver - Selenium WebDriver instance to wrap
*/
constructor(driver) {
this.driver = driver;
}
/**
* Finds a single element with automatic waiting.
*
* **Automatically waits** up to the specified timeout for the element to appear in the DOM.
* This eliminates the need for manual waits and handles dynamic content loading.
*
* @param locator - CSS selector string or Selenium By locator
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise resolving to the WebElement
* @throws Error if element is not found within the timeout period
*
* @example
* ```typescript
* // Using CSS selector (recommended)
* const button = await app.find('#submit-btn');
* const firstItem = await app.find('.list-item');
*
* // Using Selenium By locator
* const element = await app.find(By.xpath('//div[@data-test="value"]'));
*
* // With custom timeout
* const slowElement = await app.find('#async-content', { timeout: 20000 });
* ```
*/
async find(locator, { timeout = 1e4, pollTimeout = 25 } = {}) {
const errorMessage = `Failed to find element located by ${locator}.`;
if (locator instanceof import_selenium_webdriver3.By) {
return await this.driver.wait(import_selenium_webdriver3.until.elementLocated(locator), timeout, errorMessage, pollTimeout);
} else {
return await this.driver.wait(import_selenium_webdriver3.until.elementLocated(import_selenium_webdriver3.By.css(locator)), timeout, errorMessage, pollTimeout);
}
}
/**
* Finds all matching elements without waiting.
*
* Returns an empty array if no elements are found. For scenarios where you need to wait
* for at least one element, use {@link findAllWithTimeout} instead.
*
* @param locator - CSS selector string or Selenium By locator
* @returns Promise resolving to array of WebElements (empty if none found)
*
* @example
* ```typescript
* // Get all list items
* const items = await app.findAll('.list-item');
* console.log(`Found ${items.length} items`);
*
* // Iterate over elements
* for (const item of items) {
* const text = await item.getText();
* console.log(text);
* }
*
* // Using By locator
* const buttons = await app.findAll(By.css('button'));
* ```
*/
async findAll(locator) {
if (locator instanceof import_selenium_webdriver3.By) {
return await this.driver.findElements(locator);
} else {
return await this.driver.findElements(import_selenium_webdriver3.By.css(locator));
}
}
/**
* Finds all matching elements with automatic waiting for at least one element to appear.
*
* Waits until at least one element matching the locator appears, then returns all matching elements.
* Use this when you expect elements to load dynamically and need to ensure they're present.
*
* @param locator - CSS selector string or Selenium By locator
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise resolving to array of WebElements
*
* @example
* ```typescript
* // Wait for search results to load
* const results = await app.findAllWithTimeout('.search-result');
*
* // With custom timeout for slow-loading content
* const items = await app.findAllWithTimeout('.async-item', { timeout: 15000 });
* ```
*/
async findAllWithTimeout(locator, { timeout = 1e4, pollTimeout = 25 } = {}) {
const byLocator = locator instanceof import_selenium_webdriver3.By ? locator : import_selenium_webdriver3.By.css(locator);
const start = Date.now();
let elements = [];
while (Date.now() - start < timeout) {
elements = await this.driver.findElements(byLocator);
if (elements.length > 0) {
return elements;
}
await this.driver.sleep(pollTimeout);
}
elements = await this.driver.findElements(byLocator);
return elements;
}
/**
* Finds a child element within a parent element with automatic waiting.
*
* Useful for scoped element searches within a specific container. Automatically waits
* for both the parent and child elements to appear.
*
* @param rootElement - Parent element (WebElement, By locator, or CSS selector)
* @param locator - Child element selector (By locator or CSS selector)
* @param options - Optional configuration
* @param options.waitForChild - Whether to wait for child to appear (default: true)
* @param options.timeout - Maximum time to wait in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise resolving to the child WebElement
* @throws Error if child element is not found within the timeout period
*
* @example
* ```typescript
* // Find button within a specific dialog
* const dialog = await app.find('.modal-dialog');
* const closeBtn = await app.findChild(dialog, '.close-button');
*
* // Or find child directly using parent selector
* const button = await app.findChild('.modal-dialog', 'button.submit');
*
* // Without waiting for child (if you know it exists)
* const child = await app.findChild(parent, '.child', { waitForChild: false });
* ```
*/
async findChild(rootElement, locator, { waitForChild = true, timeout = 1e4, pollTimeout = 25 } = {}) {
if (!(rootElement instanceof import_selenium_webdriver3.WebElement)) {
rootElement = await this.find(rootElement);
}
if (waitForChild) {
const message = `Failed to find child element located by ${locator}.`;
await this.wait(EC.hasChild(rootElement, locator), { timeout, message, pollTimeout });
}
return locator instanceof import_selenium_webdriver3.By ? rootElement.findElement(locator) : rootElement.findElement(import_selenium_webdriver3.By.css(locator));
}
/**
* Finds all child elements within a parent element with automatic waiting.
*
* Similar to {@link findChild} but returns all matching children instead of just the first one.
* Waits for at least one child to appear before returning.
*
* @param rootElement - Parent element (WebElement, By locator, or CSS selector)
* @param locator - Child elements selector (By locator or CSS selector)
* @param options - Optional configuration
* @param options.waitForChild - Whether to wait for at least one child to appear (default: true)
* @param options.timeout - Maximum time to wait in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise resolving to array of child WebElements
*
* @example
* ```typescript
* // Find all rows in a specific table
* const table = await app.find('#data-table');
* const rows = await app.findChildren(table, 'tr');
*
* // Or find children directly using parent selector
* const items = await app.findChildren('.dropdown-menu', 'li');
*
* // Process all children
* for (const row of rows) {
* const text = await row.getText();
* console.log(text);
* }
* ```
*/
async findChildren(rootElement, locator, { waitForChild = true, timeout = 1e4, pollTimeout = 25 } = {}) {
if (!(rootElement instanceof import_selenium_webdriver3.WebElement)) {
rootElement = await this.find(rootElement);
}
if (waitForChild) {
const message = `Failed to find child element located by ${locator}.`;
await this.wait(EC.hasChild(rootElement, locator), { timeout, message, pollTimeout });
}
return locator instanceof import_selenium_webdriver3.By ? rootElement.findElements(locator) : rootElement.findElements(import_selenium_webdriver3.By.css(locator));
}
/**
* Clicks an element with automatic waiting and retry logic.
*
* **Handles common click issues automatically:**
* - Waits for element to appear in DOM
* - Waits for element to be visible
* - Waits for element to be enabled
* - Retries if click fails initially
*
* This eliminates flaky tests caused by timing issues.
*
* @param element - Element to click (WebElement, By locator, or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise that resolves when click succeeds
* @throws Error if element cannot be clicked within timeout
*
* @example
* ```typescript
* // Simple click using CSS selector
* await app.click('#submit-button');
*
* // Click with custom timeout
* await app.click('.slow-loading-btn', { timeout: 15000 });
*
* // Click using By locator
* await app.click(By.xpath('//button[text()="Submit"]'));
*
* // Click a previously found element
* const button = await app.find('#my-button');
* await app.click(button);
* ```
*/
async click(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
try {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element, { timeout, pollTimeout });
}
await element.click();
} catch {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element, { timeout, pollTimeout });
}
const notVisibleMessage = element instanceof import_selenium_webdriver3.WebElement ? `Element located is not visible.` : `Element located by ${element} is not visible.`;
const notEnabledMessage = element instanceof import_selenium_webdriver3.WebElement ? `Element located is not enabled.` : `Element located by ${element} is not enabled.`;
await this.wait(import_selenium_webdriver3.until.elementIsVisible(element), { timeout, message: notVisibleMessage });
await this.wait(import_selenium_webdriver3.until.elementIsEnabled(element), { timeout, message: notEnabledMessage });
await element.click();
}
}
/**
* Moves the mouse cursor over an element (hover action).
*
* Useful for testing hover states, tooltips, dropdown menus, and other hover-triggered UI.
* Automatically waits for the element to be present before hovering.
*
* @param element - Element to hover over (WebElement, By locator, or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait for element in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise that resolves when hover completes
*
* @example
* ```typescript
* // Hover to reveal dropdown menu
* await app.hover('.menu-item');
* await app.click('.submenu-option');
*
* // Hover to show tooltip
* await app.hover('#info-icon');
* const tooltip = await app.find('.tooltip');
* const text = await app.getText(tooltip);
*
* // Hover on element found with By locator
* await app.hover(By.css('[data-test="hover-target"]'));
* ```
*/
async hover(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element, { timeout, pollTimeout });
}
const actions = this.driver.actions({ async: true, bridge: true });
await actions.move({ origin: element }).perform();
}
/**
* Sets focus on an element programmatically.
*
* Directly focuses the element using JavaScript, which is more reliable than clicking for focus.
* Useful for testing keyboard interactions, input fields, and focus-dependent behaviors.
*
* @param element - Element to focus (WebElement, By locator, or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait for element in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise that resolves when focus is set
*
* @example
* ```typescript
* // Focus an input field
* await app.focus('#username');
* await app.sendKey(Key.CONTROL, 'a'); // Select all
*
* // Focus before typing
* await app.focus('#search-box');
* await app.type('#search-box', 'test query');
*
* // Test focus-dependent behavior
* await app.focus('#email');
* await app.expect('.validation-hint').toBeVisible();
* ```
*/
async focus(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element, { timeout, pollTimeout });
}
await this.driver.executeScript(`arguments[0].focus();`, element);
await this.driver.sleep(50);
}
/**
* Performs a right-click (context menu click) on an element.
*
* Opens the context menu for the element, allowing you to test right-click menus and actions.
*
* @param element - Element to right-click (WebElement, By locator, or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait for element in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise that resolves when right-click completes
*
* @example
* ```typescript
* // Open context menu and select option
* await app.contextClick('.file-item');
* await app.click('.context-menu-delete');
*
* // Right-click on canvas element
* await app.contextClick('#drawing-canvas');
* await app.expect('.context-menu').toBeVisible();
* ```
*/
async contextClick(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element, { timeout, pollTimeout });
}
const actions = this.driver.actions({ async: true, bridge: true });
await actions.contextClick(element).perform();
}
/**
* Performs a double-click on an element.
*
* Useful for testing double-click interactions like selecting text, opening files, or
* triggering double-click-specific behaviors in your application.
*
* @param element - Element to double-click (WebElement, By locator, or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait for element in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise that resolves when double-click completes
*
* @example
* ```typescript
* // Double-click to open file
* await app.doubleClick('.file-icon');
*
* // Double-click to select word
* await app.doubleClick('.text-content');
*
* // Double-click with custom wait
* await app.doubleClick('#expandable-item', { timeout: 5000 });
* ```
*/
async doubleClick(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element, { timeout, pollTimeout });
}
const actions = this.driver.actions({ async: true, bridge: true });
await actions.doubleClick(element).perform();
}
/**
* Waits for an element to stop animating, then clicks it.
*
* Perfect for clicking elements that are animating into view (slides, fades, etc.).
* Prevents clicks during animation which can cause missed clicks or wrong targets.
*
* @param element - Element to wait for and click (WebElement, By locator, or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between animation checks in milliseconds (default: 50)
* @returns Promise that resolves when element is stable and clicked
*
* @example
* ```typescript
* // Click button that slides into view
* await app.waitForAnimationAndClick('.animated-button');
*
* // Click element in animated modal
* await app.waitForAnimationAndClick('.modal .submit-btn', { timeout: 5000 });
* ```
*/
async waitForAnimationAndClick(element, { timeout = 1e4, pollTimeout = 50 } = {}) {
await this.waitForAnimation(element, { timeout, pollTimeout });
await this.click(element, { timeout, pollTimeout });
}
/**
* Scrolls element into view and then clicks it.
*
* Handles elements that are not initially in viewport. Scrolls the element to the center
* of the viewport before clicking, ensuring reliable clicks on off-screen elements.
*
* @param element - Element to scroll to and click (WebElement, By locator, or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait for element in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise that resolves when element is scrolled into view and clicked
*
* @example
* ```typescript
* // Click element at bottom of page
* await app.scrollAndClick('#footer-button');
*
* // Scroll and click in long form
* await app.scrollAndClick('.form-submit', { timeout: 5000 });
*
* // Click element in scrollable container
* await app.scrollAndClick('.list-item:last-child');
* ```
*/
async scrollAndClick(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element, { timeout, pollTimeout });
}
await this.driver.executeScript("arguments[0].scrollIntoView({ behavior: 'instant', block: 'center', inline: 'nearest' });", element);
await element.click();
}
/**
* Scrolls an element into the viewport without clicking it.
*
* Useful when you need an element visible for screenshots, visibility checks, or
* before performing other actions. Centers the element in the viewport.
*
* @param locator - Element to scroll to (By locator or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait for element in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise that resolves when element is scrolled into view
*
* @example
* ```typescript
* // Scroll to element for screenshot
* await app.scrollIntoView('#chart');
* const screenshot = await app.getScreenshot();
*
* // Scroll before checking visibility
* await app.scrollIntoView('.lazy-load-content');
* await app.expect('.lazy-load-content').toBeVisible();
* ```
*/
async scrollIntoView(locator, { timeout = 1e4, pollTimeout = 25 } = {}) {
const element = await this.find(locator, { timeout, pollTimeout });
await this.driver.executeScript("arguments[0].scrollIntoView({ behavior: 'instant', block: 'center', inline: 'nearest' });", element);
}
/**
* Drags a source element and drops it onto a target element.
*
* Performs a complete drag-and-drop operation, useful for testing sortable lists,
* drag-and-drop file uploads, kanban boards, and similar interactions.
*
* @param source - Element to drag (WebElement, By locator, or CSS selector)
* @param target - Element to drop onto (WebElement, By locator, or CSS selector)
* @returns Promise that resolves when drag-and-drop completes
*
* @example
* ```typescript
* // Drag list item to reorder
* await app.dragTo(
* By.css('.list-item:nth-child(1)'),
* By.css('.list-item:nth-child(3)')
* );
*
* // Drag file to upload area
* const file = await app.find('.file-icon');
* const dropzone = await app.find('.upload-dropzone');
* await app.dragTo(file, dropzone);
*
* // Drag card between columns (kanban)
* await app.dragTo('#card-1', '#column-done');
* ```
*/
async dragTo(source, target) {
let sourceElement;
let targetElement;
if (source instanceof import_selenium_webdriver3.WebElement) {
sourceElement = source;
} else {
sourceElement = await this.find(source);
}
if (target instanceof import_selenium_webdriver3.WebElement) {
targetElement = target;
} else {
targetElement = await this.find(target);
}
const actions = this.driver.actions({ async: true, bridge: true });
await actions.dragAndDrop(sourceElement, targetElement).perform();
}
/**
* Drags an element by a specified pixel offset.
*
* Moves an element by dragging it a specific number of pixels in X and Y directions.
* Useful for testing sliders, resizable panels, draggable windows, and custom drag interactions.
*
* @param element - Element to drag (WebElement, By locator, or CSS selector)
* @param offsetX - Horizontal offset in pixels (positive = right, negative = left)
* @param offsetY - Vertical offset in pixels (positive = down, negative = up)
* @returns Promise that resolves when drag completes
*
* @example
* ```typescript
* // Drag slider to the right
* await app.dragByOffset('.slider-handle', 100, 0);
*
* // Drag element down and left
* await app.dragByOffset('.draggable-box', -50, 75);
*
* // Resize panel by dragging splitter
* await app.dragByOffset('.splitter', 0, -100);
* ```
*/
async dragByOffset(element, offsetX, offsetY) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element);
}
const actions = this.driver.actions({ async: true, bridge: true });
await actions.dragAndDrop(element, { x: offsetX, y: offsetY }).perform();
}
/**
* Types text into an input element.
*
* Automatically finds the element, optionally clears existing content, types the text,
* and can optionally press Enter after typing. Perfect for form filling and text input.
*
* @param element - Input element to type into (WebElement, By locator, or CSS selector)
* @param text - Text to type
* @param options - Optional configuration
* @param options.clear - Whether to clear existing content first (default: true)
* @param options.sendEnter - Whether to press Enter after typing (default: false)
* @returns Promise that resolves when typing completes
*
* @example
* ```typescript
* // Type into input (clears existing text by default)
* await app.type('#username', 'testuser');
*
* // Type and submit with Enter
* await app.type('#search', 'search query', { sendEnter: true });
*
* // Type without clearing existing text
* await app.type('#notes', 'additional text', { clear: false });
*
* // Fill form fields
* await app.type('#email', 'user@example.com');
* await app.type('#password', 'secret123');
* await app.click('#login-button');
* ```
*/
async type(element, text, { clear = true, sendEnter = false } = {}) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element);
}
if (clear) {
await element.clear();
}
await element.sendKeys(text);
if (sendEnter) {
await this.sendKey(import_selenium_webdriver3.Key.ENTER);
}
}
/**
* Sends a single keyboard key press.
*
* Simulates pressing a keyboard key. Use Selenium's Key constants for special keys.
*
* @param key - Key to press (use Key.ENTER, Key.TAB, Key.ESCAPE, etc.)
* @returns Promise that resolves when key press completes
*
* @example
* ```typescript
* import { Key } from '@progress/kendo-e2e';
*
* // Press Enter
* await app.sendKey(Key.ENTER);
*
* // Press Tab to move focus
* await app.sendKey(Key.TAB);
*
* // Press Escape to close modal
* await app.sendKey(Key.ESCAPE);
*
* // Press Arrow Down
* await app.sendKey(Key.ARROW_DOWN);
* ```
*/
async sendKey(key) {
await this.driver.actions({ async: true, bridge: true }).sendKeys(key).perform();
}
/**
* Sends a two-key combination (e.g., Ctrl+C).
*
* Convenience method for common two-key combinations. For more keys, use {@link sendKeysCombination}.
*
* @param key1 - First key (typically a modifier like Key.CONTROL)
* @param key2 - Second key
* @returns Promise that resolves when key combination completes
*
* @example
* ```typescript
* import { Key } from '@progress/kendo-e2e';
*
* // Copy text
* await app.sendKeyCombination(Key.CONTROL, 'c');
*
* // Paste text
* await app.sendKeyCombination(Key.CONTROL, 'v');
*
* // Open browser dev tools (F12 might not work in some contexts)
* await app.sendKeyCombination(Key.CONTROL, Key.SHIFT);
* ```
*/
async sendKeyCombination(key1, key2) {
await this.sendKeysCombination([key1, key2]);
}
/**
* Sends a combination of multiple keyboard keys simultaneously.
*
* Holds down all keys in order, then releases them in reverse order, simulating
* a realistic key combination press.
*
* @param keys - Array of keys to press together
* @returns Promise that resolves when key combination completes
*
* @example
* ```typescript
* import { Key } from '@progress/kendo-e2e';
*
* // Ctrl+Shift+Delete
* await app.sendKeysCombination([Key.CONTROL, Key.SHIFT, Key.DELETE]);
*
* // Select all (Ctrl+A)
* await app.sendKeysCombination([Key.CONTROL, 'a']);
*
* // Custom three-key combo
* await app.sendKeysCombination([Key.ALT, Key.SHIFT, 'F']);
* ```
*/
async sendKeysCombination(keys) {
const actions = this.driver.actions({ async: false, bridge: true });
for (const key of keys) {
actions.keyDown(key).pause(10);
}
for (const key of [...keys].reverse()) {
actions.keyUp(key).pause(10);
}
await actions.perform();
}
/**
* Sends Ctrl+key on Windows/Linux or Cmd+key on macOS automatically.
*
* Cross-platform helper that uses the appropriate modifier key for the current OS.
* Perfect for common shortcuts like copy, paste, save, etc.
*
* @param key - Key to combine with Ctrl/Cmd
* @returns Promise that resolves when key combination completes
*
* @example
* ```typescript
* // Copy (Ctrl+C on Windows/Linux, Cmd+C on macOS)
* await app.sendControlKeyCombination('c');
*
* // Paste (cross-platform)
* await app.sendControlKeyCombination('v');
*
* // Select all (cross-platform)
* await app.sendControlKeyCombination('a');
*
* // Save (cross-platform)
* await app.sendControlKeyCombination('s');
* ```
*/
async sendControlKeyCombination(key) {
const control = process.platform === "darwin" ? import_selenium_webdriver3.Key.COMMAND : import_selenium_webdriver3.Key.CONTROL;
await this.sendKeysCombination([control, key]);
}
async isVisible(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
return await this.waitSafely(EC.isVisible(element), { timeout, pollTimeout });
}
async isNotVisible(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
return await this.waitSafely(EC.notVisible(element), { timeout, pollTimeout });
}
async isInViewport(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
return await this.waitSafely(EC.isInViewport(element), { timeout, pollTimeout });
}
async isNotInViewport(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
return await this.waitSafely(EC.notInViewport(element), { timeout, pollTimeout });
}
async hasFocus(element) {
return element instanceof import_selenium_webdriver3.WebElement ? await this.waitSafely(EC.hasFocus(element)) : await this.waitSafely(EC.hasFocus(await this.find(element)));
}
async hasNoFocus(element) {
return element instanceof import_selenium_webdriver3.WebElement ? await this.waitSafely(EC.hasNoFocus(element)) : await this.waitSafely(EC.hasNoFocus(await this.find(element)));
}
async hasText(element, text) {
return element instanceof import_selenium_webdriver3.WebElement ? await this.waitSafely(EC.hasText(element, text)) : await this.waitSafely(EC.hasText(await this.find(element), text));
}
async hasValue(element, value) {
return element instanceof import_selenium_webdriver3.WebElement ? await this.waitSafely(EC.hasValue(element, value)) : await this.waitSafely(EC.hasValue(await this.find(element), value));
}
async hasAttribute(element, attribute, value, exactMatch = true) {
return element instanceof import_selenium_webdriver3.WebElement ? await this.waitSafely(EC.hasAttribute(element, attribute, value, exactMatch)) : await this.waitSafely(EC.hasAttribute(await this.find(element), attribute, value, exactMatch));
}
async hasClass(element, value, exactMatch = false) {
return element instanceof import_selenium_webdriver3.WebElement ? await this.waitSafely(EC.hasClass(element, value, exactMatch)) : await this.waitSafely(EC.hasClass(await this.find(element), value, exactMatch));
}
async sleep(milliseconds) {
await this.driver.sleep(milliseconds);
}
/**
* Waits for a condition to become true.
*
* Core waiting method that polls a condition until it returns true or timeout is reached.
* Use predefined conditions from {@link EC} class or create custom conditions.
*
* @param condition - Condition function or WebElementCondition to wait for
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait in milliseconds (default: 10000)
* @param options.message - Custom error message if condition times out (default: 'Failed to satisfy condition.')
* @param options.pollTimeout - Interval between condition checks in milliseconds (default: 25)
* @returns Promise that resolves when condition is met
* @throws Error with the specified message if condition is not met within timeout
*
* @example
* ```typescript
* // Wait for element to be visible
* await app.wait(EC.isVisible('#modal'));
*
* // Wait with custom timeout and message
* await app.wait(EC.hasText(element, 'Success'), {
* timeout: 15000,
* message: 'Success message did not appear'
* });
*
* // Wait for custom condition
* await app.wait(async () => {
* const count = await app.findAll('.item');
* return count.length > 5;
* }, { message: 'Less than 5 items found' });
* ```
*/
async wait(condition, { timeout = 1e4, message = "Failed to satisfy condition.", pollTimeout = 25 } = {}) {
await this.driver.wait(condition, timeout, message, pollTimeout);
}
/**
* Waits for a condition without throwing an error if it fails.
*
* Returns true if condition is met, false if timeout is reached. Perfect for conditional
* logic in tests where you want to check if something happened without failing the test.
*
* @param condition - Condition function or WebElementCondition to wait for
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between condition checks in milliseconds (default: 25)
* @returns Promise resolving to true if condition met, false if timeout reached
*
* @example
* ```typescript
* // Check if element appears (don't fail if it doesn't)
* const appeared = await app.waitSafely(EC.isVisible('.optional-message'));
* if (appeared) {
* console.log('Message was shown');
* }
*
* // Conditional test flow
* const hasModal = await app.waitSafely(EC.isVisible('.modal'), { timeout: 3000 });
* if (hasModal) {
* await app.click('.modal .close');
* }
*
* // Check if element has specific text
* const hasText = await app.waitSafely(EC.hasText('#status', 'Complete'));
* ```
*/
async waitSafely(condition, { timeout = 1e4, pollTimeout = 25 } = {}) {
try {
await this.driver.wait(condition, timeout, null, pollTimeout);
return true;
} catch {
return false;
}
}
/**
* Waits for an element to stop moving or resizing (animation to complete).
*
* Monitors element position and size, waiting until they remain stable for a poll interval.
* Essential for reliable interaction with animated elements.
*
* @param element - Element to monitor (WebElement, By locator, or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between stability checks in milliseconds (default: 50)
* @returns Promise that resolves when element is stable
* @throws Error if element doesn't stabilize within timeout
*
* @example
* ```typescript
* // Wait for sliding panel to stop
* await app.waitForAnimation('.slide-panel');
* await app.click('.slide-panel button');
*
* // Wait for expanding accordion
* await app.click('.accordion-header');
* await app.waitForAnimation('.accordion-content');
*
* // Use before taking screenshots of animated content
* await app.waitForAnimation('.chart');
* const screenshot = await app.getScreenshot();
* ```
*/
async waitForAnimation(element, { timeout = 1e4, pollTimeout = 50 } = {}) {
const locatorStringValue = element instanceof import_selenium_webdriver3.WebElement ? "element" : element;
await this.wait(EC.isVisible(element), { timeout, message: `Failed to find ${locatorStringValue}` });
const isElementStable = async () => {
const rect = element instanceof import_selenium_webdriver3.WebElement ? await element.getRect() : await (await this.find(element)).getRect();
await this.sleep(pollTimeout);
const newRect = element instanceof import_selenium_webdriver3.WebElement ? await element.getRect() : await (await this.find(element)).getRect();
return rect.x === newRect.x && rect.y === newRect.y && rect.width === newRect.width && rect.height === newRect.height;
};
await this.wait(isElementStable, { timeout, message: `Element ${locatorStringValue} is still moving or resizing.` });
}
async getScreenshot() {
return await this.driver.takeScreenshot();
}
/**
* Gets the visible text content of an element.
*
* Returns the text that would be visible to a user, excluding hidden elements.
* Automatically finds the element if a locator is provided.
*
* @param element - Element to get text from (WebElement, By locator, or CSS selector)
* @returns Promise resolving to the element's text, or undefined if element has no text
*
* @example
* ```typescript
* // Get button text
* const buttonText = await app.getText('#submit-btn');
* console.log(buttonText); // 'Submit Form'
*
* // Get paragraph content
* const message = await app.getText('.success-message');
*
* // Get text from found element
* const element = await app.find('.label');
* const text = await app.getText(element);
* ```
*/
async getText(element) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element);
}
try {
return await element.getText();
} catch {
return void 0;
}
}
/**
* Gets the value of an HTML attribute from an element.
*
* Retrieves attribute values like 'href', 'src', 'disabled', 'data-*', etc.
* Returns null if the attribute doesn't exist.
*
* @param element - Element to get attribute from (WebElement, By locator, or CSS selector)
* @param attribute - Name of the attribute to retrieve
* @returns Promise resolving to the attribute value or null
*
* @example
* ```typescript
* // Get link href
* const url = await app.getAttribute('a.download', 'href');
*
* // Check if button is disabled
* const isDisabled = await app.getAttribute('#submit', 'disabled');
*
* // Get data attribute
* const userId = await app.getAttribute('.user', 'data-user-id');
*
* // Get input value
* const value = await app.getAttribute('#email', 'value');
* ```
*/
async getAttribute(element, attribute) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element);
}
return await element.getAttribute(attribute);
}
/**
* Gets a JavaScript property value from an element.
*
* Different from {@link getAttribute} - this gets DOM properties (like 'value', 'checked')
* which may differ from HTML attributes. Properties reflect the current state.
*
* @param element - Element to get property from (WebElement, By locator, or CSS selector)
* @param property - Name of the property to retrieve
* @returns Promise resolving to the property value
*
* @example
* ```typescript
* // Get checkbox checked state (property, not attribute)
* const isChecked = await app.getProperty('#agree', 'checked');
*
* // Get input value (current value, not initial)
* const currentValue = await app.getProperty('#username', 'value');
*
* // Get element's innerHTML
* const html = await app.getProperty('.container', 'innerHTML');
*
* // Get computed style property
* const display = await app.getProperty('#element', 'style');
* ```
*/
async getProperty(element, property) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element);
}
const script = function(element2, property2) {
return element2[property2];
};
return await this.driver.executeScript(script, element, property);
}
/**
* Gets the text color (color CSS property) of an element as hex value.
*
* Converts the color from any format (rgb, rgba, named) to hex format (#RRGGBB).
* Useful for visual testing and theme verification.
*
* @param element - Element to get color from (WebElement, By locator, or CSS selector)
* @returns Promise resolving to hex color string (e.g., '#ff0000')
*
* @example
* ```typescript
* // Check error message color
* const errorColor = await app.getColor('.error-message');
* expect(errorColor).toBe('#ff0000'); // red
*
* // Verify link color
* const linkColor = await app.getColor('a.primary');
*
* // Check themed text color
* const textColor = await app.getColor('.themed-text');
* ```
*/
async getColor(element) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element);
}
const color = await element.getCssValue("color");
return rgbToHex(color);
}
/**
* Gets the background color (background-color CSS property) of an element as hex value.
*
* Converts the background color from any format to hex format (#RRGGBB).
* Useful for verifying button states, highlights, and theme colors.
*
* @param element - Element to get background color from (WebElement, By locator, or CSS selector)
* @returns Promise resolving to hex color string (e.g., '#ffffff')
*
* @example
* ```typescript
* // Check button background
* const btnBg = await app.getBackgroundColor('#primary-btn');
* expect(btnBg).toBe('#007bff'); // bootstrap primary
*
* // Verify selected item highlight
* const selectedBg = await app.getBackgroundColor('.selected');
*
* // Check alert background color
* const alertBg = await app.getBackgroundColor('.alert-warning');
* ```
*/
async getBackgroundColor(element) {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element);
}
const color = await element.getCssValue("background-color");
return rgbToHex(color);
}
/**
* Hides the text cursor/caret for cleaner screenshots.
*
* When called without arguments, hides the cursor globally for all input/textarea elements.
* When called with an element, hides the cursor only for that specific element.
* Global hiding persists until page reload.
*
* @param element - Optional specific element to hide cursor for (WebElement, By locator, or CSS selector)
* @param options - Optional configuration
* @param options.timeout - Maximum time to wait for element in milliseconds (default: 10000)
* @param options.pollTimeout - Interval between retry attempts in milliseconds (default: 25)
* @returns Promise that resolves when cursor is hidden
*
* @example
* ```typescript
* // Hide cursor globally before screenshot
* await app.hideCursor();
* const screenshot = await app.getScreenshot();
*
* // Hide cursor for specific input
* await app.focus('#username');
* await app.hideCursor('#username');
* const inputScreenshot = await app.getScreenshot();
*
* // Hide cursor in focused field for visual test
* await app.type('#search', 'test');
* await app.hideCursor('#search');
* ```
*
* @see For more details, see [hideCursor documentation](../../docs/hideCursor.md)
*/
async hideCursor(element, { timeout = 1e4, pollTimeout = 25 } = {}) {
if (!element) {
await this.driver.executeScript(() => {
const existing = document.head.querySelector('style[data-hide-caret="true"]');
if (existing) {
return;
}
const style = document.createElement("style");
style.setAttribute("data-hide-caret", "true");
style.textContent = "input, textarea { caret-color: transparent !important; }";
document.head.appendChild(style);
});
} else {
if (!(element instanceof import_selenium_webdriver3.WebElement)) {
element = await this.find(element, { timeout, pollTimeout });
}
await this.driver.executeScript((el) => {
if (el && el instanceof HTMLElement) {
el.style.caretColor = "transparent";
}
}, element);
}
}
/**
* Creates an expectation API for the specified element selector.
* This provides a fluent interface for asserting element states with automatic retry logic.
* The expect API will continuously retry finding the element and checking the condition
* until it passes or the timeout is reached (default: 3000ms).
*
* @param selector - CSS selector string or By locator to identify the element
* @returns ExpectApi object with assertion methods
*
* @example
* ```typescript
* // Assert element has specific text (default 3s timeout)
* await app.expect('#result').toHaveText('Welcome user');
*
* // Assert element is visible
* await app.expect('.modal').toBeVisible();
*
* // Assert element is not visible
* await app.expect('.spinner').not.toBeVisible();
*
* // Assert with custom timeout and message
* await app.expect('#message').toHaveText('Success', {
* timeout: 5000,
* message: 'Failed to load results.'
* });
*
* // Assert using regex pattern
* await app.expect('#status').toHaveText(/completed|success/i, { timeout: 10000 });
*
* // Assert element has value
* await app.expect('#input').toHaveValue('expected value', { timeout: 2000 });
*
* // Assert element has focus
* await app.expect('#activeInput').toHaveFocus();
*
* // Assert element has attribute
* await app.expect('#btn').toHaveAttribute('disabled', 'true');
*
* // Assert element has class (partial match)
* await app.expect('#div').toHaveClass('active', { exactMatch: false });
*
* // Assert element is enabled/disabled
* await app.expect('#submit').toBeEnabled();
* await app.expect('#submit').toBeDisabled();
*
* // Assert checkbox/radio is checked
* await app.expect('#agree').toBeChecked();
* await app.expect('#opt-out').not.toBeChecked();
*
* // Assert partial text match
* await app.expect('#message').toContainText('success');
*
* // Assert element count
* await app.expect('.list-item').toHaveCount(5);
* ```
*/
expect(selector) {
const by = typeof selector === "string" ? import_selenium_webdriver3.By.css(selector) : selector;
return expectSelector(this.driver, by);
}
};
}
});
// src/selenium/browser.ts
var browser_exports = {};
__export(browser_exports, {
Browser: () => Browser2,
By: () => import_selenium_webdriver5.By,
Key: () => import_selenium_webdriver5.Key,
ThenableWebDriver: () => import_selenium_webdriver5.ThenableWebDriver,
WebElement: () => import_selenium_webdriver5.WebElement,
WebElementCondition: () => import_selenium_webdriver5.WebElementCondition,
until: () => import_selenium_webdriver5.until
});
function isDriver(obj) {
return obj && typeof obj.getSession === "function";
}
var import_webdriverjs, import_selenium_webdriver4, import_logging, import_selenium_webdriver5, Browser2;
var init_browser = __esm({
"src/selenium/browser.ts"() {
import_webdriverjs = __toESM(require("@axe-core/webdriverjs"));
import_selenium_webdriver4 = require("selenium-webdriver");
import_logging = require("selenium-webdriver/lib/logging");
init_driver_manager();
init_web_app();
import_selenium_webdriver5 = require("selenium-webdriver");
Browser2 = class extends WebApp {
/**
* Creates an instance of the Browser class.
*
* @param {ThenableWebDriver | BrowserOptions} [driverOrOptions] - Either a WebDriver instance or an options object.
* If a WebDriver instance is provided, it will be used as the driver. If an options object is provided, a new driver will be created with those options.
* @param {Object} [mobileEmulation] - (Optional) Mobile emulation options, used only if the first parameter is a WebDriver instance.
* Mobile options can be an object with either a `deviceName` or `width`, `height`, and `pixelRatio`.
* @param {boolean} [enableBidi] - (Optional) Enables BiDi (Bidirectional communication) if set to `true`.
*
* __Usage Examples:__
*
* __Example 1: Using a Pre-configured Device__
* ```typescript
* const mobileEmulation = { deviceName: "iPhone 14 Pro Max" };
* const browser = new Browser(mobileEmulation);
* ```
*
* __Example 2: Using Custom Screen Configuration__
* ```typescript
* const mobileEmulation = { deviceMetrics: { width: 360, height: 640, pixelRatio: 3.0 }, userAgent: 'My Agent' };
* const browser = new Browser(mobileEmulation);
* ```
*
* __Example 3: Providing a WebDriver Instance With Mobile Emulation__
* ```typescript
* const driver = new Builder().forBrowser('chrome').build();
* const mobileEmulation = { deviceName: "iPhone 14 Pro Max" };
* const browser = new Browser(driver, mobileEmulation);
* ```
*
* __Example 4: Enabling BiDi Mode__
* ```typescript
* const browser = new Browser({ enableBidi: true });
* ```
*
* __Example 5: Combining Mobile Emulation and BiDi Mode__
* ```typescript
* const browser = new Browser({ mobileEmulation: { deviceName: "iPhone 14 Pro Max" }, enableBidi: true });
* ```
*
* __Example 6: Passing Custom Chrome Arguments__
* ```typescript
* const browser = new Browser({
* chromeArguments: ['--allow-file-access-from-files', '--disable-web-security']
* });
* ```
*
* [em]: https://chromedriver.chromium.org/mobile-emulation
* [devem]: https://developer.chrome.com/devtools/docs/device-mode
*/
constructor(driverOrOptions, mobileEmulation, enableBidi) {
let driver;
if (driverOrOptions && isDriver(driverOrOptions)) {
driver = driverOrOptions;
} else {
const options = driverOrOptions || {};
if (mobileEmulation && !options.mobileEmulation) {
options.mobileEmulation = mobileEmulation;
}
if (enableBidi && options.enableBidi === void 0) {
options.enableBidi = enableBidi;
}
driver = options.driver ?? new DriverManager().getDriver({
mobileEmulation: options.mobileEmulation,
enableBidi: options.enableBidi,
chromeArguments: options.chromeArguments
});
}
super(driver);
}
/**
* Closes the browser and ends the WebDriver session.
*
* Should be called at the end of each test to clean up resources.
* Closes all browser windows and terminates the driver.
*
* @returns Promise that resolves when browser is closed
*
* @example
* ```typescript
* const browser = new Browser();
* try {
* await browser.navigateTo('https://example.com');
* // ... test code ...
* } finally {
* await browser.close(); // Always close to free resources
* }
* ```
*/
async close() {
await this.driver.quit();
}
/**
* Navigates the browser to a specified URL.
*
* Opens the URL in the current browser window. Waits for the page to load before
* the promise resolves.
*
* @param url - The URL to navigate to (must include protocol: http:// or https://)
* @returns Promise that resolves when navigation completes
*
* @example
* ```typescript
* // Navigate to a website
* await browser.navigateTo('https://example.com');
*
* // Navigate to local development server
* await browser.navigateTo('http://localhost:3000');
*
* // Navigate to specific page
* await browser.navigateTo('https://example.com/products/123');
* ```
*/
async navigateTo(url) {
await this.driver.navigate().to(url);
}
async getRect() {
return await this.driver.manage().window().getRect();
}
async setRect(rect) {
const currentRect = await this.driver.manage().window().getRect();
this.driver.manage().window().setRect({
width: rect.width ?? currentRect.width,
height: rect.height ?? currentRect.height,
x: rect.x ?? currentRect.x,
y: rect.y ?? currentRect.y
});
}
async resizeToDocumentScrollHeight() {
const originalRect = await this.getRect();
const viewportHeight = await this.driver.executeScript("return window.innerHeight");
const documentHeight = await this.driver.executeScript("return document.body.scrollHeight");
await this.setRect({ height: documentHeight + originalRect.height - viewportHeight });
}
/**
* Resizes the browser window to the specified width and height.
*
* @param width - The new width of the window in pixels
* @param height - The new height of the window in pixels
* @throws Error if the window resize operation fails
*
* @example
* ```typescript
* // Resize window to 1920x1080
* await browser.resizeWindow(1920, 1080);
*
* // Resize to mobile size
* await browser.resizeWindow(375, 667);
* ```
*/
async resizeWindow(width, height) {
try {
await this.setRect({ width, height });
} catch (error) {
throw new Error(`Failed to resize window to ${width}x${height}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Refreshes the current page (like pressing F5 or clicking browser refresh).
*
* Reloads the page from the server, resetting all JavaScript state.
*
* @returns Promise that resolves when page reload completes
*
* @example
* ```typescript
* // Refresh after making changes
* await browser.click('#update-settings');
* await browser.refresh();
*
* // Verify data persists after refresh
* await browser.type('#input', 'test');
* await browser.click('#save');
* await browser.refresh();
* const value = await browser.getAttribute('#input', 'value');
* expect(value).toBe('test');
* ```
*/
async refresh() {
await this.driver.navigate().refresh();
}
/**
* Switches the WebDriver context to an iframe.
*
* After calling this, all subsequent commands will target elements within the iframe.
* To switch back to the main page, use `driver.switchTo().defaultContent()`.
*
* @param elementLocator - By locator for the iframe element
* @returns Promise that resolves when context is switched
*
* @example
* ```typescript
* // Switch to iframe and interact with its content
* await browser.switchToIFrame(By.css('#my-iframe'));
* await browser.click('#button-inside-iframe');
*
* // Switch back to main page
* await browser.driver.switchTo().defaultContent();
* await browser.click('#button-on-main-page');
*
* // Work with nested iframes
* await browser.switchToIFrame(By.css('#outer-frame'));
* await browser.switchToIFrame(By.css('#inner-frame'));
* ```
*/
async switchToIFrame(elementLocator) {
const iframe = await this.find(elementLocator);
await this.driver.switchTo().frame(iframe);
}
/**
* Gets the current URL of the browser.
*
* Returns the complete URL including protocol, domain, path, and query parameters.
*
* @returns Promise resolving to the current URL string
*
* @example
* ```typescript
* // Verify navigation occurred
* await browser.click('#products-link');
* const url = await browser.getCurrentUrl();
* expect(url).toContain('/products');
*
* // Check URL parameters
* const currentUrl = await browser.getCurrentUrl();
* expect(currentUrl).toContain('?filter=active');
*
* // Verify redirect
* await browser.navigateTo('http://example.com/old-page');
* const redirectedUrl = await browser.getCurrentUrl();
* expect(redirectedUrl).toBe('http://example.com/new-page');
* ```
*/
async getCurrentUrl() {
return await this.driver.getCurrentUrl();
}
/**
* Gets the name of the current browser.
*
* Returns lowercase browser name: 'chrome', 'firefox', 'safari', 'edge', etc.
* Useful for browser-specific test logic.
*
* @returns Promise resolving to lowercase browser name
*
* @example
* ```typescript
* const browserName = await browser.getBrowserName();
*
* if (browserName === 'safari') {
* // Skip Safari-incompatible test
* console.log('Skipping on Safari');
* return;
* }
*
* // Browser-specific assertions
* if (browserName === 'firefox') {
* // Firefox-specific validation
* }
* ```
*/
async getBrowserName() {
const capabilities = await (await this.driver).getCapabilities();
const browserName = capabilities.getBrowserName().toLowerCase();
return browserName;
}
/**
* Runs accessibility (a11y) tests using axe-core and returns violations.
*
* Scans the page for accessibility issues like missing alt text, insufficient color contrast,
* missing ARIA labels, etc. Returns an array of violations that should be addressed.
*
* @param cssSelector - CSS selector to limit scanning scope (default: 'html' for full page)
* @param disableRules - Array of axe rule IDs to disable (default: ['color-contrast'])
* @returns Promise resolving to array of accessibility violations
*
* @example
* ```typescript
* // Scan entire page
* const violations = await browser.getAccessibilityViolations();
* expect(violations).toHaveLength(0);
*
* // Scan specific component
* const formViolations = await browser.getAccessibilityViolations('#login-form');
*
* // Enable all rules including color contrast
* const allViolations = await browser.getAccessibilityViolations('html', []);
*
* // Disable specific rules
* const violations = await browser.getAccessibilityViolations('html', [
* 'color-contrast',
* 'landmark-one-main'
* ]);
* ```
*/
async getAccessibilityViolations(cssSelector = "html", disableRules = ["color-contrast"]) {
await this.find(import_selenium_webdriver4.By.css(cssSelector));
const axe = new import_webdriverjs.default(this.driver).include(cssSelector).disableRules(disableRules);
const result = await axe.analyze();
return result.violations;
}
/**
* Clears the browser console logs.
*
* Call this before performing actions to get a clean slate for error detection.
* Useful when you want to check if a specific action causes console errors.
*
* @returns Promise that resolves when logs are cleared
*
* @example
* ```typescript
* // Clear logs before action
* await browser.clearLogs();
* await browser.click('#potential-error-button');
* const errors = await browser.getErrorLogs();
* ```
*/
async clearLogs() {
await this.driver.manage().logs().get(import_logging.Type.BROWSER);
}
/**
* Gets console errors from the browser (Chrome only).
*
* Retrieves console errors logged by the browser. Only works in Chrome on desktop platforms.
* Firefox and mobile platforms don't support log retrieval.
*
* @param excludeList - Array of strings to filter out from errors (default: ['favicon.ico'])
* @param logLevel - Minimum log level to collect (default: Level.SEVERE for errors)
* @returns Promise resolving to array of error message strings
*
* @example
* ```typescript
* // Check for any console errors
* const errors = await browser.getErrorLogs();
* expect(errors.length).toBe(0);
*
* // Exclude known non-critical errors
* const errors = await browser.getErrorLogs(['favicon', 'analytics']);
*
* // Get all warnings and errors
* const logs = await browser.getErrorLogs([], Level.WARNING);
*
* // Check for specific error
* const errors = await browser.getErrorLogs();
* expect(errors.some(e => e.includes('TypeError'))).toBe(false);
* ```
*/
async getErrorLogs(excludeList = ["favicon.ico"], logLevel = import_logging.Level.SEVERE) {
const errors = [];
const capabilities = await (await this.driver).getCapabilities();
const platform = capabilities.getPlatform().toLowerCase();
const browserName = capabilities.getBrowserName().toLowerCase();
if (browserName === "chrome" && platform !== "android" && platform !== "iphone") {
const logs = await this.driver.manage().logs().get(import_logging.Type.BROWSER);
for (const entry of logs) {
if (entry.level.value >= logLevel.value) {
errors.push(entry.message);
}
}
}
let filteredErrors = errors;
for (const excludeItem of excludeList) {
filteredErrors = filteredErrors.filter((error) => {
return error.toLowerCase().indexOf(excludeItem.toLowerCase()) < 0;
});
}
return filteredErrors;
}
/**
* Executes a JavaScript script in the browser context.
*
* @param script - The JavaScript code to execute as a string
* @param waitBeforeMs - Optional wait time in milliseconds before executing the script (default: 0)
* @param waitAfterMs - Optional wait time in milliseconds after executing the script (default: 0)
* @returns Promise<unknown> - The result of the script execution
* @throws Error if the script execution fails
*
* @example
* ```typescript
* // Basic script execution
* const result = await browser.executeScript('return document.title;');
*
* // With wait before execution
* await browser.executeScript('document.body.style.background = "red";', 1000);
*
* // With waits before and after execution
* const height = await browser.executeScript('return document.body.scrollHeight;', 500, 200);
* ```
*/
async executeScript(script, waitBeforeMs = 0, waitAfterMs = 0) {
try {
if (waitBeforeMs > 0) {
await new Promise((resolve2) => setTimeout(resolve2, waitBeforeMs));
}
const result = await this.driver.executeScript(script);
if (waitAfterMs > 0) {
await new Promise((resolve2) => setTimeout(resolve2, waitAfterMs));
}
return result;
} catch (error) {
throw new Error(`Failed to execute JavaScript script: ${error instanceof Error ? error.message : String(error)}`);
}
}
};
}
});
// src/cli/daemon.ts
var daemon_exports = {};
__export(daemon_exports, {
DAEMON_FILE: () => DAEMON_FILE,
startServer: () => startServer
});
module.exports = __toCommonJS(daemon_exports);
var http = __toESM(require("http"));
var fs = __toESM(require("fs"));
var path = __toESM(require("path"));
var import_selenium_webdriver6 = require("selenium-webdriver");
var OUTPUT_DIR = ".kendo-e2e";
var DAEMON_FILE = path.join(OUTPUT_DIR, "daemon.json");
var sessions = /* @__PURE__ */ new Map();
function getSession(name) {
const s = sessions.get(name);
if (s) s.lastAccess = Date.now();
return s?.browser;
}
function setSession(name, browser) {
sessions.set(name, { browser, lastAccess: Date.now() });
}
function deleteSession(name) {
sessions.delete(name);
}
var SESSION_IDLE_MS = 5 * 60 * 1e3;
var cleanupTimer = setInterval(async () => {
const now = Date.now();
for (const [name, s] of sessions) {
if (now - s.lastAccess > SESSION_IDLE_MS) {
await s.browser.close().catch(() => {
});
sessions.delete(name);
}
}
}, 6e4);
cleanupTimer.unref();
var BrowserClass;
function getBrowserClass() {
if (!BrowserClass) {
BrowserClass = (init_browser(), __toCommonJS(browser_exports)).Browser;
}
return BrowserClass;
}
function ensureOutputDir() {
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
}
function writeOutput(filename, content) {
ensureOutputDir();
const filePath = path.join(OUTPUT_DIR, filename);
const resolved = path.resolve(filePath);
const outputDirResolved = path.resolve(OUTPUT_DIR);
if (!resolved.startsWith(outputDirResolved + path.sep) && resolved !== outputDirResolved) {
throw new Error("Invalid filename: path traversal not allowed");
}
fs.writeFileSync(resolved, content);
return filePath;
}
function writeOutputBinary(filename, content) {
ensureOutputDir();
const filePath = path.join(OUTPUT_DIR, filename);
const resolved = path.resolve(filePath);
const outputDirResolved = path.resolve(OUTPUT_DIR);
if (!resolved.startsWith(outputDirResolved + path.sep) && resolved !== outputDirResolved) {
throw new Error("Invalid filename: path traversal not allowed");
}
fs.writeFileSync(resolved, content);
return filePath;
}
function timestampedName(prefix, ext) {
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
return `${prefix}-${ts}.${ext}`;
}
var domSnapshotScript = `
const opts = arguments[0] || {};
const rootSelector = opts.rootSelector;
const filteredTags = new Set(["script", "style", "link", "meta", "noscript"]);
const norm = (s) => (s ?? "").replace(/\\s+/g, " ").trim();
function snapElement(el) {
const tag = el.tagName ? el.tagName.toLowerCase() : "";
if (filteredTags.has(tag)) return null;
const id = el.id || undefined;
const classes = el.classList ? Array.from(el.classList) : [];
const role = el.getAttribute?.("role") || undefined;
const dataRole = el.getAttribute?.("data-role") || undefined;
const type = (tag === "input" || tag === "button") && el.type ? String(el.type) : undefined;
const aria = {};
if (el.hasAttributes?.()) {
for (const attr of Array.from(el.attributes)) {
if (attr.name?.startsWith("aria-")) aria[attr.name] = attr.value;
}
}
let hidden = false;
let frame;
try {
const style = window.getComputedStyle?.(el);
if (style) {
hidden = style.display === "none" || style.visibility === "hidden" ||
parseFloat(style.opacity || "1") === 0 || el.offsetParent === null;
}
if (!hidden) {
const rect = el.getBoundingClientRect();
if (rect && (rect.width > 0 || rect.height > 0)) {
frame = [
Math.round(rect.left + window.scrollX),
Math.round(rect.top + window.scrollY),
Math.round(rect.width),
Math.round(rect.height)
];
}
}
} catch (e) {}
const content = [];
if (el.childNodes) {
for (const child of Array.from(el.childNodes)) {
if (child.nodeType === 3) {
const text = norm(child.textContent);
if (text) content.push(text);
} else if (child.nodeType === 1) {
const childEl = snapElement(child);
if (childEl) content.push(childEl);
}
}
}
return {
element: tag,
...(id ? { id } : {}),
...(classes.length ? { classes } : {}),
...(role ? { role } : {}),
...(dataRole ? { dataRole } : {}),
...(type ? { type } : {}),
...(Object.keys(aria).length ? { aria } : {}),
...(frame ? { frame } : {}),
...(hidden ? { hidden } : {}),
...(content.length ? { content } : {})
};
}
const root = rootSelector ? document.querySelector(rootSelector) : document.body;
if (!root) return { error: "root-not-found" };
return snapElement(root);
`;
function resolvedOutputPath(filename, prefix, ext) {
const name = filename ? filename.includes(".") ? filename : `${filename}.${ext}` : timestampedName(prefix, ext);
return path.join(OUTPUT_DIR, name);
}
function treeToYaml(node, indent = 0, maxDepth) {
if (!node) return "";
if (typeof node === "string") return `${" ".repeat(indent)}- text: "${node}"`;
const spaces = " ".repeat(indent);
const tag = node.element || "unknown";
const attrs = [];
if (node.id) attrs.push(`id="${node.id}"`);
if (node.classes?.length) attrs.push(`class="${node.classes.join(" ")}"`);
if (node.role) attrs.push(`role="${node.role}"`);
if (node.dataRole) attrs.push(`data-role="${node.dataRole}"`);
if (node.type) attrs.push(`type="${node.type}"`);
if (node.aria) {
for (const [key, value] of Object.entries(node.aria)) {
attrs.push(`${key}="${value}"`);
}
}
if (node.hidden) attrs.push("hidden");
const attrStr = attrs.length > 0 ? " " + attrs.join(" ") : "";
let line = `${spaces}- ${tag}${attrStr}`;
if (node.frame) {
line += ` [${node.frame.join(",")}]`;
}
if (!node.content?.length) return line;
if (maxDepth !== void 0 && indent >= maxDepth) {
return line + " ...";
}
const textOnly = node.content.every((c) => typeof c === "string");
if (textOnly) {
const text = node.content.join(" ");
if (text.length < 80) return `${line}: ${text}`;
}
let result = line + "\n";
for (const item of node.content) {
result += treeToYaml(item, indent + 1, maxDepth) + "\n";
}
return result.trimEnd();
}
async function handleNavigate(session, args) {
const { url, device } = args;
try {
new URL(url);
} catch {
return { ok: false, error: `Invalid URL: ${url}` };
}
let browser = getSession(session);
try {
if (!browser) {
const Browser3 = getBrowserClass();
const opts = device ? { mobileEmulation: { deviceName: device } } : void 0;
browser = new Browser3(opts);
setSession(session, browser);
}
await browser.navigateTo(url);
return { ok: true, message: `Navigated session "${session}" to ${url}` };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handlePageInfo(session) {
const browser = getSession(session);
if (!browser) return { ok: false, error: `No active session "${session}". Run "kendo-e2e open <url>" first.` };
try {
const info = await browser.driver.executeScript(`
return {
title: document.title,
url: window.location.href,
readyState: document.readyState,
viewport: { width: window.innerWidth, height: window.innerHeight },
counts: {
forms: document.forms?.length || 0,
links: document.links?.length || 0,
images: document.images?.length || 0
}
};
`);
return { ok: true, message: `Page: "${info.title}" | ${info.url} | ${info.readyState} | ${info.viewport.width}x${info.viewport.height}` };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handleSnapshot(session, args) {
const browser = getSession(session);
if (!browser) return { ok: false, error: `No active session "${session}". Run "kendo-e2e open <url>" first.` };
try {
const tree = await browser.driver.executeScript(domSnapshotScript, { rootSelector: args.root });
if (tree.error) return { ok: false, error: `Snapshot error: ${tree.error} \u2014 selector "${args.root}" matched nothing` };
const depth = args.depth;
let content;
let ext;
if (args.format === "json") {
content = JSON.stringify(tree, null, 2);
ext = "json";
} else {
content = treeToYaml(tree, 0, depth);
ext = "yaml";
}
const filePath = writeOutput(resolvedOutputPath(args.filename, "snapshot", ext).replace(OUTPUT_DIR + path.sep, ""), content);
return { ok: true, message: `Snapshot saved: ${filePath}`, data: { filePath } };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handleScreenshot(session, args) {
const browser = getSession(session);
if (!browser) return { ok: false, error: `No active session "${session}". Run "kendo-e2e open <url>" first.` };
try {
let base64;
if (args.selector) {
const element = await browser.find(args.selector);
base64 = await element.takeScreenshot();
} else {
base64 = await browser.driver.takeScreenshot();
}
const buffer = Buffer.from(base64, "base64");
const name = args.filename ? args.filename.includes(".") ? args.filename : `${args.filename}.png` : timestampedName("screenshot", "png");
const filePath = writeOutputBinary(name, buffer);
return { ok: true, message: `Screenshot saved: ${filePath}`, data: { filePath } };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handleClick(session, args) {
const browser = getSession(session);
if (!browser) return { ok: false, error: `No active session "${session}". Run "kendo-e2e open <url>" first.` };
try {
await browser.click(args.selector, { timeout: args.timeout || 2e3 });
return { ok: true, message: `Clicked: ${args.selector}` };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handleType(session, args) {
const browser = getSession(session);
if (!browser) return { ok: false, error: `No active session "${session}". Run "kendo-e2e open <url>" first.` };
try {
const timeout = args.timeout || 2e3;
const el = await browser.find(args.selector, { timeout });
await browser.type(el, args.text, { clear: !!args.clear });
return { ok: true, message: `Typed "${args.text}" into: ${args.selector}` };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handleFind(session, args) {
const browser = getSession(session);
if (!browser) return { ok: false, error: `No active session "${session}". Run "kendo-e2e open <url>" first.` };
const properties = (args.props || "text,visible").split(",").map((p) => p.trim());
const attributes = args.attrs ? args.attrs.split(",").map((a) => a.trim()) : [];
const timeout = args.timeout || 2e3;
async function getInfo(el) {
const info = {};
for (const prop of properties) {
switch (prop) {
case "text":
info.text = await el.getText();
break;
case "enabled":
info.enabled = await el.isEnabled();
break;
case "visible":
info.visible = await el.isDisplayed();
break;
case "tag":
info.tag = await el.getTagName();
break;
case "id":
info.id = await el.getAttribute("id");
break;
case "classes":
const cls = await el.getAttribute("class");
info.classes = cls ? cls.split(/\s+/) : [];
break;
}
}
for (const attr of attributes) {
info[attr] = await el.getAttribute(attr);
}
return info;
}
try {
let result;
if (args.all) {
const elements = await browser.findAll(args.selector);
const items = [];
for (const el of elements.slice(0, 20)) {
items.push(await getInfo(el));
}
result = { selector: args.selector, count: elements.length, elements: items };
} else {
const el = await browser.find(args.selector, { timeout });
result = { selector: args.selector, element: await getInfo(el) };
}
const name = args.filename ? args.filename.includes(".") ? args.filename : `${args.filename}.json` : timestampedName("find", "json");
const filePath = writeOutput(name, JSON.stringify(result, null, 2));
return { ok: true, message: `Found ${args.all ? result.count + " elements" : "element"}: ${filePath}`, data: { filePath, result } };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handleEval(session, args) {
const browser = getSession(session);
if (!browser) return { ok: false, error: `No active session "${session}". Run "kendo-e2e open <url>" first.` };
try {
const result = await browser.driver.executeScript(args.script);
const output = JSON.stringify(result, null, 2);
if (output.length > 200) {
const filename = timestampedName("eval", "json");
const filePath = writeOutput(filename, output);
return { ok: true, message: `Result saved: ${filePath}`, data: { filePath } };
} else {
return { ok: true, message: output };
}
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handlePress(session, args) {
const browser = getSession(session);
if (!browser) return { ok: false, error: `No active session "${session}". Run "kendo-e2e open <url>" first.` };
const keyMap = {
enter: import_selenium_webdriver6.Key.ENTER,
return: import_selenium_webdriver6.Key.RETURN,
tab: import_selenium_webdriver6.Key.TAB,
escape: import_selenium_webdriver6.Key.ESCAPE,
esc: import_selenium_webdriver6.Key.ESCAPE,
space: import_selenium_webdriver6.Key.SPACE,
backspace: import_selenium_webdriver6.Key.BACK_SPACE,
delete: import_selenium_webdriver6.Key.DELETE,
arrowup: import_selenium_webdriver6.Key.ARROW_UP,
up: import_selenium_webdriver6.Key.ARROW_UP,
arrowdown: import_selenium_webdriver6.Key.ARROW_DOWN,
down: import_selenium_webdriver6.Key.ARROW_DOWN,
arrowleft: import_selenium_webdriver6.Key.ARROW_LEFT,
left: import_selenium_webdriver6.Key.ARROW_LEFT,
arrowright: import_selenium_webdriver6.Key.ARROW_RIGHT,
right: import_selenium_webdriver6.Key.ARROW_RIGHT,
home: import_selenium_webdriver6.Key.HOME,
end: import_selenium_webdriver6.Key.END,
pageup: import_selenium_webdriver6.Key.PAGE_UP,
pagedown: import_selenium_webdriver6.Key.PAGE_DOWN,
f1: import_selenium_webdriver6.Key.F1,
f2: import_selenium_webdriver6.Key.F2,
f3: import_selenium_webdriver6.Key.F3,
f4: import_selenium_webdriver6.Key.F4,
f5: import_selenium_webdriver6.Key.F5,
f6: import_selenium_webdriver6.Key.F6,
f7: import_selenium_webdriver6.Key.F7,
f8: import_selenium_webdriver6.Key.F8,
f9: import_selenium_webdriver6.Key.F9,
f10: import_selenium_webdriver6.Key.F10,
f11: import_selenium_webdriver6.Key.F11,
f12: import_selenium_webdriver6.Key.F12
};
const resolvedKey = keyMap[args.key.toLowerCase()] ?? args.key;
try {
await browser.sendKey(resolvedKey);
return { ok: true, message: `Pressed: ${args.key}` };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handleReload(session) {
const browser = getSession(session);
if (!browser) return { ok: false, error: `No active session "${session}". Run "kendo-e2e open <url>" first.` };
try {
await browser.driver.navigate().refresh();
return { ok: true, message: "Page reloaded." };
} catch (err) {
return { ok: false, error: err.message || String(err) };
}
}
async function handleClose(session) {
const browser = getSession(session);
if (browser) {
await browser.close().catch(() => {
});
deleteSession(session);
}
return { ok: true, message: `Closed session "${session}".` };
}
async function handleCloseAll() {
for (const [name, s] of sessions) {
await s.browser.close().catch(() => {
});
sessions.delete(name);
}
return { ok: true, message: "Closed all sessions." };
}
function handleList() {
const list = Array.from(sessions.keys()).map((name) => ({
name,
active: true
}));
if (list.length === 0) {
return { ok: true, message: "No active sessions." };
}
const lines = list.map((s) => ` ${s.name}`);
return { ok: true, message: `Active sessions:
${lines.join("\n")}`, data: { sessions: list } };
}
async function handlePing() {
return { ok: true, message: "pong" };
}
async function dispatch(body) {
const { command, session = "default", args = {} } = body;
switch (command) {
case "navigate":
return handleNavigate(session, args);
case "page-info":
return handlePageInfo(session);
case "snapshot":
return handleSnapshot(session, args);
case "screenshot":
return handleScreenshot(session, args);
case "click":
return handleClick(session, args);
case "type":
return handleType(session, args);
case "find":
return handleFind(session, args);
case "eval":
return handleEval(session, args);
case "press":
return handlePress(session, args);
case "reload":
return handleReload(session);
case "close":
return handleClose(session);
case "close-all":
return handleCloseAll();
case "list":
return handleList();
case "ping":
return handlePing();
case "shutdown":
await handleCloseAll();
setTimeout(() => {
cleanup();
process.exit(0);
}, 100);
return { ok: true, message: "Daemon shutting down." };
default:
return { ok: false, error: `Unknown command: ${command}` };
}
}
function cleanup() {
try {
if (fs.existsSync(DAEMON_FILE)) {
fs.unlinkSync(DAEMON_FILE);
}
} catch {
}
}
function startServer() {
ensureOutputDir();
const server = http.createServer((req, res) => {
if (req.method !== "POST") {
res.writeHead(405);
res.end(JSON.stringify({ ok: false, error: "Method not allowed" }));
return;
}
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", async () => {
try {
const parsed = JSON.parse(body);
const result = await dispatch(parsed);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result));
} catch (err) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: err.message || String(err) }));
}
});
});
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
const port = addr.port;
const info = {
pid: process.pid,
port,
startedAt: (/* @__PURE__ */ new Date()).toISOString()
};
fs.writeFileSync(DAEMON_FILE, JSON.stringify(info, null, 2), { mode: 384 });
process.stdout.write(`DAEMON_READY:${port}
`);
});
process.on("SIGINT", () => {
cleanup();
process.exit(0);
});
process.on("SIGTERM", () => {
cleanup();
process.exit(0);
});
process.on("exit", cleanup);
}
if (require.main === module || process.argv.includes("--daemon")) {
startServer();
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
DAEMON_FILE,
startServer
});