MOBILE-4110 behat: Allow finding disabled elements
parent
e974912880
commit
a0363deb6a
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue