browsertime
Version:
Get performance metrics from your web page using Browsertime.
802 lines (741 loc) • 29.3 kB
JavaScript
import webdriver from 'selenium-webdriver';
import { parseSelector } from './command/selectorParser.js';
import { Actions } from './command/actions.js';
import { AddText } from './command/addText.js';
import { Click } from './command/click.js';
import { Element } from './command/element.js';
import { Wait } from './command/wait.js';
import { Measure } from './command/measure.js';
import { JavaScript } from './command/javaScript.js';
import { Switch } from './command/switch.js';
import { Screenshot } from './command/screenshot.js';
import { Set } from './command/set.js';
import { Cache } from './command/cache.js';
import { Cookie } from './command/cookie.js';
import { Meta } from './command/meta.js';
import { Watch as StopWatch } from './command/stopWatch.js';
import { Select } from './command/select.js';
import { Debug } from './command/debug.js';
import { Bidi } from './command/bidi.js';
import { AndroidCommand } from './command/android.js';
import { ChromeDevelopmentToolsProtocol } from './command/chromeDevToolsProtocol.js';
import { ChromeTrace } from './command/chromeTrace.js';
import {
SingleClick,
DoubleClick,
ClickAndHold,
ContextClick,
MouseMove
} from './command/mouse/index.js';
import { Scroll } from './command/scroll.js';
import { Navigation } from './command/navigation.js';
import { GeckoProfiler } from '../../firefox/geckoProfiler.js';
import { GeckoProfiler as GeckoProfilerCommand } from './command/geckoProfiler.js';
import { PerfStatsInterface } from './command/perfStats.js';
import { PerfettoTrace } from './command/perfetto.js';
import { SimplePerfProfiler } from './command/simpleperf.js';
/**
* Represents the set of commands available in a Browsertime script.
* @hideconstructor
*/
export class Commands {
constructor({
browser,
engineDelegate,
index,
result,
storageManager,
pageCompleteCheck,
context,
videos,
screenshotManager,
scriptsByCategory,
asyncScriptsByCategory,
postURLScripts,
options
}) {
const measure = new Measure({
browser,
index,
pageCompleteCheck,
result,
engineDelegate,
storageManager,
videos,
scriptsByCategory,
asyncScriptsByCategory,
postURLScripts,
context,
screenshotManager,
options
});
/**
* Provides functionality to collect perfetto traces.
* @type {PerfettoTrace}
*/
this.perfetto = new PerfettoTrace(browser, index, storageManager, options);
/**
* Provides functionality to collect simpleperf profiles.
* @type {SimplePerfProfiler}
*/
this.simpleperf = new SimplePerfProfiler(
browser,
index,
storageManager,
options
);
/**
* Manages GeckoProfiler functionality to collect performance profiles.
* @type {GeckoProfiler}
*/
const browserProfiler = new GeckoProfiler(browser, storageManager, options);
// Profiler
this.profiler = new GeckoProfilerCommand(
browserProfiler,
browser,
index,
options,
result
);
/**
* Manages PerfStats functionality to collect performance counters.
* @type {PerfStatsInterface}
*/
const perfStats = new PerfStatsInterface(browser, options);
this.perfStats = perfStats;
const cdp = new ChromeDevelopmentToolsProtocol(
engineDelegate,
options.browser
);
/**
* Manages Chrome trace functionality, enabling custom profiling and trace collection in Chrome.
* @type {ChromeTrace}
*/
this.trace = new ChromeTrace(engineDelegate, index, options, result);
/**
* Provides functionality to perform click actions on elements in a web page using various selectors.
* Can be called directly as a function with a unified selector string, or use the
* existing by* methods for backward compatibility.
* @type {Function & Click}
*/
const clickInstance = new Click(browser, pageCompleteCheck, options);
/**
* Click on an element using a unified selector string.
* Supports CSS selectors (default), and prefixes: 'id:', 'xpath:', 'text:', 'link:', 'name:', 'class:'.
* @async
* @example await commands.click('#login-btn');
* @example await commands.click('id:login-btn');
* @example await commands.click('text:Submit', { waitForNavigation: true });
* @param {string} selector - The selector string.
* @param {Object} [clickOptions] - Options for the click.
* @param {boolean} [clickOptions.waitForNavigation=false] - If true, waits for the page to complete loading after clicking.
* @returns {Promise<void>}
*/
this.click = async (selector, clickOptions) =>
clickInstance.run(selector, clickOptions);
// Preserve all existing by* methods for backward compatibility
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(clickInstance)
)) {
if (key !== 'constructor' && typeof clickInstance[key] === 'function') {
this.click[key] = clickInstance[key].bind(clickInstance);
}
}
measure.setClick(this.click);
/**
* Provides functionality to control page scrolling in the browser.
* @type {Scroll}
*/
this.scroll = new Scroll(browser, options);
/**
* Provides functionality to add text to elements on a web page using various selectors.
* Can be called directly as a function with a unified selector string, or use the
* existing by* methods for backward compatibility.
* @type {Function & AddText}
*/
const addTextInstance = new AddText(browser, options);
/**
* Add text to an element using a unified selector string.
* @async
* @example await commands.addText('#search-input', 'search term');
* @example await commands.addText('id:username', 'myuser');
* @param {string} selector - The selector string.
* @param {string} text - The text to add.
* @returns {Promise<void>}
*/
this.addText = async (selector, text) =>
addTextInstance.run(selector, text);
// Preserve all existing by* methods for backward compatibility
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(addTextInstance)
)) {
if (key !== 'constructor' && typeof addTextInstance[key] === 'function') {
this.addText[key] = addTextInstance[key].bind(addTextInstance);
}
}
/**
* Provides functionality to wait for different conditions in the browser.
* Can be called directly as a function with a unified selector string, or use the
* existing by* methods for backward compatibility.
* @type {Function & Wait}
*/
const waitInstance = new Wait(browser, pageCompleteCheck);
/**
* Wait for an element using a unified selector string.
* @async
* @example await commands.wait('#my-element', { timeout: 5000 });
* @example await commands.wait('id:loaded', { timeout: 3000, visible: true });
* @param {string} selector - The selector string.
* @param {Object} [waitOptions] - Options for waiting.
* @param {number} [waitOptions.timeout=6000] - Maximum time to wait in ms.
* @param {boolean} [waitOptions.visible=false] - Wait for visibility.
* @returns {Promise<void>}
*/
this.wait = async (selector, waitOptions) =>
waitInstance.run(selector, waitOptions);
// Preserve all existing by* methods for backward compatibility
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(waitInstance)
)) {
if (key !== 'constructor' && typeof waitInstance[key] === 'function') {
this.wait[key] = waitInstance[key].bind(waitInstance);
}
}
/**
* Provides functionality for measuring a navigation.
* @type {Measure}
*/
this.measure = measure;
/**
* Navigates to a specified URL and handles additional setup for a page visit.
* @async
* @example await commands.navigate('https://www.example.org');
* @param {string} url - The URL to navigate to.
* @throws {Error} Throws an error if navigation or setup fails.
* @returns {Promise<void>} A promise that resolves when the navigation and setup are
* @type {(url: string) => Promise<void>}
*/
this.navigate = measure._navigate.bind(measure);
/**
* Provides functionality to control browser navigation such as back, forward, and refresh actions.
* @type {Navigation}
*/
this.navigation = new Navigation(browser, pageCompleteCheck);
/**
* Add a text that will be an error attached to the current page.
* @example await commands.error('My error message');
* @param {string} message - The error message.
* @type {(message: string) => void}
*/
this.error = measure._error.bind(measure);
/**
* Mark this run as an failure. Add a message that explains the failure.
* @example await commands.markAsFailure('My failure message');
* @param {string} message - The message attached as a failure
* @type {(message: string) => void}
*/
this.markAsFailure = measure._failure.bind(measure);
/**
* Executes JavaScript in the browser context.
* @type {JavaScript}
*/
this.js = new JavaScript(browser, pageCompleteCheck);
/**
* Switches context to different frames, windows, or tabs in the browser.
* @type {Switch}
*/
this.switch = new Switch(
browser,
pageCompleteCheck,
measure._navigate.bind(measure)
);
/**
* Sets values on HTML elements in the page.
* Can be called directly as a function with a unified selector string, or use the
* existing by* methods for backward compatibility.
* @type {Function & Set}
*/
const setInstance = new Set(browser, options);
/**
* Set a property on an element using a unified selector string.
* @async
* @example await commands.set('#field', 'new value');
* @example await commands.set('id:title', '<h1>Hello</h1>', 'innerHTML');
* @param {string} selector - The selector string for the element.
* @param {string} setValue - The value to set.
* @param {string} [property='value'] - The property: 'value', 'innerText', or 'innerHTML'.
* @returns {Promise<void>}
*/
this.set = async (selector, setValue, property) =>
setInstance.run(selector, setValue, property);
// Preserve all existing by* methods for backward compatibility
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(setInstance)
)) {
if (key !== 'constructor' && typeof setInstance[key] === 'function') {
this.set[key] = setInstance[key].bind(setInstance);
}
}
/**
* Stopwatch utility for measuring time intervals.
* @type {StopWatch}
*/
this.stopWatch = new StopWatch(measure);
/**
* Manages the browser's cache.
* @type {Cache}
*/
this.cache = new Cache(browser, options.browser, cdp);
/**
* Manages browser cookies — get, set, delete individual or all cookies.
* @type {Cookie}
*/
this.cookie = new Cookie(browser);
/**
* Adds metadata to the user journey.
* @type {Meta}
*/
this.meta = new Meta();
/**
* Takes and manages screenshots.
* @type {Screenshot}
*/
this.screenshot = new Screenshot(screenshotManager, browser, index);
/**
* Use the Chrome DevTools Protocol, available in Chrome and Edge.
* @type {ChromeDevelopmentToolsProtocol}
*/
this.cdp = cdp;
/**
*
* Use WebDriver Bidi. Availible in Firefox and in the future more browsers.
* @type {Bidi}
*/
this.bidi = new Bidi(engineDelegate, options.browser);
/**
* Provides commands for interacting with an Android device.
* @type {AndroidCommand}
*/
this.android = new AndroidCommand(options);
/**
* Provides debugging capabilities within a browser automation script.
* It allows setting breakpoints to pause script execution and inspect the current state.
* @type {Debug}
*/
this.debug = new Debug(browser, options);
const singleClickInstance = new SingleClick(
browser,
pageCompleteCheck,
options
);
const doubleClickInstance = new DoubleClick(
browser,
pageCompleteCheck,
options
);
const contextClickInstance = new ContextClick(browser, options);
const mouseMoveInstance = new MouseMove(browser, options);
const clickAndHoldInstance = new ClickAndHold(browser);
/**
* Interact with the page using the mouse.
* @type {{
* moveTo: (selector: string) => Promise<void>,
* singleClick: (selector: string, mouseOptions?: { waitForNavigation?: boolean }) => Promise<void>,
* doubleClick: (selector: string) => Promise<void>,
* contextClick: (selector: string) => Promise<void>,
* clickAndHold: ClickAndHold
* }}
*/
this.mouse = {
moveTo: async selector => mouseMoveInstance.run(selector),
singleClick: async (selector, mouseOptions) =>
singleClickInstance.run(selector, mouseOptions),
doubleClick: async selector => doubleClickInstance.run(selector),
contextClick: async selector => contextClickInstance.run(selector),
clickAndHold: clickAndHoldInstance
};
// Preserve backward-compatible methods on each mouse command
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(mouseMoveInstance)
)) {
if (
key !== 'constructor' &&
typeof mouseMoveInstance[key] === 'function'
) {
this.mouse.moveTo[key] = mouseMoveInstance[key].bind(mouseMoveInstance);
}
}
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(singleClickInstance)
)) {
if (
key !== 'constructor' &&
typeof singleClickInstance[key] === 'function'
) {
this.mouse.singleClick[key] =
singleClickInstance[key].bind(singleClickInstance);
}
}
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(doubleClickInstance)
)) {
if (
key !== 'constructor' &&
typeof doubleClickInstance[key] === 'function'
) {
this.mouse.doubleClick[key] =
doubleClickInstance[key].bind(doubleClickInstance);
}
}
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(contextClickInstance)
)) {
if (
key !== 'constructor' &&
typeof contextClickInstance[key] === 'function'
) {
this.mouse.contextClick[key] =
contextClickInstance[key].bind(contextClickInstance);
}
}
/**
* Interact with a select element.
* Can be called directly as a function with a unified selector string, or use the
* existing by* methods for backward compatibility.
* @type {Function & Select}
*/
const selectInstance = new Select(browser, options);
/**
* Select an option in a select element using a unified selector string.
* @async
* @example await commands.select('#country', 'SE');
* @example await commands.select('id:language', 'en');
* @param {string} selector - The selector string for the select element.
* @param {string} selectValue - The value of the option to select.
* @returns {Promise<void>}
*/
this.select = async (selector, selectValue) =>
selectInstance.run(selector, selectValue);
// Preserve all existing by* methods for backward compatibility
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(selectInstance)
)) {
if (key !== 'constructor' && typeof selectInstance[key] === 'function') {
this.select[key] = selectInstance[key].bind(selectInstance);
}
}
/**
* Select an option in a select element by its visible text.
* @async
* @example await commands.select.byText('#country', 'Sweden');
* @param {string} selector - The selector string for the select element.
* @param {string} text - The visible text of the option to select.
* @returns {Promise<void>}
*/
this.select.byText = async (selector, text) =>
selectInstance.runByText(selector, text);
/**
* Selenium's action sequence functionality.
* @type {Actions}
* @see https://www.selenium.dev/documentation/webdriver/actions_api/
*/
this.action = new Actions(browser);
/**
* Types text into an element identified by a CSS selector.
* This is a convenience method that wraps {@link AddText#bySelector addText.bySelector}
* with a more conventional parameter order (selector first, then text).
* @async
* @example await commands.type('#search-input', 'search term');
* @param {string} selector - The CSS selector of the element.
* @param {string} text - The text to type into the element.
* @returns {Promise<void>} A promise that resolves when the text has been added.
* @throws {Error} Throws an error if the element is not found.
*/
this.type = async (selector, text) => {
return this.addText(selector, text);
};
this.element = new Element(browser, options);
/**
* Finds an element using a CSS selector, with optional waiting and visibility check.
* @async
* @example const element = await commands.find('.my-element', { timeout: 5000, visible: true });
* @param {string} selector - The CSS selector of the element.
* @param {Object} [options] - Options for finding the element.
* @param {number} [options.timeout] - Maximum time in milliseconds to wait for the element. Defaults to the configured --timeouts.elementWait value.
* @param {boolean} [options.visible=false] - If true, waits for the element to be visible.
* @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
* @throws {Error} Throws an error if the element is not found.
* @type {(selector: string, options?: { timeout?: number, visible?: boolean }) => Promise<import('selenium-webdriver').WebElement>}
*/
this.find = this.element.find.bind(this.element);
/**
* Checks if an element matching the selector exists in the DOM.
* Unlike find(), this does not throw if the element is not found.
* @async
* @example
* if (await commands.exists('#cookie-banner')) {
* await commands.click('#accept-cookies');
* }
* @param {string} selector - The CSS selector or prefixed selector.
* @param {Object} [existsOptions] - Options.
* @param {number} [existsOptions.timeout=0] - Maximum time to wait for the element. Default 0 (no wait).
* @returns {Promise<boolean>} True if the element exists.
*/
this.exists = async (selector, existsOptions = {}) => {
const { locator } = parseSelector(selector);
const timeout = existsOptions.timeout ?? 0;
const driver = browser.getDriver();
try {
await (timeout > 0
? driver.wait(webdriver.until.elementLocated(locator), timeout)
: driver.findElement(locator));
return true;
} catch {
return false;
}
};
const findElement = async selector => {
const { locator } = parseSelector(selector);
const driver = browser.getDriver();
const timeout = options?.timeouts?.elementWait ?? 0;
if (timeout > 0) {
await driver.wait(webdriver.until.elementLocated(locator), timeout);
}
return driver.findElement(locator);
};
/**
* Gets the visible text of an element matching the selector.
* @async
* @example const text = await commands.getText('#greeting');
* @example const text = await commands.getText('id:heading');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<string>} The visible text content.
* @throws {Error} Throws an error if the element is not found.
*/
this.getText = async selector => {
const element = await findElement(selector);
return element.getText();
};
/**
* Gets the value of a form element matching the selector.
* @async
* @example const value = await commands.getValue('#price');
* @example const value = await commands.getValue('id:email');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<string>} The element's value.
* @throws {Error} Throws an error if the element is not found.
*/
this.getValue = async selector => {
const element = await findElement(selector);
return element.getAttribute('value');
};
/**
* Checks if an element matching the selector is visible/displayed.
* @async
* @example const visible = await commands.isVisible('#error-message');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<boolean>} True if the element is visible.
* @throws {Error} Throws an error if the element is not found.
*/
this.isVisible = async selector => {
const element = await findElement(selector);
return element.isDisplayed();
};
/**
* Gets an attribute value of an element matching the selector.
* @async
* @example const href = await commands.getAttribute('#my-link', 'href');
* @example const dataId = await commands.getAttribute('id:item', 'data-id');
* @param {string} selector - The CSS selector or prefixed selector.
* @param {string} attribute - The attribute name.
* @returns {Promise<string>} The attribute value.
* @throws {Error} Throws an error if the element is not found.
*/
this.getAttribute = async (selector, attribute) => {
const element = await findElement(selector);
return element.getAttribute(attribute);
};
/**
* Checks if a form element matching the selector is enabled.
* @async
* @example const enabled = await commands.isEnabled('#submit-btn');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<boolean>} True if the element is enabled.
* @throws {Error} Throws an error if the element is not found.
*/
this.isEnabled = async selector => {
const element = await findElement(selector);
return element.isEnabled();
};
/**
* Checks if a checkbox or radio button is selected/checked.
* @async
* @example const checked = await commands.isChecked('#agree-checkbox');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<boolean>} True if the element is checked/selected.
* @throws {Error} Throws an error if the element is not found.
*/
this.isChecked = async selector => {
const element = await findElement(selector);
return element.isSelected();
};
/**
* Clears the content of a form element matching the selector.
* @async
* @example await commands.clear('#search-input');
* @example await commands.clear('id:email');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<void>} A promise that resolves when the element is cleared.
* @throws {Error} Throws an error if the element is not found.
*/
this.clear = async selector => {
const { locator } = parseSelector(selector);
const driver = browser.getDriver();
const timeout = options?.timeouts?.elementWait ?? 0;
if (timeout > 0) {
await driver.wait(webdriver.until.elementLocated(locator), timeout);
}
const element = await driver.findElement(locator);
return element.clear();
};
/**
* Fills multiple form fields at once. Each key is a selector, each value is the text to type.
* @async
* @example
* await commands.fill({
* '#username': 'admin',
* '#password': 'secret',
* 'id:email': 'user@example.com'
* });
* @param {Object<string, string>} fields - An object mapping selectors to values.
* @returns {Promise<void>} A promise that resolves when all fields are filled.
* @throws {Error} Throws an error if any element is not found.
*/
this.fill = async fields => {
for (const [selector, text] of Object.entries(fields)) {
await this.addText(selector, text);
}
};
/**
* Hovers over an element matching the selector. This is a convenience
* alias for commands.mouse.moveTo(selector).
* @async
* @example await commands.hover('#menu-item');
* @example await commands.hover('id:tooltip-trigger');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<void>}
* @throws {Error} Throws an error if the element is not found.
*/
this.hover = async selector => {
return this.mouse.moveTo(selector);
};
/**
* Presses a keyboard key. Use key names like 'Enter', 'Tab', 'Escape',
* 'Backspace', 'ArrowUp', 'ArrowDown', etc.
* @async
* @example await commands.press('Enter');
* @example await commands.press('Tab');
* @example await commands.press('Escape');
* @param {string} key - The key name to press (e.g. 'Enter', 'Tab', 'Escape').
* @returns {Promise<void>}
*/
this.press = async key => {
const keyValue = webdriver.Key[key.toUpperCase()] || key;
const driver = browser.getDriver();
await driver.actions({ async: true }).sendKeys(keyValue).perform();
return driver.actions().clear();
};
/**
* Gets the title of the current page.
* @async
* @example const title = await commands.getTitle();
* @returns {Promise<string>} The page title.
*/
this.getTitle = async () => {
return browser.getDriver().getTitle();
};
/**
* Gets the URL of the current page.
* @async
* @example const url = await commands.getUrl();
* @returns {Promise<string>} The current URL.
*/
this.getUrl = async () => {
return browser.getDriver().getCurrentUrl();
};
/**
* Checks a checkbox or radio button. Does nothing if already checked.
* @async
* @example await commands.check('#agree-terms');
* @example await commands.check('id:newsletter-opt-in');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<void>}
* @throws {Error} Throws an error if the element is not found.
*/
this.check = async selector => {
const element = await findElement(selector);
const checked = await element.isSelected();
if (!checked) {
return element.click();
}
};
/**
* Unchecks a checkbox. Does nothing if already unchecked.
* @async
* @example await commands.uncheck('#agree-terms');
* @example await commands.uncheck('id:newsletter-opt-in');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<void>}
* @throws {Error} Throws an error if the element is not found.
*/
this.uncheck = async selector => {
const element = await findElement(selector);
const checked = await element.isSelected();
if (checked) {
return element.click();
}
};
/**
* Scrolls the page so that the element matching the selector is visible in the viewport.
* @async
* @example await commands.scrollIntoView('#footer');
* @example await commands.scrollIntoView('id:comments-section');
* @param {string} selector - The CSS selector or prefixed selector.
* @returns {Promise<void>}
* @throws {Error} Throws an error if the element is not found.
*/
this.scrollIntoView = async selector => {
const element = await findElement(selector);
return browser
.getDriver()
.executeScript(
'arguments[0].scrollIntoView({block: "center", behavior: "instant"})',
element
);
};
/**
* Waits until the browser's current URL contains the given string.
* Useful after form submissions, login redirects, or SPA navigation.
* @async
* @example await commands.waitForUrl('dashboard');
* @example await commands.waitForUrl('/account', { timeout: 10000 });
* @param {string} pattern - The string to match against the current URL.
* @param {Object} [urlOptions] - Options.
* @param {number} [urlOptions.timeout=10000] - Maximum time to wait in milliseconds.
* @returns {Promise<void>}
* @throws {Error} Throws an error if the URL does not match within the timeout.
*/
this.waitForUrl = async (pattern, urlOptions = {}) => {
const timeout = urlOptions.timeout ?? 10_000;
const driver = browser.getDriver();
try {
await driver.wait(webdriver.until.urlContains(pattern), timeout);
} catch {
const currentUrl = await driver.getCurrentUrl();
throw new Error(
`URL did not contain '${pattern}' within ${timeout} ms. Current URL: ${currentUrl}`
);
}
};
}
}