UNPKG

@progress/kendo-e2e

Version:

Kendo UI end-to-end test utilities.

3,143 lines 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, 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
});