Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add access to Selenium Action API. #2061

Merged
merged 14 commits into from
Jan 7, 2024
52 changes: 52 additions & 0 deletions lib/core/engine/command/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// We disable these because they are needed for code completion
/* eslint no-unused-vars: "off" */
import { Actions as SeleniumActions } from 'selenium-webdriver/lib/input.js';
/**
* This class provides an abstraction layer for Selenium's action sequence functionality.
* It allows for easy interaction with web elements using different locating strategies
* and simulating complex user gestures like mouse movements, key presses, etc.
*
* @class
* @hideconstructor
* @see https://www.selenium.dev/documentation/webdriver/actions_api/
* @see https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html
*/
export class Actions {
constructor(browser) {
/**
* @private
*/
this.driver = browser.getDriver();
/**
* @private
*/
this.actions = this.driver.actions({ async: true });
}

/*
* Clears the stored actions. You need to manually clear actions, before you start a new action.
*
* @returns {Promise<void>} A promise that will be resolved when the actions have been
* cleared.
*/
async clear() {
return this.driver.actions().clear();
}

/**
* Retrieves the current action sequence builder.
* The actions builder can be used to chain multiple browser actions.
* @returns {SeleniumActions} The current Selenium Actions builder object for chaining browser actions.
* @example
* // Example of using the actions builder to perform a drag-and-drop operation:
* const elementToDrag = await commands.action.getElementByCss('.draggable');
* const dropTarget = await commands.action.getElementByCss('.drop-target');
* await commands.action.getAction()
* .dragAndDrop(elementToDrag, dropTarget)
* .perform();
*
*/
getActions() {
return this.actions;
}
}
66 changes: 66 additions & 0 deletions lib/core/engine/command/element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// We disable these because they are needed for code completion
/* eslint no-unused-vars: "off" */
import { By, WebElement } from 'selenium-webdriver';
/**
* This class provides a way to get hokld of Seleniums WebElements.
* @class
* @hideconstructor
*/
export class Element {
constructor(browser) {
/**
* @private
*/
this.driver = browser.getDriver();
}

/**
* Finds an element by its CSS selector.
*
* @param {string} name - The CSS selector of the element.
* @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
*/
async getByCss(name) {
return this.driver.findElement(By.css(name));
}

/**
* Finds an element by its ID.
*
* @param {string} id - The ID of the element.
* @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
*/
async getById(id) {
return this.driver.findElement(By.id(id));
}

/**
* Finds an element by its XPath.
*
* @param {string} xpath - The XPath query of the element.
* @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
*/
async getByXpath(xpath) {
return this.driver.findElement(By.xpath(xpath));
}

/**
* Finds an element by its class name.
*
* @param {string} className - The class name of the element.
* @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
*/
async getByClassName(className) {
return this.driver.findElement(By.className(className));
}

/**
* Finds an element by its name attribute.
*
* @param {string} name - The name attribute of the element.
* @returns {Promise<WebElement>} A promise that resolves to the WebElement found.
*/
async getByName(name) {
return this.driver.findElement(By.name(name));
}
}
15 changes: 15 additions & 0 deletions lib/core/engine/commands.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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';
Expand Down Expand Up @@ -248,5 +250,18 @@ export class Commands {
* @type {Select}
*/
this.select = new Select(browser);

/**
* Selenium's action sequence functionality.
* @type {Actions}
* @see https://www.selenium.dev/documentation/webdriver/actions_api/
*/
this.action = new Actions(browser);

/**
* Get Selenium's WebElements.
* @type {Element}
*/
this.element = new Element(browser);
}
}
40 changes: 40 additions & 0 deletions test/commandtests/actionTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import test from 'ava';
const { before, after, serial } = test;
import { resolve } from 'node:path';
import { getEngine } from '../util/engine.js';
import { startServer, stopServer } from '../util/httpserver.js';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const timeout = 20_000;

let engine;

function getPath(file) {
return resolve(__dirname, '..', 'data', 'commandscripts', file);
}

before('Setup the HTTP server', () => {
return startServer();
});

after.always('Stop the HTTP server', () => {
return stopServer();
});

serial.beforeEach('Start the browser', async t => {
t.timeout(timeout);
engine = getEngine();
return engine.start();
});

serial('Run through the action API', async t => {
const result = await engine.runMultiple([getPath('actions.cjs')], {
scripts: { uri: 'document.documentURI' }
});
t.deepEqual(
result[0].browserScripts[0].scripts.uri,
'http://127.0.0.1:3000/simple/'
);
});
12 changes: 12 additions & 0 deletions test/data/commandscripts/actions.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = async function (context, commands) {
await commands.measure.start('http://127.0.0.1:3000/simple/');
const clickable = await commands.element.getById('clickable');
return commands.action.getActions()
.move({ origin: clickable })
.pause(1000)
.press()
.pause(1000)
.sendKeys('abc')
.perform();

};
2 changes: 2 additions & 0 deletions test/data/html/simple/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
<h1>Welcome to the super simple and fast test page</h1>
<p>It will be hard to create a faster page than this page.</p>
<p><a href="/dimple/">Dimple</a></p>
<input type="text" id="clickable" placeholder="Clickable"/>

</body>
</html>