MOBILE-4110 behat: Allow finding disabled elements

main
Noel De Martin 2022-06-29 12:17:29 +02:00
parent e974912880
commit a0363deb6a
5 changed files with 140 additions and 56 deletions

View File

@ -126,14 +126,14 @@ class behat_app extends behat_app_helper {
$containerName = json_encode($containerName);
$this->spin(function() use ($not, $locator, $containerName) {
$result = $this->js("return window.behat.find($locator, $containerName);");
$result = $this->js("return window.behat.find($locator, { containerName: $containerName });");
if ($not && $result === 'OK') {
throw new DriverException('Error, found an item that should not be found');
throw new DriverException('Error, found an element that should not be found');
}
if (!$not && $result !== 'OK') {
throw new DriverException('Error finding item - ' . $result);
throw new DriverException('Error finding element - ' . $result);
}
return true;
@ -155,7 +155,7 @@ class behat_app extends behat_app_helper {
$result = $this->js("return window.behat.scrollTo($locator);");
if ($result !== 'OK') {
throw new DriverException('Error finding item - ' . $result);
throw new DriverException('Error finding element - ' . $result);
}
return true;
@ -224,16 +224,16 @@ class behat_app extends behat_app_helper {
switch ($result) {
case 'YES':
if ($not) {
throw new ExpectationException("Item was selected and shouldn't have", $this->getSession()->getDriver());
throw new ExpectationException("Element was selected and shouldn't have", $this->getSession()->getDriver());
}
break;
case 'NO':
if (!$not) {
throw new ExpectationException("Item wasn't selected and should have", $this->getSession()->getDriver());
throw new ExpectationException("Element wasn't selected and should have", $this->getSession()->getDriver());
}
break;
default:
throw new DriverException('Error finding item - ' . $result);
throw new DriverException('Error finding element - ' . $result);
}
return true;
@ -536,7 +536,7 @@ class behat_app extends behat_app_helper {
* Clicks on / touches something that is visible in the app.
*
* Note it is difficult to use the standard 'click on' or 'press' steps because those do not
* distinguish visible items and the app always has many non-visible items in the DOM.
* distinguish visible elements and the app always has many non-visible elements in the DOM.
*
* @When /^I press (".+") in the app$/
* @param string $locator Element locator
@ -578,6 +578,33 @@ class behat_app extends behat_app_helper {
$this->wait_for_pending_js();
}
/**
* Checks if elements can be pressed in the app.
*
* @Then /^I should( not)? be able to press (".+") in the app$/
* @param bool $not Whether to assert that the element cannot be pressed
* @param string $locator Element locator
*/
public function i_should_be_able_to_press_in_the_app(bool $not, string $locator) {
$locator = $this->parse_element_locator($locator);
$this->spin(function() use ($not, $locator) {
$result = $this->js("return window.behat.find($locator, { onlyClickable: true });");
if ($not && $result === 'OK') {
throw new DriverException('Error, found a clickable element that should not be found');
}
if (!$not && $result !== 'OK') {
throw new DriverException('Error finding clickable element - ' . $result);
}
return true;
});
$this->wait_for_pending_js();
}
/**
* Select an item from a list of options, such as a radio button.
*
@ -602,11 +629,11 @@ class behat_app extends behat_app_helper {
return true;
}
// Press item.
// Press element.
$result = $this->js("return await window.behat.press($locator);");
if ($result !== 'OK') {
throw new DriverException('Error pressing item - ' . $result);
throw new DriverException('Error pressing element - ' . $result);
}
// Check that it worked as expected.

View File

@ -45,8 +45,9 @@ Feature: Users can manage entries in database activities
Scenario: Browse entry
Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app
# TODO Create and use a generator for database entries.
And I press "Add entries" in the app
When I press "Add entries" in the app
And I set the following fields to these values in the app:
| URL | https://moodle.org/ |
| Description | Moodle community site |
@ -59,16 +60,19 @@ Feature: Users can manage entries in database activities
And I press "Save" near "Web links" in the app
And I press "More" near "Moodle community site" in the app
Then I should find "Moodle community site" in the app
And I should not find "Next" in the app
And I should find "Previous" in the app
And I press "Previous" in the app
And I should find "Moodle Cloud" in the app
And I should find "Next" in the app
And I should not find "Previous" in the app
And I press "Next" in the app
And I should find "Moodle community site" in the app
And I should not find "Moodle Cloud" in the app
And I press the back button in the app
And I should be able to press "Previous" in the app
But I should not be able to press "Next" in the app
When I press "Previous" in the app
Then I should find "Moodle Cloud" in the app
And I should be able to press "Next" in the app
But I should not be able to press "Previous" in the app
When I press "Next" in the app
Then I should find "Moodle community site" in the app
But I should not find "Moodle Cloud" in the app
When I press the back button in the app
And I should find "Moodle community site" in the app
And I should find "Moodle Cloud" in the app

View File

@ -79,9 +79,14 @@ export class TestingBehatDomUtils {
*
* @param container Parent element to search the element within
* @param text Text to look for
* @param options Search options.
* @return Elements containing the given text with exact boolean.
*/
protected static findElementsBasedOnTextWithinWithExact(container: HTMLElement, text: string): ElementsWithExact[] {
protected static findElementsBasedOnTextWithinWithExact(
container: HTMLElement,
text: string,
options: TestingBehatFindOptions,
): ElementsWithExact[] {
const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"], [placeholder*="${text}"]`;
const elements = Array.from(container.querySelectorAll<HTMLElement>(attributesSelector))
@ -97,16 +102,23 @@ export class TestingBehatDomUtils {
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_DOCUMENT_FRAGMENT | NodeFilter.SHOW_TEXT, // eslint-disable-line no-bitwise
{
acceptNode: node => {
if (node instanceof HTMLStyleElement ||
if (
node instanceof HTMLStyleElement ||
node instanceof HTMLLinkElement ||
node instanceof HTMLScriptElement) {
node instanceof HTMLScriptElement
) {
return NodeFilter.FILTER_REJECT;
}
if (node instanceof HTMLElement &&
(node.getAttribute('aria-hidden') === 'true' ||
node.getAttribute('aria-disabled') === 'true' ||
getComputedStyle(node).display === 'none')) {
if (!(node instanceof HTMLElement)) {
return NodeFilter.FILTER_ACCEPT;
}
if (options.onlyClickable && (node.getAttribute('aria-disabled') === 'true' || node.hasAttribute('disabled'))) {
return NodeFilter.FILTER_REJECT;
}
if (node.getAttribute('aria-hidden') === 'true' || getComputedStyle(node).display === 'none') {
return NodeFilter.FILTER_REJECT;
}
@ -160,7 +172,7 @@ export class TestingBehatDomUtils {
continue;
}
elements.push(...this.findElementsBasedOnTextWithinWithExact(childNode, text));
elements.push(...this.findElementsBasedOnTextWithinWithExact(childNode, text, options));
}
}
}
@ -187,10 +199,15 @@ export class TestingBehatDomUtils {
*
* @param container Parent element to search the element within.
* @param text Text to look for.
* @param options Search options.
* @return Elements containing the given text.
*/
protected static findElementsBasedOnTextWithin(container: HTMLElement, text: string): HTMLElement[] {
const elements = this.findElementsBasedOnTextWithinWithExact(container, text);
protected static findElementsBasedOnTextWithin(
container: HTMLElement,
text: string,
options: TestingBehatFindOptions,
): HTMLElement[] {
const elements = this.findElementsBasedOnTextWithinWithExact(container, text, options);
// Give more relevance to exact matches.
elements.sort((a, b) => Number(b.exact) - Number(a.exact));
@ -325,32 +342,33 @@ export class TestingBehatDomUtils {
* Function to find element based on their text or Aria label.
*
* @param locator Element locator.
* @param containerName Whether to search only inside a specific container.
* @param options Search options.
* @return First found element.
*/
static findElementBasedOnText(locator: TestingBehatElementLocator, containerName = ''): HTMLElement {
return this.findElementsBasedOnText(locator, containerName, true)[0];
static findElementBasedOnText(
locator: TestingBehatElementLocator,
options: TestingBehatFindOptions,
): HTMLElement {
return this.findElementsBasedOnText(locator, options)[0];
}
/**
* Function to find elements based on their text or Aria label.
*
* @param locator Element locator.
* @param containerName Whether to search only inside a specific container.
* @param stopWhenFound Stop looking in containers once an element is found.
* @param options Search options.
* @return Found elements
*/
protected static findElementsBasedOnText(
locator: TestingBehatElementLocator,
containerName = '',
stopWhenFound = false,
options: TestingBehatFindOptions,
): HTMLElement[] {
const topContainers = this.getCurrentTopContainerElements(containerName);
const topContainers = this.getCurrentTopContainerElements(options.containerName);
let elements: HTMLElement[] = [];
for (let i = 0; i < topContainers.length; i++) {
elements = elements.concat(this.findElementsBasedOnTextInContainer(locator, topContainers[i]));
if (stopWhenFound && elements.length) {
elements = elements.concat(this.findElementsBasedOnTextInContainer(locator, topContainers[i], options));
if (elements.length) {
break;
}
}
@ -363,16 +381,18 @@ export class TestingBehatDomUtils {
*
* @param locator Element locator.
* @param topContainer Container to search in.
* @param options Search options.
* @return Found elements
*/
protected static findElementsBasedOnTextInContainer(
locator: TestingBehatElementLocator,
topContainer: HTMLElement,
options: TestingBehatFindOptions,
): HTMLElement[] {
let container: HTMLElement | null = topContainer;
if (locator.within) {
const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer);
const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer, options);
if (withinElements.length === 0) {
throw new Error('There was no match for within text');
@ -390,7 +410,10 @@ export class TestingBehatDomUtils {
}
if (topContainer && locator.near) {
const nearElements = this.findElementsBasedOnTextInContainer(locator.near, topContainer);
const nearElements = this.findElementsBasedOnTextInContainer(locator.near, topContainer, {
...options,
onlyClickable: false,
});
if (nearElements.length === 0) {
throw new Error('There was no match for near text');
@ -412,7 +435,7 @@ export class TestingBehatDomUtils {
break;
}
const elements = this.findElementsBasedOnTextWithin(container, locator.text);
const elements = this.findElementsBasedOnTextWithin(container, locator.text, options);
let filteredElements: HTMLElement[] = elements;

View File

@ -153,23 +153,27 @@ export class TestingBehatRuntime {
// Find button
let foundButton: HTMLElement | undefined;
const options: TestingBehatFindOptions = {
onlyClickable: true,
containerName: '',
};
switch (button) {
case 'back':
foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Back' });
foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Back' }, options);
break;
case 'main menu': // Deprecated name.
case 'more menu':
foundButton = TestingBehatDomUtils.findElementBasedOnText({
text: 'More',
selector: 'ion-tab-button',
});
}, options);
break;
case 'user menu' :
foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'User account' });
foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'User account' }, options);
break;
case 'page menu':
foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Display options' });
foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Display options' }, options);
break;
default:
return 'ERROR: Unsupported standard button type';
@ -215,20 +219,24 @@ export class TestingBehatRuntime {
* Function to find an arbitrary element based on its text or aria label.
*
* @param locator Element locator.
* @param containerName Whether to search only inside a specific container content.
* @param options Search options.
* @return OK if successful, or ERROR: followed by message
*/
static find(locator: TestingBehatElementLocator, containerName: string): string {
this.log('Action - Find', { locator, containerName });
static find(locator: TestingBehatElementLocator, options: Partial<TestingBehatFindOptions> = {}): string {
this.log('Action - Find', { locator, ...options });
try {
const element = TestingBehatDomUtils.findElementBasedOnText(locator, containerName);
const element = TestingBehatDomUtils.findElementBasedOnText(locator, {
onlyClickable: false,
containerName: '',
...options,
});
if (!element) {
return 'ERROR: No element matches locator to find.';
}
this.log('Action - Found', { locator, containerName, element });
this.log('Action - Found', { locator, element, ...options });
return 'OK';
} catch (error) {
@ -246,7 +254,7 @@ export class TestingBehatRuntime {
this.log('Action - scrollTo', { locator });
try {
let element = TestingBehatDomUtils.findElementBasedOnText(locator);
let element = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: false, containerName: '' });
if (!element) {
return 'ERROR: No element matches element to scroll to.';
@ -320,7 +328,7 @@ export class TestingBehatRuntime {
this.log('Action - Is Selected', locator);
try {
const element = TestingBehatDomUtils.findElementBasedOnText(locator);
const element = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: false, containerName: '' });
return TestingBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO';
} catch (error) {
@ -338,7 +346,7 @@ export class TestingBehatRuntime {
this.log('Action - Press', locator);
try {
const found = TestingBehatDomUtils.findElementBasedOnText(locator);
const found = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: true, containerName: '' });
if (!found) {
return 'ERROR: No element matches locator to press.';
@ -421,6 +429,7 @@ export class TestingBehatRuntime {
const found: HTMLElement | HTMLInputElement = TestingBehatDomUtils.findElementBasedOnText(
{ text: field, selector: 'input, textarea, [contenteditable="true"], ion-select' },
{ onlyClickable: false, containerName: '' },
);
if (!found) {
@ -478,6 +487,11 @@ export type BehatTestsWindow = Window & {
behat?: unknown;
};
export type TestingBehatFindOptions = {
containerName: string;
onlyClickable: boolean;
};
export type TestingBehatElementLocator = {
text: string;
within?: TestingBehatElementLocator;

View File

@ -0,0 +1,16 @@
@app @javascript
Feature: It has a Behat runtime with testing helpers.
Background:
Given the following "users" exist:
| username |
| student1 |
Scenario: Finds and presses elements
Given I entered the app as "student1"
When I set the following fields to these values in the app:
| Search by activity type or name | Foo bar |
Then I should find "Search" "button" in the app
And I should find "Clear search" in the app
And I should be able to press "Search" "button" in the app
But I should not be able to press "Clear search" in the app