Skip to content

Commit

Permalink
Add access to Selenium Action API. (#2061)
Browse files Browse the repository at this point in the history
* Add access to Selenium Action API.

The Action API gives us the ability to write so much cleaner code.
  • Loading branch information
soulgalore authored Jan 7, 2024
1 parent 96f83d8 commit d1a4b87
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 0 deletions.
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>

0 comments on commit d1a4b87

Please sign in to comment.