UNPKG

@progress/kendo-e2e

Version:

Kendo UI end-to-end test utilities.

1,216 lines (1,209 loc) 129 kB
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) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; 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'); *