UNPKG

@progress/kendo-e2e

Version:

Kendo UI end-to-end test utilities.

515 lines 21 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Browser = exports.WebElementCondition = exports.WebElement = exports.until = exports.Key = exports.By = void 0; const webdriverjs_1 = __importDefault(require("@axe-core/webdriverjs")); const selenium_webdriver_1 = require("selenium-webdriver"); const logging_1 = require("selenium-webdriver/lib/logging"); const driver_manager_1 = require("./driver-manager"); const web_app_1 = require("./web-app"); var selenium_webdriver_2 = require("selenium-webdriver"); Object.defineProperty(exports, "By", { enumerable: true, get: function () { return selenium_webdriver_2.By; } }); Object.defineProperty(exports, "Key", { enumerable: true, get: function () { return selenium_webdriver_2.Key; } }); Object.defineProperty(exports, "until", { enumerable: true, get: function () { return selenium_webdriver_2.until; } }); Object.defineProperty(exports, "WebElement", { enumerable: true, get: function () { return selenium_webdriver_2.WebElement; } }); Object.defineProperty(exports, "WebElementCondition", { enumerable: true, get: function () { return selenium_webdriver_2.WebElementCondition; } }); function isDriver(obj) { return obj && typeof obj.getSession === 'function'; } /** * Browser automation class with automatic waiting and modern web testing features. * * Extends {@link WebApp} with browser-specific capabilities like navigation, window management, * accessibility testing, and console log monitoring. Perfect for testing web applications in * real browsers (Chrome, Firefox, Safari, Edge). * * **Key features:** * - Browser navigation and URL management * - Window resizing and iframe handling * - Mobile device emulation * - Accessibility (a11y) testing with axe-core * - Console error detection * - BiDi protocol support * * @example * ```typescript * // Basic browser test * const browser = new Browser(); * await browser.navigateTo('https://example.com'); * await browser.click('#login-button'); * await browser.expect('.welcome-message').toBeVisible(); * await browser.close(); * * // Mobile emulation * const mobile = new Browser({ mobileEmulation: { deviceName: 'iPhone 14 Pro Max' } }); * await mobile.navigateTo('https://example.com'); * * // With BiDi for advanced features * const browser = new Browser({ enableBidi: true }); * * // Check for console errors * await browser.clearLogs(); * await browser.click('#trigger-error'); * const errors = await browser.getErrorLogs(); * expect(errors).toHaveLength(0); * * // Accessibility testing * const violations = await browser.getAccessibilityViolations(); * expect(violations).toHaveLength(0); * ``` */ class Browser extends web_app_1.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 }); * ``` * * [em]: https://chromedriver.chromium.org/mobile-emulation * [devem]: https://developer.chrome.com/devtools/docs/device-mode */ constructor(driverOrOptions, mobileEmulation, enableBidi) { var _a; let driver; // If the first parameter is a driver instance, use it directly. if (driverOrOptions && isDriver(driverOrOptions)) { driver = driverOrOptions; } else { const options = driverOrOptions || {}; if (mobileEmulation && !options.mobileEmulation) { options.mobileEmulation = mobileEmulation; } if (enableBidi && options.enableBidi === undefined) { options.enableBidi = enableBidi; } driver = (_a = options.driver) !== null && _a !== void 0 ? _a : new driver_manager_1.DriverManager().getDriver({ mobileEmulation: options.mobileEmulation, enableBidi: options.enableBidi }); } 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 * } * ``` */ close() { return __awaiter(this, void 0, void 0, function* () { yield 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'); * ``` */ navigateTo(url) { return __awaiter(this, void 0, void 0, function* () { yield this.driver.navigate().to(url); }); } getRect() { return __awaiter(this, void 0, void 0, function* () { return yield this.driver.manage().window().getRect(); }); } setRect(rect) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c, _d; const currentRect = yield this.driver.manage().window().getRect(); this.driver.manage().window().setRect({ width: (_a = rect.width) !== null && _a !== void 0 ? _a : currentRect.width, height: (_b = rect.height) !== null && _b !== void 0 ? _b : currentRect.height, x: (_c = rect.x) !== null && _c !== void 0 ? _c : currentRect.x, y: (_d = rect.y) !== null && _d !== void 0 ? _d : currentRect.y }); }); } resizeToDocumentScrollHeight() { return __awaiter(this, void 0, void 0, function* () { const originalRect = yield this.getRect(); const viewportHeight = yield this.driver.executeScript("return window.innerHeight"); const documentHeight = yield this.driver.executeScript("return document.body.scrollHeight"); yield 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); * ``` */ resizeWindow(width, height) { return __awaiter(this, void 0, void 0, function* () { try { yield 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'); * ``` */ refresh() { return __awaiter(this, void 0, void 0, function* () { yield 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')); * ``` */ switchToIFrame(elementLocator) { return __awaiter(this, void 0, void 0, function* () { const iframe = yield this.find(elementLocator); yield 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'); * ``` */ getCurrentUrl() { return __awaiter(this, void 0, void 0, function* () { return yield 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 * } * ``` */ getBrowserName() { return __awaiter(this, void 0, void 0, function* () { const capabilities = (yield (yield 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' * ]); * ``` */ getAccessibilityViolations() { return __awaiter(this, arguments, void 0, function* (cssSelector = "html", disableRules = ["color-contrast"]) { yield this.find(selenium_webdriver_1.By.css(cssSelector)); const axe = new webdriverjs_1.default(this.driver) .include(cssSelector) .disableRules(disableRules); const result = yield 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(); * ``` */ clearLogs() { return __awaiter(this, void 0, void 0, function* () { yield this.driver.manage().logs().get(logging_1.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); * ``` */ getErrorLogs() { return __awaiter(this, arguments, void 0, function* (excludeList = ["favicon.ico"], logLevel = logging_1.Level.SEVERE) { const errors = []; const capabilities = (yield (yield this.driver).getCapabilities()); const platform = capabilities.getPlatform().toLowerCase(); const browserName = capabilities.getBrowserName().toLowerCase(); // Can not get FF logs due to issue. // Please see: // https://github.com/mozilla/geckodriver/issues/284#issuecomment-477677764 // // Note: Logs are not supported on mobile platforms too! if (browserName === "chrome" && platform !== "android" && platform !== "iphone") { const logs = yield this.driver.manage().logs().get(logging_1.Type.BROWSER); for (const entry of logs) { // Check if the entry's level is greater than or equal to the provided logLevel and collect all included logs if (entry.level.value >= logLevel.value) { errors.push(entry.message); } } } // Filter errors 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); * ``` */ executeScript(script_1) { return __awaiter(this, arguments, void 0, function* (script, waitBeforeMs = 0, waitAfterMs = 0) { try { // Wait before execution if specified if (waitBeforeMs > 0) { yield new Promise(resolve => setTimeout(resolve, waitBeforeMs)); } // Execute the script const result = yield this.driver.executeScript(script); // Wait after execution if specified if (waitAfterMs > 0) { yield new Promise(resolve => setTimeout(resolve, waitAfterMs)); } return result; } catch (error) { throw new Error(`Failed to execute JavaScript script: ${error instanceof Error ? error.message : String(error)}`); } }); } } exports.Browser = Browser; //# sourceMappingURL=browser.js.map