diff --git a/src/addons/mod/forum/tests/behat/basic_usage.feature b/src/addons/mod/forum/tests/behat/basic_usage.feature index ddf3e4990..b566d7f00 100755 --- a/src/addons/mod/forum/tests/behat/basic_usage.feature +++ b/src/addons/mod/forum/tests/behat/basic_usage.feature @@ -247,7 +247,7 @@ Feature: Test basic usage of forum activity in app And I switch offline mode to "true" And I press "None" near "test2" in the app And I press "0" near "Cancel" in the app - Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." inside the toast in the app + Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." in the app And I should find "Average of ratings: -" in the app And I should find "Average of ratings: 1" in the app diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index 761806d69..e0b1af642 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -17,6 +17,9 @@ import { NgZone } from '@singletons'; import { TestsBehatBlocking } from './behat-blocking'; import { TestBehatElementLocator } from './behat-runtime'; +// Containers that block containers behind them. +const blockingContainers = ['ION-ALERT', 'ION-POPOVER', 'ION-ACTION-SHEET', 'CORE-USER-TOURS-USER-TOUR', 'ION-PAGE']; + /** * Behat Dom Utils helper functions. */ @@ -251,71 +254,68 @@ export class TestsBehatDomUtils { }; /** - * Function to find top container element. + * Function to find top container elements. * * @param containerName Whether to search inside the a container name. - * @return Found top container element. + * @return Found top container elements. */ - protected static getCurrentTopContainerElement(containerName: string): HTMLElement | null { - let topContainer: HTMLElement | null = null; - let containers: HTMLElement[] = []; - const nonImplementedSelectors = - 'ion-alert, ion-popover, ion-action-sheet, ion-modal, core-user-tours-user-tour.is-active, page-core-mainmenu, ion-app'; + protected static getCurrentTopContainerElements(containerName: string): HTMLElement[] { + const topContainers: HTMLElement[] = []; + let containers = Array.from(document.querySelectorAll([ + 'ion-alert.hydrated', + 'ion-popover.hydrated', + 'ion-action-sheet.hydrated', + 'ion-modal.hydrated', + 'core-user-tours-user-tour.is-active', + 'ion-toast.hydrated', + 'page-core-mainmenu', + 'ion-app', + ].join(', '))); - switch (containerName) { - case 'html': - containers = Array.from(document.querySelectorAll('html')); - break; - case 'toast': - containers = Array.from(document.querySelectorAll('ion-app ion-toast.hydrated')); - containers = containers.map(container => container?.shadowRoot?.querySelector('.toast-container') || container); - break; - case 'alert': - containers = Array.from(document.querySelectorAll('ion-app ion-alert.hydrated')); - break; - case 'action-sheet': - containers = Array.from(document.querySelectorAll('ion-app ion-action-sheet.hydrated')); - break; - case 'modal': - containers = Array.from(document.querySelectorAll('ion-app ion-modal.hydrated')); - break; - case 'popover': - containers = Array.from(document.querySelectorAll('ion-app ion-popover.hydrated')); - break; - case 'user-tour': - containers = Array.from(document.querySelectorAll('core-user-tours-user-tour.is-active')); - break; - default: - // Other containerName or not implemented. - containers = Array.from(document.querySelectorAll(nonImplementedSelectors)); + containers = containers + .filter(container => { + if (container.tagName === 'ION-ALERT') { + // For some reason, in Behat sometimes alerts aren't removed from DOM, the close animation doesn't finish. + // Filter alerts with pointer-events none since that style is set before the close animation starts. + return container.style.pointerEvents !== 'none'; + } + + // Ignore pages that are inside other visible pages. + return container.tagName !== 'ION-PAGE' || !container.closest('.ion-page.ion-page-hidden'); + }) + // Sort them by z-index. + .sort((a, b) => Number(getComputedStyle(b).zIndex) - Number(getComputedStyle(a).zIndex)); + + if (containerName === 'split-view content') { + // Find non hidden pages inside the containers. + containers.some(container => { + if (!container.classList.contains('ion-page')) { + return false; + } + + const pageContainers = Array.from(container.querySelectorAll('.ion-page:not(.ion-page-hidden)')); + let topContainer = pageContainers.find((page) => !page.closest('.ion-page.ion-page-hidden')) ?? null; + + topContainer = (topContainer || container).querySelector('core-split-view ion-router-outlet'); + topContainer && topContainers.push(topContainer); + + return !!topContainer; + }); + + return topContainers; } - if (containers.length > 0) { - // Get the one with more zIndex. - topContainer = - containers.reduce((a, b) => getComputedStyle(a).zIndex > getComputedStyle(b).zIndex ? a : b, containers[0]); - } - - if (!topContainer) { - return null; - } - - if (containerName == 'page' || containerName == 'split-view content') { - // Find non hidden pages inside the container. - let pageContainers = Array.from(topContainer.querySelectorAll('.ion-page:not(.ion-page-hidden)')); - pageContainers = pageContainers.filter((page) => !page.closest('.ion-page.ion-page-hidden')); - - if (pageContainers.length > 0) { - // Get the more general one to avoid failing. - topContainer = pageContainers[0]; + // Get containers until one blocks other views. + containers.find(container => { + if (container.tagName === 'ION-TOAST') { + container = container.shadowRoot?.querySelector('.toast-container') || container; } + topContainers.push(container); - if (containerName == 'split-view content') { - topContainer = topContainer.querySelector('core-split-view ion-router-outlet'); - } - } + return blockingContainers.includes(container.tagName); + }); - return topContainer; + return topContainers; }; /** @@ -337,9 +337,24 @@ export class TestsBehatDomUtils { * @return Found elements */ protected static findElementsBasedOnText(locator: TestBehatElementLocator, containerName = ''): HTMLElement[] { - let topContainer = this.getCurrentTopContainerElement(containerName); + const topContainers = this.getCurrentTopContainerElements(containerName); - let container = topContainer; + return topContainers.reduce((elements, container) => + elements.concat(this.findElementsBasedOnTextInContainer(locator, container)), []); + } + + /** + * Function to find elements based on their text or Aria label. + * + * @param locator Element locator. + * @param container Container to search in. + * @return Found elements + */ + protected static findElementsBasedOnTextInContainer( + locator: TestBehatElementLocator, + topContainer: HTMLElement, + ): HTMLElement[] { + let container: HTMLElement | null = topContainer; if (locator.within) { const withinElements = this.findElementsBasedOnText(locator.within);