Merge pull request #3338 from NoelDeMartin/MOBILE-4110
MOBILE-4110: Behat improvements
This commit is contained in:
		
						commit
						1f81ea3513
					
				
							
								
								
									
										21
									
								
								.github/scripts/functions.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/scripts/functions.sh
									
									
									
									
										vendored
									
									
								
							| @ -63,3 +63,24 @@ function notify_on_error_exit { | ||||
|         exit 1 | ||||
|     fi | ||||
| } | ||||
| 
 | ||||
| function get_behat_plugin_changes_diff { | ||||
|     i=0 | ||||
|     previoushash="" | ||||
|     currenthash=`git rev-parse HEAD` | ||||
|     initialhash=`git rev-list HEAD | tail -n 1` | ||||
|     totalcommits=`git log --oneline | wc -l` | ||||
|     repositoryname=`echo $GITHUB_REPOSITORY | sed "s/\\//\\\\\\\\\\//"` | ||||
| 
 | ||||
|     ((totalcommits--)) | ||||
|     while [ $i -lt $totalcommits ] && [[ -z $previoushash ]]; do | ||||
|         previoushash=`git rev-list --format=%B --max-count=1 HEAD~$i | grep -o "https:\/\/github\.com\/$repositoryname\/compare\/[^.]\+\.\.\.[^.]\+" | sed "s/https:\/\/github\.com\/$repositoryname\/compare\/[^.]\+\.\.\.//"` | ||||
|         ((i++)) | ||||
|     done | ||||
| 
 | ||||
|     if [[ -z $previoushash ]]; then | ||||
|         previoushash=$initialhash | ||||
|     fi | ||||
| 
 | ||||
|     echo "$previoushash...$currenthash" | ||||
| } | ||||
|  | ||||
							
								
								
									
										68
									
								
								.github/scripts/update_behat_plugin.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										68
									
								
								.github/scripts/update_behat_plugin.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @ -0,0 +1,68 @@ | ||||
| #!/bin/bash | ||||
| source "./.github/scripts/functions.sh" | ||||
| 
 | ||||
| if [ -z $GIT_TOKEN ] || [ -z $BEHAT_PLUGIN_GITHUB_REPOSITORY ] || [ -z $BEHAT_PLUGIN_BRANCH ]; then | ||||
|     print_error "Env vars not correctly defined" | ||||
|     exit 1 | ||||
| fi | ||||
| 
 | ||||
| if [[ $BEHAT_PLUGIN_BRANCH != $GITHUB_REF_NAME ]]; then | ||||
|     echo "Script disabled for this branch" | ||||
|     exit 0 | ||||
| fi | ||||
| 
 | ||||
| # Clone plugin repository. | ||||
| print_title "Cloning Behat plugin repository..." | ||||
| 
 | ||||
| git clone https://$GIT_TOKEN@github.com/$BEHAT_PLUGIN_GITHUB_REPOSITORY.git tmp/local_moodleappbehat -b integration | ||||
| pluginversion=$(cat tmp/local_moodleappbehat/version.php | grep "\$plugin->version" | grep -o -E "[0-9]+") | ||||
| 
 | ||||
| # Auto-generate plugin. | ||||
| print_title "Building Behat plugin..." | ||||
| 
 | ||||
| if [ -z $BEHAT_PLUGIN_EXCLUDE_FEATURES ]; then | ||||
|     scripts/build-behat-plugin.js tmp/local_moodleappbehat | ||||
| else | ||||
|     scripts/build-behat-plugin.js tmp/local_moodleappbehat --exclude-features | ||||
| fi | ||||
| notify_on_error_exit "Unsuccessful build, stopping..." | ||||
| 
 | ||||
| # Check if there are any changes (ignoring plugin version). | ||||
| print_title "Checking changes..." | ||||
| 
 | ||||
| newpluginversion=$(cat tmp/local_moodleappbehat/version.php | grep "\$plugin->version" | grep -o -E "[0-9]+") | ||||
| sed -i s/\$plugin-\>version\ =\ [0-9]\\+\;/\$plugin-\>version\ =\ $pluginversion\;/ tmp/local_moodleappbehat/version.php | ||||
| 
 | ||||
| if [[ -z `git -C tmp/local_moodleappbehat/ status --short` ]]; then | ||||
|     echo "There weren't any changes to apply!" | ||||
|     exit | ||||
| fi | ||||
| 
 | ||||
| if [[ $pluginversion -eq $newpluginversion ]]; then | ||||
|     ((newpluginversion++)) | ||||
| fi | ||||
| 
 | ||||
| sed -i s/\$plugin-\>version\ =\ [0-9]\\+\;/\$plugin-\>version\ =\ $newpluginversion\;/ tmp/local_moodleappbehat/version.php | ||||
| 
 | ||||
| # Apply new changes | ||||
| print_title "Applying changes to repository..." | ||||
| 
 | ||||
| cd tmp/local_moodleappbehat | ||||
| 
 | ||||
| diff=`get_behat_plugin_changes_diff` | ||||
| 
 | ||||
| # Set up Github Actions bot user | ||||
| # See https://github.community/t/github-actions-bot-email-address/17204/6 | ||||
| git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||||
| git config --local user.name "github-actions[bot]" | ||||
| git add . | ||||
| git commit -m "[auto-generated] Update plugin files | ||||
| Check out the commits that caused these changes: https://github.com/$GITHUB_REPOSITORY/compare/$diff | ||||
| " | ||||
| notify_on_error_exit "Unsuccessful commit, stopping..." | ||||
| 
 | ||||
| echo "Pushing changes..." | ||||
| git push | ||||
| notify_on_error_exit "Unsuccessful push, stopping..." | ||||
| 
 | ||||
| echo "Behat plugin updated!" | ||||
							
								
								
									
										21
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,10 +1,10 @@ | ||||
| name: Behat tests | ||||
| name: Acceptance tests (Behat) | ||||
| 
 | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|     inputs: | ||||
|       tags: | ||||
|         description: 'Execute tags' | ||||
|       behat_tags: | ||||
|         description: 'Behat tags to execute' | ||||
|         required: true | ||||
|         default: '~@performance' | ||||
|       moodle_branch: | ||||
| @ -15,6 +15,10 @@ on: | ||||
|         description: 'Moodle repository' | ||||
|         required: true | ||||
|         default: 'https://github.com/moodle/moodle' | ||||
|   pull_request: | ||||
|     branches: | ||||
|      - integration | ||||
|      - unscheduled | ||||
| 
 | ||||
| jobs: | ||||
|   behat: | ||||
| @ -23,6 +27,9 @@ jobs: | ||||
|       MOODLE_DOCKER_DB: pgsql | ||||
|       MOODLE_DOCKER_BROWSER: chrome | ||||
|       MOODLE_DOCKER_PHP_VERSION: 7.3 | ||||
|       MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'master' }} | ||||
|       MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }} | ||||
|       BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance' }} | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - id: nvmrc | ||||
| @ -32,7 +39,7 @@ jobs: | ||||
|         node-version: '${{ steps.nvmrc.outputs.node_version }}' | ||||
|     - name: Additional checkouts | ||||
|       run: | | ||||
|         git clone --branch ${{ github.event.inputs.moodle_branch }} --depth 1 ${{ github.event.inputs.moodle_repository }} $GITHUB_WORKSPACE/moodle | ||||
|         git clone --branch $MOODLE_BRANCH --depth 1 $MOODLE_REPOSITORY $GITHUB_WORKSPACE/moodle | ||||
|         git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker | ||||
|     - name: Install npm packages | ||||
|       run: npm ci --no-audit | ||||
| @ -48,9 +55,9 @@ jobs: | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose pull | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose up -d | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-wait-for-db | ||||
|     - name: Compile & launch production app with Docker | ||||
|     - name: Compile & launch app with Docker | ||||
|       run: | | ||||
|         docker build -t moodlehq/moodleapp:behat . | ||||
|         docker build --build-arg build_command="npm run build:test" -t moodlehq/moodleapp:behat . | ||||
|         docker run -d --rm --name moodleapp moodlehq/moodleapp:behat | ||||
|         docker network connect moodle-docker_default moodleapp --alias moodleapp | ||||
|     - name: Init Behat | ||||
| @ -60,4 +67,4 @@ jobs: | ||||
|     - name: Run Behat tests | ||||
|       run: | | ||||
|         export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --verbose --tags='@app&&${{ github.event.inputs.tags }}' --auto-rerun" | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --verbose --tags='@app&&$BEHAT_TAGS' --auto-rerun" | ||||
|  | ||||
							
								
								
									
										4
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							| @ -40,9 +40,9 @@ jobs: | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose pull | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose up -d | ||||
|         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-wait-for-db | ||||
|     - name: Compile & launch production app with Docker | ||||
|     - name: Compile & launch app with Docker | ||||
|       run: | | ||||
|         docker build -t moodlehq/moodleapp:performance . | ||||
|         docker build --build-arg build_command="npm run build:test" -t moodlehq/moodleapp:performance . | ||||
|         docker run -d --rm --name moodleapp moodlehq/moodleapp:performance | ||||
|         docker network connect moodle-docker_default moodleapp --alias moodleapp | ||||
|     - name: Init Behat | ||||
|  | ||||
							
								
								
									
										18
									
								
								.github/workflows/update_behat_plugin.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/update_behat_plugin.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| name: Update Behat plugin | ||||
| 
 | ||||
| on: ['push'] | ||||
| 
 | ||||
| jobs: | ||||
|   update: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - uses: actions/setup-node@v3 | ||||
|     - name: Install npm packages | ||||
|       run: npm ci --no-audit | ||||
|     - name: Update Behat plugin | ||||
|       env: | ||||
|         GIT_TOKEN: ${{ secrets.GIT_TOKEN }} | ||||
|         BEHAT_PLUGIN_GITHUB_REPOSITORY: ${{ secrets.BEHAT_PLUGIN_GITHUB_REPOSITORY }} | ||||
|         BEHAT_PLUGIN_BRANCH: ${{ secrets.BEHAT_PLUGIN_BRANCH }} | ||||
|       run: ./.github/scripts/update_behat_plugin.sh | ||||
							
								
								
									
										18
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								.travis.yml
									
									
									
									
									
								
							| @ -68,21 +68,3 @@ jobs: | ||||
|       homebrew: | ||||
|         packages: | ||||
|         - jq | ||||
|   - stage: test | ||||
|     name: "End to end tests (mod_forum and mod_messages)" | ||||
|     services: | ||||
|     - docker | ||||
|     if: type = cron | ||||
|     script: scripts/test_e2e.sh "@app&&@mod_forum" "@app&&@mod_messages" | ||||
|   - stage: test | ||||
|     name: "End to end tests (mod_course, core_course and mod_courses)" | ||||
|     services: | ||||
|     - docker | ||||
|     if: type = cron | ||||
|     script: scripts/test_e2e.sh "@app&&@mod_course" "@app&&@core_course" "@app&&@mod_courses" | ||||
|   - stage: test | ||||
|     name: "End to end tests (others)" | ||||
|     services: | ||||
|     - docker | ||||
|     if: type = cron | ||||
|     script: scripts/test_e2e.sh "@app&&~@mod_forum&&~@mod_messages&&~@mod_course&&~@core_course&&~@mod_courses" | ||||
|  | ||||
| @ -46,6 +46,12 @@ | ||||
|           }, | ||||
|           "configurations": { | ||||
|             "production": { | ||||
|               "fileReplacements": [ | ||||
|                 { | ||||
|                   "replace": "src/testing/testing.module.ts", | ||||
|                   "with": "src/testing/testing.module.prod.ts" | ||||
|                 } | ||||
|               ], | ||||
|               "optimization": { | ||||
|                 "scripts": false, | ||||
|                 "styles": true | ||||
|  | ||||
| @ -71,5 +71,5 @@ gulp.task('watch', () => { | ||||
| }); | ||||
| 
 | ||||
| gulp.task('watch-behat', () => { | ||||
|     gulp.watch(['./src/**/*.feature', './local-moodleappbehat'], { interval: 500 }, gulp.parallel('behat')); | ||||
|     gulp.watch(['./src/**/*.feature', './local_moodleappbehat'], { interval: 500 }, gulp.parallel('behat')); | ||||
| }); | ||||
|  | ||||
| @ -96,9 +96,8 @@ class behat_app extends behat_app_helper { | ||||
|     public function i_wait_the_app_to_restart() { | ||||
|         // Wait window to reload.
 | ||||
|         $this->spin(function() { | ||||
|             $result = $this->js("return !window.behat;"); | ||||
| 
 | ||||
|             if (!$result) { | ||||
|             if ($this->runtime_js('hasInitialized()')) { | ||||
|                 // Behat runtime shouldn't be initialized after reload.
 | ||||
|                 throw new DriverException('Window is not reloading properly.'); | ||||
|             } | ||||
| 
 | ||||
| @ -115,25 +114,25 @@ class behat_app extends behat_app_helper { | ||||
|      * @Then /^I should( not)? find (".+")( inside the .+)? in the app$/ | ||||
|      * @param bool $not Whether assert that the element was not found | ||||
|      * @param string $locator Element locator | ||||
|      * @param string $containerName Container name | ||||
|      * @param string $container Container name | ||||
|      */ | ||||
|     public function i_find_in_the_app(bool $not, string $locator, string $containerName = '') { | ||||
|     public function i_find_in_the_app(bool $not, string $locator, string $container = '') { | ||||
|         $locator = $this->parse_element_locator($locator); | ||||
|         if (!empty($containerName)) { | ||||
|             preg_match('/^ inside the (.+)$/', $containerName, $matches); | ||||
|             $containerName = $matches[1]; | ||||
|         if (!empty($container)) { | ||||
|             preg_match('/^ inside the (.+)$/', $container, $matches); | ||||
|             $container = $matches[1]; | ||||
|         } | ||||
|         $containerName = json_encode($containerName); | ||||
|         $options = json_encode(['containerName' => $container]); | ||||
| 
 | ||||
|         $this->spin(function() use ($not, $locator, $containerName) { | ||||
|             $result = $this->js("return window.behat.find($locator, $containerName);"); | ||||
|         $this->spin(function() use ($not, $locator, $options) { | ||||
|             $result = $this->runtime_js("find($locator, $options)"); | ||||
| 
 | ||||
|             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; | ||||
| @ -152,10 +151,10 @@ class behat_app extends behat_app_helper { | ||||
|         $locator = $this->parse_element_locator($locator); | ||||
| 
 | ||||
|         $this->spin(function() use ($locator) { | ||||
|             $result = $this->js("return window.behat.scrollTo($locator);"); | ||||
|             $result = $this->runtime_js("scrollTo($locator)"); | ||||
| 
 | ||||
|             if ($result !== 'OK') { | ||||
|                 throw new DriverException('Error finding item - ' . $result); | ||||
|                 throw new DriverException('Error finding element - ' . $result); | ||||
|             } | ||||
| 
 | ||||
|             return true; | ||||
| @ -175,7 +174,7 @@ class behat_app extends behat_app_helper { | ||||
|      */ | ||||
|     public function i_load_more_items_in_the_app(bool $not = false) { | ||||
|         $this->spin(function() use ($not) { | ||||
|             $result = $this->js('return await window.behat.loadMoreItems();'); | ||||
|             $result = $this->runtime_js('loadMoreItems()'); | ||||
| 
 | ||||
|             if ($not && $result !== 'ERROR: All items are already loaded.') { | ||||
|                 throw new DriverException('It should not have been possible to load more items'); | ||||
| @ -200,7 +199,7 @@ class behat_app extends behat_app_helper { | ||||
|     public function i_swipe_in_the_app(string $direction) { | ||||
|         $method = 'swipe' . ucwords($direction); | ||||
| 
 | ||||
|         $this->js("window.behat.getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); | ||||
|         $this->runtime_js("getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); | ||||
| 
 | ||||
|         $this->wait_for_pending_js(); | ||||
| 
 | ||||
| @ -219,21 +218,21 @@ class behat_app extends behat_app_helper { | ||||
|         $locator = $this->parse_element_locator($locator); | ||||
| 
 | ||||
|         $this->spin(function() use ($locator, $not) { | ||||
|             $result = $this->js("return window.behat.isSelected($locator);"); | ||||
|             $result = $this->runtime_js("isSelected($locator)"); | ||||
| 
 | ||||
|             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; | ||||
| @ -326,7 +325,7 @@ class behat_app extends behat_app_helper { | ||||
|             $this->login($username); | ||||
|         } | ||||
| 
 | ||||
|         $mycoursesfound = $this->js("return window.behat.find({ text: 'My courses', selector: 'ion-tab-button'});"); | ||||
|         $mycoursesfound = $this->runtime_js("find({ text: 'My courses', selector: 'ion-tab-button'})"); | ||||
| 
 | ||||
|         if ($mycoursesfound !== 'OK') { | ||||
|             // My courses not present enter from Dashboard.
 | ||||
| @ -382,7 +381,7 @@ class behat_app extends behat_app_helper { | ||||
|      */ | ||||
|     public function i_press_the_standard_button_in_the_app(string $button) { | ||||
|         $this->spin(function() use ($button) { | ||||
|             $result = $this->js("return await window.behat.pressStandard('$button');"); | ||||
|             $result = $this->runtime_js("pressStandard('$button')"); | ||||
| 
 | ||||
|             if ($result !== 'OK') { | ||||
|                 throw new DriverException('Error pressing standard button - ' . $result); | ||||
| @ -420,7 +419,7 @@ class behat_app extends behat_app_helper { | ||||
|             ], | ||||
|         ]); | ||||
| 
 | ||||
|         $this->js("window.behat.notificationClicked($notification)"); | ||||
|         $this->zone_js("pushNotifications.notificationClicked($notification)", true); | ||||
|         $this->wait_for_pending_js(); | ||||
|     } | ||||
| 
 | ||||
| @ -508,7 +507,7 @@ class behat_app extends behat_app_helper { | ||||
|      */ | ||||
|     public function i_close_the_popup_in_the_app() { | ||||
|         $this->spin(function()  { | ||||
|             $result = $this->js("return window.behat.closePopup();"); | ||||
|             $result = $this->runtime_js('closePopup()'); | ||||
| 
 | ||||
|             if ($result !== 'OK') { | ||||
|                 throw new DriverException('Error closing popup - ' . $result); | ||||
| @ -536,7 +535,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 | ||||
| @ -546,7 +545,7 @@ class behat_app extends behat_app_helper { | ||||
|         $locator = $this->parse_element_locator($locator); | ||||
| 
 | ||||
|         $this->spin(function() use ($locator) { | ||||
|             $result = $this->js("return await window.behat.press($locator);"); | ||||
|             $result = $this->runtime_js("press($locator)"); | ||||
| 
 | ||||
|             if ($result !== 'OK') { | ||||
|                 throw new DriverException('Error pressing item - ' . $result); | ||||
| @ -578,6 +577,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->runtime_js("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. | ||||
|      * | ||||
| @ -596,23 +622,23 @@ class behat_app extends behat_app_helper { | ||||
| 
 | ||||
|         $this->spin(function() use ($selectedtext, $selected, $locator) { | ||||
|             // Don't do anything if the item is already in the expected state.
 | ||||
|             $result = $this->js("return window.behat.isSelected($locator);"); | ||||
|             $result = $this->runtime_js("isSelected($locator)"); | ||||
| 
 | ||||
|             if ($result === $selected) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             // Press item.
 | ||||
|             $result = $this->js("return await window.behat.press($locator);"); | ||||
|             // Press element.
 | ||||
|             $result = $this->runtime_js("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.
 | ||||
|             $this->wait_for_pending_js(); | ||||
| 
 | ||||
|             $result = $this->js("return window.behat.isSelected($locator);"); | ||||
|             $result = $this->runtime_js("isSelected($locator)"); | ||||
| 
 | ||||
|             switch ($result) { | ||||
|                 case 'YES': | ||||
| @ -646,7 +672,7 @@ class behat_app extends behat_app_helper { | ||||
|         $value = addslashes_js($value); | ||||
| 
 | ||||
|         $this->spin(function() use ($field, $value) { | ||||
|             $result = $this->js("return await window.behat.setField(\"$field\", \"$value\");");
 | ||||
|             $result = $this->runtime_js("setField('$field', '$value')"); | ||||
| 
 | ||||
|             if ($result !== 'OK') { | ||||
|                 throw new DriverException('Error setting field - ' . $result); | ||||
| @ -685,7 +711,7 @@ class behat_app extends behat_app_helper { | ||||
|      */ | ||||
|     public function the_header_should_be_in_the_app(string $text) { | ||||
|         $this->spin(function() use ($text) { | ||||
|             $result = $this->js('return window.behat.getHeader();'); | ||||
|             $result = $this->runtime_js('getHeader()'); | ||||
| 
 | ||||
|             if (substr($result, 0, 3) !== 'OK:') { | ||||
|                 throw new DriverException('Error getting header - ' . $result); | ||||
| @ -766,7 +792,7 @@ class behat_app extends behat_app_helper { | ||||
|      * @When I run cron tasks in the app | ||||
|      */ | ||||
|     public function i_run_cron_tasks_in_the_app() { | ||||
|         $this->js('await window.behat.forceSyncExecution()'); | ||||
|         $this->zone_js('cronDelegate.forceSyncExecution()'); | ||||
|         $this->wait_for_pending_js(); | ||||
|     } | ||||
| 
 | ||||
| @ -776,7 +802,7 @@ class behat_app extends behat_app_helper { | ||||
|      * @When I wait loading to finish in the app | ||||
|      */ | ||||
|     public function i_wait_loading_to_finish_in_the_app() { | ||||
|         $this->js('await window.behat.waitLoadingToFinish()'); | ||||
|         $this->runtime_js('waitLoadingToFinish()'); | ||||
|         $this->wait_for_pending_js(); | ||||
|     } | ||||
| 
 | ||||
| @ -798,7 +824,7 @@ class behat_app extends behat_app_helper { | ||||
|             $this->getSession()->switchToWindow($names[1]); | ||||
|         } | ||||
| 
 | ||||
|         $this->js('window.close();'); | ||||
|         $this->js('window.close()'); | ||||
|         $this->getSession()->switchToWindow($names[0]); | ||||
|     } | ||||
| 
 | ||||
| @ -810,7 +836,7 @@ class behat_app extends behat_app_helper { | ||||
|      * @throws DriverException If the navigator.online mode is not available | ||||
|      */ | ||||
|     public function i_switch_offline_mode(string $offline) { | ||||
|         $this->js("window.behat.network.setForceOffline($offline);"); | ||||
|         $this->runtime_js("network.setForceOffline($offline)"); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -313,12 +313,12 @@ class behat_app_helper extends behat_base { | ||||
| 
 | ||||
|         try { | ||||
|             // Init Behat JavaScript runtime.
 | ||||
|             $initoptions = json_encode([ | ||||
|                 'skipOnBoarding' => $options['skiponboarding'] ?? true, | ||||
|                 'configOverrides' => $this->appconfig, | ||||
|             ]); | ||||
| 
 | ||||
|             $initOptions = new StdClass(); | ||||
|             $initOptions->skipOnBoarding = $options['skiponboarding'] ?? true; | ||||
|             $initOptions->configOverrides = $this->appconfig; | ||||
| 
 | ||||
|             $this->js('window.behatInit(' . json_encode($initOptions) . ');'); | ||||
|             $this->runtime_js("init($initoptions)"); | ||||
|         } catch (Exception $error) { | ||||
|             throw new DriverException('Moodle App not running or not running on Automated mode.'); | ||||
|         } | ||||
| @ -456,7 +456,7 @@ class behat_app_helper extends behat_base { | ||||
| 
 | ||||
|         $res = $this->evaluate_script("Promise.resolve($script)
 | ||||
|             .then(result => window.$promisevariable = result) | ||||
|             .catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message);");
 | ||||
|             .catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message)");
 | ||||
| 
 | ||||
|         do { | ||||
|             if (microtime(true) - $start > $timeout) { | ||||
| @ -465,15 +465,42 @@ class behat_app_helper extends behat_base { | ||||
| 
 | ||||
|             // 0.1 seconds.
 | ||||
|             usleep(100000); | ||||
|         } while (!$this->evaluate_script("return '$promisevariable' in window;")); | ||||
|         } while (!$this->evaluate_script("'$promisevariable' in window")); | ||||
| 
 | ||||
|         $result = $this->evaluate_script("return window.$promisevariable;"); | ||||
|         $result = $this->evaluate_script("window.$promisevariable"); | ||||
| 
 | ||||
|         $this->evaluate_script("delete window.$promisevariable;"); | ||||
|         $this->evaluate_script("delete window.$promisevariable"); | ||||
| 
 | ||||
|         if (is_string($result) && strrpos($result, 'Async code rejected:') === 0) { | ||||
|             throw new DriverException($result); | ||||
|         } | ||||
| 
 | ||||
|         return $result; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Evaluate and execute methods from the Behat runtime. | ||||
|      * | ||||
|      * @param string $script | ||||
|      * @return mixed Result. | ||||
|      */ | ||||
|     protected function runtime_js(string $script) { | ||||
|         return $this->js("window.behat.$script"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Evaluate and execute methods from the Behat runtime inside the Angular zone. | ||||
|      * | ||||
|      * @param string $script | ||||
|      * @param bool $blocking | ||||
|      * @return mixed Result. | ||||
|      */ | ||||
|     protected function zone_js(string $script, bool $blocking = false) { | ||||
|         $blockingjson = json_encode($blocking); | ||||
| 
 | ||||
|         return $this->runtime_js("runInZone(() => window.behat.$script, $blockingjson)"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens a custom URL for automatic login and redirect from the Moodle App (and waits to finish.) | ||||
|      * | ||||
| @ -513,9 +540,10 @@ class behat_app_helper extends behat_base { | ||||
| 
 | ||||
|         // Generate custom URL.
 | ||||
|         $parsed_url = parse_url($CFG->behat_wwwroot); | ||||
|         $domain = $parsed_url['host'] ?? ''; | ||||
|         $rootpath = $parsed_url['path'] ?? ''; | ||||
|         $url = $this->get_mobile_url_scheme() . "://$username@$domain$rootpath?token=$token&privatetoken=$privatetoken"; | ||||
|         $site = $parsed_url['host'] ?? ''; | ||||
|         $site .= isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; | ||||
|         $site .= $parsed_url['path'] ?? ''; | ||||
|         $url = $this->get_mobile_url_scheme() . "://$username@$site?token=$token&privatetoken=$privatetoken"; | ||||
| 
 | ||||
|         if (!empty($path)) { | ||||
|             $url .= '&redirect='.urlencode($CFG->behat_wwwroot.$path); | ||||
| @ -548,8 +576,7 @@ class behat_app_helper extends behat_base { | ||||
|      * @param string $successXPath The XPath of the element to lookat after navigation. | ||||
|      */ | ||||
|     protected function handle_url(string $customurl, string $successXPath = '') { | ||||
|         // Instead of using evaluate_async_script, we wait for the path to load.
 | ||||
|         $result = $this->js("return await window.behat.handleCustomURL('$customurl');"); | ||||
|         $result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')"); | ||||
| 
 | ||||
|         if ($result !== 'OK') { | ||||
|             throw new DriverException('Error handling url - ' . $result); | ||||
| @ -178,7 +178,7 @@ class performance_measure implements behat_app_listener { | ||||
|      * @return int Current time in milliseconds. | ||||
|      */ | ||||
|     private function now(): int { | ||||
|         return $this->driver->evaluateScript('Date.now();'); | ||||
|         return $this->driver->evaluateScript('Date.now()'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
							
								
								
									
										255
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										255
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -229,12 +229,31 @@ | ||||
|             "uri-js": "^4.2.2" | ||||
|           } | ||||
|         }, | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "core-js": { | ||||
|           "version": "3.6.4", | ||||
|           "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", | ||||
|           "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.0.4", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", | ||||
|           "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         }, | ||||
|         "open": { | ||||
|           "version": "7.0.4", | ||||
|           "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz", | ||||
| @ -3889,6 +3908,16 @@ | ||||
|         "strip-json-comments": "^3.1.1" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "debug": { | ||||
|           "version": "4.3.1", | ||||
|           "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", | ||||
| @ -3913,6 +3942,15 @@ | ||||
|           "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|           "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         }, | ||||
|         "ms": { | ||||
|           "version": "2.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", | ||||
| @ -11772,12 +11810,12 @@ | ||||
|       } | ||||
|     }, | ||||
|     "brace-expansion": { | ||||
|       "version": "1.1.11", | ||||
|       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|       "version": "2.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", | ||||
|       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "balanced-match": "^1.0.0", | ||||
|         "concat-map": "0.0.1" | ||||
|         "balanced-match": "^1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "braces": { | ||||
| @ -12327,6 +12365,16 @@ | ||||
|           "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "chalk": { | ||||
|           "version": "2.4.2", | ||||
|           "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", | ||||
| @ -12447,6 +12495,17 @@ | ||||
|             "table": "^5.2.3", | ||||
|             "text-table": "^0.2.0", | ||||
|             "v8-compile-cache": "^2.0.3" | ||||
|           }, | ||||
|           "dependencies": { | ||||
|             "minimatch": { | ||||
|               "version": "3.1.2", | ||||
|               "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|               "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|               "dev": true, | ||||
|               "requires": { | ||||
|                 "brace-expansion": "^1.1.7" | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "eslint-scope": { | ||||
| @ -13433,7 +13492,7 @@ | ||||
|     "concat-map": { | ||||
|       "version": "0.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", | ||||
|       "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" | ||||
|       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" | ||||
|     }, | ||||
|     "concat-stream": { | ||||
|       "version": "1.6.2", | ||||
| @ -16449,6 +16508,16 @@ | ||||
|           "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "chalk": { | ||||
|           "version": "4.1.1", | ||||
|           "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", | ||||
| @ -16493,6 +16562,15 @@ | ||||
|           "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|           "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         }, | ||||
|         "ms": { | ||||
|           "version": "2.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", | ||||
| @ -16625,6 +16703,16 @@ | ||||
|         "tsconfig-paths": "^3.9.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "doctrine": { | ||||
|           "version": "1.5.0", | ||||
|           "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", | ||||
| @ -16634,6 +16722,15 @@ | ||||
|             "esutils": "^2.0.2", | ||||
|             "isarray": "^1.0.0" | ||||
|           } | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|           "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| @ -17635,6 +17732,16 @@ | ||||
|             "color-convert": "^1.9.0" | ||||
|           } | ||||
|         }, | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "braces": { | ||||
|           "version": "2.3.2", | ||||
|           "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", | ||||
| @ -17766,6 +17873,15 @@ | ||||
|             "to-regex": "^3.0.2" | ||||
|           } | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|           "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         }, | ||||
|         "semver": { | ||||
|           "version": "5.7.1", | ||||
|           "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", | ||||
| @ -18326,6 +18442,25 @@ | ||||
|         "minimatch": "^3.0.4", | ||||
|         "once": "^1.3.0", | ||||
|         "path-is-absolute": "^1.0.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|           "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "glob-base": { | ||||
| @ -19962,6 +20097,25 @@ | ||||
|       "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", | ||||
|       "requires": { | ||||
|         "minimatch": "^3.0.4" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|           "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "image-size": { | ||||
| @ -20959,6 +21113,16 @@ | ||||
|           "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "chalk": { | ||||
|           "version": "4.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", | ||||
| @ -20968,6 +21132,15 @@ | ||||
|             "ansi-styles": "^4.1.0", | ||||
|             "supports-color": "^7.1.0" | ||||
|           } | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|           "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| @ -23349,11 +23522,12 @@ | ||||
|       "dev": true | ||||
|     }, | ||||
|     "minimatch": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", | ||||
|       "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", | ||||
|       "version": "5.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", | ||||
|       "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "brace-expansion": "^1.1.7" | ||||
|         "brace-expansion": "^2.0.1" | ||||
|       } | ||||
|     }, | ||||
|     "minimist": { | ||||
| @ -25138,6 +25312,16 @@ | ||||
|         "which": "^1.3.1" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "cacache": { | ||||
|           "version": "12.0.4", | ||||
|           "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", | ||||
| @ -25200,6 +25384,15 @@ | ||||
|             "yallist": "^3.0.2" | ||||
|           } | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|           "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         }, | ||||
|         "minipass": { | ||||
|           "version": "2.9.0", | ||||
|           "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", | ||||
| @ -28127,6 +28320,27 @@ | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "minimatch": "3.0.4" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.0.4", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", | ||||
|           "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "reflect-metadata": { | ||||
| @ -31856,6 +32070,27 @@ | ||||
|         "@istanbuljs/schema": "^0.1.2", | ||||
|         "glob": "^7.1.4", | ||||
|         "minimatch": "^3.0.4" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "brace-expansion": { | ||||
|           "version": "1.1.11", | ||||
|           "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
|           "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "balanced-match": "^1.0.0", | ||||
|             "concat-map": "0.0.1" | ||||
|           } | ||||
|         }, | ||||
|         "minimatch": { | ||||
|           "version": "3.1.2", | ||||
|           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", | ||||
|           "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "brace-expansion": "^1.1.7" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "text-table": { | ||||
|  | ||||
| @ -172,6 +172,7 @@ | ||||
|     "jest": "^26.5.2", | ||||
|     "jest-preset-angular": "^8.3.1", | ||||
|     "jsonc-parser": "^2.3.1", | ||||
|     "minimatch": "^5.1.0", | ||||
|     "native-run": "^1.4.0", | ||||
|     "patch-package": "^6.4.7", | ||||
|     "terser-webpack-plugin": "^4.2.3", | ||||
|  | ||||
| @ -14,6 +14,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| const minimatch = require('minimatch'); | ||||
| const { existsSync, readFileSync, writeFileSync, statSync, renameSync, rmSync } = require('fs'); | ||||
| const { readdir } = require('fs').promises; | ||||
| const { mkdirSync, copySync } = require('fs-extra'); | ||||
| @ -21,12 +22,22 @@ const { resolve, extname, dirname, basename, relative } = require('path'); | ||||
| 
 | ||||
| async function main() { | ||||
|     const pluginPath = process.argv[2] || guessPluginPath() || fail('Folder argument missing!'); | ||||
|     const excludeFeatures = process.argv.some(arg => arg === '--exclude-features'); | ||||
|     const exclusions = excludeFeatures | ||||
|         ? [ | ||||
|             '*.feature', | ||||
|             '**/js/mobile/index.js', | ||||
|             '**/db/mobile.php', | ||||
|             '**/classes/output/mobile.php', | ||||
|         ] | ||||
|         : []; | ||||
| 
 | ||||
|     if (!existsSync(pluginPath)) { | ||||
|         mkdirSync(pluginPath); | ||||
|     } else { | ||||
|         // Empty directory, except the excluding list.
 | ||||
|         const excludeFromErase = [ | ||||
|             ...exclusions, | ||||
|             '.git', | ||||
|             '.gitignore', | ||||
|             'README.md', | ||||
| @ -34,7 +45,7 @@ async function main() { | ||||
| 
 | ||||
|         const files = await readdir(pluginPath, { withFileTypes: true }); | ||||
|         for (const file of files) { | ||||
|             if (excludeFromErase.indexOf(file.name) >= 0) { | ||||
|             if (isExcluded(file.name, excludeFromErase)) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
| @ -43,13 +54,17 @@ async function main() { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     // Copy plugin template.
 | ||||
|     const { version: appVersion } = require(projectPath('package.json')); | ||||
|     const templatePath = projectPath('local-moodleappbehat'); | ||||
|     const templatePath = projectPath('local_moodleappbehat'); | ||||
| 
 | ||||
|     for await (const file of getDirectoryFiles(templatePath)) { | ||||
|         if (isExcluded(file, exclusions)) { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|     copySync(templatePath, pluginPath); | ||||
|         copySync(file, file.replace(templatePath, pluginPath)); | ||||
|     } | ||||
| 
 | ||||
|     // Update version.php
 | ||||
|     const pluginFilePath = pluginPath + '/version.php'; | ||||
| @ -62,6 +77,7 @@ async function main() { | ||||
|     writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements)); | ||||
| 
 | ||||
|     // Copy feature files.
 | ||||
|     if (!excludeFeatures) { | ||||
|         const behatTempFeaturesPath = `${pluginPath}/behat-tmp`; | ||||
|         copySync(projectPath('src'), behatTempFeaturesPath, { filter: isFeatureFileOrDirectory }); | ||||
| 
 | ||||
| @ -84,6 +100,7 @@ async function main() { | ||||
|         } | ||||
| 
 | ||||
|         rmSync(behatTempFeaturesPath, {recursive: true}); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function isFeatureFileOrDirectory(src) { | ||||
| @ -92,6 +109,10 @@ function isFeatureFileOrDirectory(src) { | ||||
|     return stats.isDirectory() || extname(src) === '.feature'; | ||||
| } | ||||
| 
 | ||||
| function isExcluded(file, exclusions) { | ||||
|     return exclusions.some(exclusion => minimatch(file, exclusion)); | ||||
| } | ||||
| 
 | ||||
| function fail(message) { | ||||
|     console.error(message); | ||||
|     process.exit(1); | ||||
|  | ||||
| @ -1,50 +0,0 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| source "scripts/functions.sh" | ||||
| 
 | ||||
| # Prepare variables | ||||
| basedir="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../" && pwd )" | ||||
| dockerscripts="$HOME/moodle-docker/bin/" | ||||
| dockercompose="$dockerscripts/moodle-docker-compose" | ||||
| 
 | ||||
| export MOODLE_DOCKER_DB=pgsql | ||||
| export MOODLE_DOCKER_BROWSER=chrome | ||||
| export MOODLE_DOCKER_WWWROOT="$HOME/moodle" | ||||
| export MOODLE_DOCKER_PHP_VERSION=7.4 | ||||
| export MOODLE_DOCKER_APP_PATH=$basedir | ||||
| 
 | ||||
| # Prepare dependencies | ||||
| print_title "Preparing dependencies" | ||||
| git clone --branch master --depth 1 git://github.com/moodle/moodle $HOME/moodle | ||||
| git clone --branch ionic5 --depth 1 git://github.com/moodlehq/moodle-local_moodlemobileapp $HOME/moodle/local/moodlemobileapp | ||||
| 
 | ||||
| # TODO replace for moodlehq/moodle-docker after merging https://github.com/moodlehq/moodle-docker/pull/156 | ||||
| git clone --branch MOBILE-3738 --depth 1 git://github.com/NoelDeMartin/moodle-docker $HOME/moodle-docker | ||||
| 
 | ||||
| cp $HOME/moodle-docker/config.docker-template.php $HOME/moodle/config.php | ||||
| 
 | ||||
| # Build app | ||||
| print_title "Building app" | ||||
| npm ci | ||||
| 
 | ||||
| # Start containers | ||||
| print_title "Starting containers" | ||||
| $dockercompose pull | ||||
| $dockercompose up -d | ||||
| $dockerscripts/moodle-docker-wait-for-db | ||||
| $dockerscripts/moodle-docker-wait-for-app | ||||
| 
 | ||||
| $dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/init.php" | ||||
| notify_on_error_exit "e2e failed initializing behat" | ||||
| 
 | ||||
| print_title "Running e2e tests" | ||||
| 
 | ||||
| # Run tests | ||||
| for tags in "$@" | ||||
| do | ||||
|     $dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags=\"$tags\" --auto-rerun" | ||||
|     notify_on_error_exit "Some e2e tests are failing, please review" | ||||
| done | ||||
| 
 | ||||
| # Clean up | ||||
| $dockercompose down | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AfterViewInit, ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; | ||||
| import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; | ||||
| import { IonRouterOutlet } from '@ionic/angular'; | ||||
| import { BackButtonEvent, ScrollDetail } from '@ionic/core'; | ||||
| 
 | ||||
| @ -21,7 +21,7 @@ import { CoreLoginHelper } from '@features/login/services/login-helper'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { NgZone, SplashScreen, Translate } from '@singletons'; | ||||
| import { CoreNetwork } from '@services/network'; | ||||
| import { CoreApp, CoreAppProvider } from '@services/app'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSubscriptions } from '@singletons/subscriptions'; | ||||
| @ -38,10 +38,6 @@ import { CorePlatform } from '@services/platform'; | ||||
| const MOODLE_VERSION_PREFIX = 'version-'; | ||||
| const MOODLEAPP_VERSION_PREFIX = 'moodleapp-'; | ||||
| 
 | ||||
| type AutomatedTestsWindow = Window & { | ||||
|     changeDetector?: ChangeDetectorRef; | ||||
| }; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'app-root', | ||||
|     templateUrl: 'app.component.html', | ||||
| @ -54,12 +50,6 @@ export class AppComponent implements OnInit, AfterViewInit { | ||||
|     protected lastUrls: Record<string, number> = {}; | ||||
|     protected lastInAppUrl?: string; | ||||
| 
 | ||||
|     constructor(changeDetector: ChangeDetectorRef) { | ||||
|         if (CoreAppProvider.isAutomated()) { | ||||
|             (window as AutomatedTestsWindow).changeDetector = changeDetector; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      * | ||||
|  | ||||
| @ -32,7 +32,7 @@ import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; | ||||
| import { CoreCronDelegate } from '@services/cron'; | ||||
| import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron'; | ||||
| import { moodleTransitionAnimation } from '@classes/page-transition'; | ||||
| import { BehatTestingModule } from '@/testing/behat-testing.module'; | ||||
| import { TestingModule } from '@/testing/testing.module'; | ||||
| 
 | ||||
| // For translate loader. AoT requires an exported function for factories.
 | ||||
| export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { | ||||
| @ -60,7 +60,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { | ||||
|         AppRoutingModule, | ||||
|         CoreModule, | ||||
|         AddonsModule, | ||||
|         BehatTestingModule, | ||||
|         TestingModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, | ||||
|  | ||||
| @ -220,22 +220,9 @@ Feature: Test basic usage of comments in app | ||||
| 
 | ||||
|   Scenario: Add comments & Delete comments (blogs) | ||||
|     # Create blog as a teacher | ||||
|     Given the following "blocks" exist: | ||||
|       | blockname        | contextlevel | reference | pagetypepattern | defaultregion | configdata | | ||||
|       | blog_menu        | Course       | C1        | course-view-*   | site-pre      |            | | ||||
|     And I entered the course "Course 1" as "teacher1" in the app | ||||
|     And I press "Course summary" in the app | ||||
|     # TODO Create and use a generator blog entries. | ||||
|     And I press "Open in browser" in the app | ||||
|     And I switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
|     And I click on "Open block drawer" "button" | ||||
|     And I click on "Add an entry about this course" "link" in the "Blog menu" "block" | ||||
|     And I set the following fields to these values: | ||||
|       | Entry title | Blog test | | ||||
|       | Blog entry body | Blog body | | ||||
|     And I press "Save changes" | ||||
|     And I close the browser tab opened by the app | ||||
|     Given the following "core_blog > entries" exist: | ||||
|       | subject   | body      | user     | | ||||
|       | Blog test | Blog body | teacher1 | | ||||
| 
 | ||||
|     # Create and delete comments as a student | ||||
|     When I entered the app as "student1" | ||||
| @ -263,21 +250,9 @@ Feature: Test basic usage of comments in app | ||||
| 
 | ||||
|   Scenario: Add comments offline & Delete comments offline & Sync comments (blogs) | ||||
|     # Create blog as a teacher | ||||
|     Given the following "blocks" exist: | ||||
|       | blockname        | contextlevel | reference | pagetypepattern | defaultregion | configdata | | ||||
|       | blog_menu        | Course       | C1        | course-view-*   | site-pre      |            | | ||||
|     And I entered the course "Course 1" as "teacher1" in the app | ||||
|     And I press "Course summary" in the app | ||||
|     And I press "Open in browser" in the app | ||||
|     And I switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
|     And I click on "Open block drawer" "button" | ||||
|     And I click on "Add an entry about this course" "link" in the "Blog menu" "block" | ||||
|     And I set the following fields to these values: | ||||
|       | Entry title | Blog test | | ||||
|       | Blog entry body | Blog body | | ||||
|     And I press "Save changes" | ||||
|     And I close the browser tab opened by the app | ||||
|     Given the following "core_blog > entries" exist: | ||||
|       | subject   | body      | user     | | ||||
|       | Blog test | Blog body | teacher1 | | ||||
| 
 | ||||
|     # Create and delete comments as a student | ||||
|     When I entered the app as "student1" | ||||
|  | ||||
| @ -1,34 +0,0 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { CoreAppProvider } from '@services/app'; | ||||
| import { TestsBehatBlockingService } from './services/behat-blocking'; | ||||
| import { BehatTestsWindow, TestsBehatRuntime } from './services/behat-runtime'; | ||||
| 
 | ||||
| function initializeBehatTestsWindow(window: BehatTestsWindow) { | ||||
|     // Make functions publicly available for Behat to call.
 | ||||
|     window.behatInit = TestsBehatRuntime.init; | ||||
| } | ||||
| 
 | ||||
| @NgModule({ | ||||
|     providers: | ||||
|         CoreAppProvider.isAutomated() | ||||
|             ? [ | ||||
|                 { provide: APP_INITIALIZER, multi: true, useValue: () => initializeBehatTestsWindow(window) }, | ||||
|                 TestsBehatBlockingService, | ||||
|             ] | ||||
|             : [], | ||||
| }) | ||||
| export class BehatTestingModule {} | ||||
| @ -15,13 +15,13 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { makeSingleton, NgZone } from '@singletons'; | ||||
| import { BehatTestsWindow, TestsBehatRuntime } from './behat-runtime'; | ||||
| import { BehatTestsWindow, TestingBehatRuntime } from './behat-runtime'; | ||||
| 
 | ||||
| /** | ||||
|  * Behat block JS manager. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class TestsBehatBlockingService { | ||||
| export class TestingBehatBlockingService { | ||||
| 
 | ||||
|     protected waitingBlocked = false; | ||||
|     protected recentMutation = false; | ||||
| @ -48,7 +48,7 @@ export class TestsBehatBlockingService { | ||||
|         win.M.util = win.M.util ?? {}; | ||||
|         win.M.util.pending_js = win.M.util.pending_js ?? []; | ||||
| 
 | ||||
|         TestsBehatRuntime.log('Initialized!'); | ||||
|         TestingBehatRuntime.log('Initialized!'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -90,7 +90,7 @@ export class TestsBehatBlockingService { | ||||
|         } | ||||
|         this.pendingList.push(key); | ||||
| 
 | ||||
|         TestsBehatRuntime.log('PENDING+: ' + this.pendingList); | ||||
|         TestingBehatRuntime.log('PENDING+: ' + this.pendingList); | ||||
| 
 | ||||
|         return key; | ||||
|     } | ||||
| @ -105,7 +105,7 @@ export class TestsBehatBlockingService { | ||||
|         // Remove the key immediately.
 | ||||
|         this.pendingList = this.pendingList.filter((x) => x !== key); | ||||
| 
 | ||||
|         TestsBehatRuntime.log('PENDING-: ' + this.pendingList); | ||||
|         TestingBehatRuntime.log('PENDING-: ' + this.pendingList); | ||||
| 
 | ||||
|         // If the only thing left is DELAY, then remove that as well, later...
 | ||||
|         if (this.pendingList.length === 1) { | ||||
| @ -124,7 +124,7 @@ export class TestsBehatBlockingService { | ||||
|             // Only remove it if the pending array is STILL empty after all that.
 | ||||
|             if (this.pendingList.length === 1) { | ||||
|                 this.pendingList = []; | ||||
|                 TestsBehatRuntime.log('PENDING-: ' + this.pendingList); | ||||
|                 TestingBehatRuntime.log('PENDING-: ' + this.pendingList); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @ -221,16 +221,16 @@ export class TestsBehatBlockingService { | ||||
| 
 | ||||
|                 try { | ||||
|                 // Add to the list of pending requests.
 | ||||
|                     TestsBehatBlocking.block(key); | ||||
|                     TestingBehatBlocking.block(key); | ||||
| 
 | ||||
|                     // Detect when it finishes and remove it from the list.
 | ||||
|                     this.addEventListener('loadend', () => { | ||||
|                         TestsBehatBlocking.unblock(key); | ||||
|                         TestingBehatBlocking.unblock(key); | ||||
|                     }); | ||||
| 
 | ||||
|                     return realOpen.apply(this, args); | ||||
|                 } catch (error) { | ||||
|                     TestsBehatBlocking.unblock(key); | ||||
|                     TestingBehatBlocking.unblock(key); | ||||
|                     throw error; | ||||
|                 } | ||||
|             }); | ||||
| @ -239,4 +239,4 @@ export class TestsBehatBlockingService { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const TestsBehatBlocking = makeSingleton(TestsBehatBlockingService); | ||||
| export const TestingBehatBlocking = makeSingleton(TestingBehatBlockingService); | ||||
|  | ||||
| @ -12,10 +12,11 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CorePromisedValue } from '@classes/promised-value'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { NgZone } from '@singletons'; | ||||
| import { TestBehatElementLocator } from './behat-runtime'; | ||||
| import { makeSingleton, NgZone } from '@singletons'; | ||||
| import { TestingBehatElementLocator, TestingBehatFindOptions } 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']; | ||||
| @ -23,7 +24,8 @@ const blockingContainers = ['ION-ALERT', 'ION-POPOVER', 'ION-ACTION-SHEET', 'COR | ||||
| /** | ||||
|  * Behat Dom Utils helper functions. | ||||
|  */ | ||||
| export class TestsBehatDomUtils { | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class TestingBehatDomUtilsService { | ||||
| 
 | ||||
|     /** | ||||
|      * Check if an element is visible. | ||||
| @ -32,7 +34,7 @@ export class TestsBehatDomUtils { | ||||
|      * @param container Container. | ||||
|      * @return Whether the element is visible or not. | ||||
|      */ | ||||
|     static isElementVisible(element: HTMLElement, container: HTMLElement): boolean { | ||||
|     isElementVisible(element: HTMLElement, container: HTMLElement): boolean { | ||||
|         if (element.getAttribute('aria-hidden') === 'true' || getComputedStyle(element).display === 'none') { | ||||
|             return false; | ||||
|         } | ||||
| @ -56,7 +58,7 @@ export class TestsBehatDomUtils { | ||||
|      * @param container Container. | ||||
|      * @return Whether the element is selected or not. | ||||
|      */ | ||||
|     static isElementSelected(element: HTMLElement, container: HTMLElement): boolean { | ||||
|     isElementSelected(element: HTMLElement, container: HTMLElement): boolean { | ||||
|         const ariaCurrent = element.getAttribute('aria-current'); | ||||
|         if ( | ||||
|             (ariaCurrent && ariaCurrent !== 'false') || | ||||
| @ -79,9 +81,14 @@ export class TestsBehatDomUtils { | ||||
|      * | ||||
|      * @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 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 +104,23 @@ export class TestsBehatDomUtils { | ||||
|             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 +174,7 @@ export class TestsBehatDomUtils { | ||||
|                         continue; | ||||
|                     } | ||||
| 
 | ||||
|                     elements.push(...this.findElementsBasedOnTextWithinWithExact(childNode, text)); | ||||
|                     elements.push(...this.findElementsBasedOnTextWithinWithExact(childNode, text, options)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @ -175,7 +189,7 @@ export class TestsBehatDomUtils { | ||||
|      * @param text Text to check. | ||||
|      * @return If text matches any of the label attributes. | ||||
|      */ | ||||
|     protected static checkElementLabel(element: HTMLElement, text: string): boolean { | ||||
|     protected checkElementLabel(element: HTMLElement, text: string): boolean { | ||||
|         return element.title === text || | ||||
|             element.getAttribute('alt') === text || | ||||
|             element.getAttribute('aria-label') === text || | ||||
| @ -187,10 +201,15 @@ export class TestsBehatDomUtils { | ||||
|      * | ||||
|      * @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 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)); | ||||
| @ -206,7 +225,7 @@ export class TestsBehatDomUtils { | ||||
|      * @param elements Elements list. | ||||
|      * @return Top ancestors. | ||||
|      */ | ||||
|     protected static getTopAncestors(elements: HTMLElement[]): HTMLElement[] { | ||||
|     protected getTopAncestors(elements: HTMLElement[]): HTMLElement[] { | ||||
|         const uniqueElements = new Set(elements); | ||||
| 
 | ||||
|         for (const element of uniqueElements) { | ||||
| @ -230,7 +249,7 @@ export class TestsBehatDomUtils { | ||||
|      * @param element Element. | ||||
|      * @return Parent element. | ||||
|      */ | ||||
|     protected static getParentElement(element: HTMLElement): HTMLElement | null { | ||||
|     protected getParentElement(element: HTMLElement): HTMLElement | null { | ||||
|         return element.parentElement || | ||||
|             (element.getRootNode() && (element.getRootNode() as ShadowRoot).host as HTMLElement) || | ||||
|             null; | ||||
| @ -244,7 +263,7 @@ export class TestsBehatDomUtils { | ||||
|      * @param container Topmost container to search within. | ||||
|      * @return Closest matching element. | ||||
|      */ | ||||
|     protected static getClosestMatching(element: HTMLElement, selector: string, container: HTMLElement | null): HTMLElement | null { | ||||
|     protected getClosestMatching(element: HTMLElement, selector: string, container: HTMLElement | null): HTMLElement | null { | ||||
|         if (element.matches(selector)) { | ||||
|             return element; | ||||
|         } | ||||
| @ -262,7 +281,7 @@ export class TestsBehatDomUtils { | ||||
|      * @param containerName Whether to search inside the a container name. | ||||
|      * @return Found top container elements. | ||||
|      */ | ||||
|     protected static getCurrentTopContainerElements(containerName: string): HTMLElement[] { | ||||
|     protected getCurrentTopContainerElements(containerName: string): HTMLElement[] { | ||||
|         const topContainers: HTMLElement[] = []; | ||||
|         let containers = Array.from(document.querySelectorAll<HTMLElement>([ | ||||
|             'ion-alert.hydrated', | ||||
| @ -325,32 +344,33 @@ export class TestsBehatDomUtils { | ||||
|      * 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: TestBehatElementLocator, containerName = ''): HTMLElement { | ||||
|         return this.findElementsBasedOnText(locator, containerName, true)[0]; | ||||
|     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: TestBehatElementLocator, | ||||
|         containerName = '', | ||||
|         stopWhenFound = false, | ||||
|     protected findElementsBasedOnText( | ||||
|         locator: TestingBehatElementLocator, | ||||
|         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 +383,18 @@ export class TestsBehatDomUtils { | ||||
|      * | ||||
|      * @param locator Element locator. | ||||
|      * @param topContainer Container to search in. | ||||
|      * @param options Search options. | ||||
|      * @return Found elements | ||||
|      */ | ||||
|     protected static findElementsBasedOnTextInContainer( | ||||
|         locator: TestBehatElementLocator, | ||||
|     protected 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 +412,10 @@ export class TestsBehatDomUtils { | ||||
|         } | ||||
| 
 | ||||
|         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 +437,7 @@ export class TestsBehatDomUtils { | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             const elements = this.findElementsBasedOnTextWithin(container, locator.text); | ||||
|             const elements = this.findElementsBasedOnTextWithin(container, locator.text, options); | ||||
| 
 | ||||
|             let filteredElements: HTMLElement[] = elements; | ||||
| 
 | ||||
| @ -442,7 +467,7 @@ export class TestsBehatDomUtils { | ||||
|      * | ||||
|      * @param element Element. | ||||
|      */ | ||||
|     protected static async ensureElementVisible(element: HTMLElement): Promise<DOMRect> { | ||||
|     protected async ensureElementVisible(element: HTMLElement): Promise<DOMRect> { | ||||
|         const initialRect = element.getBoundingClientRect(); | ||||
| 
 | ||||
|         element.scrollIntoView(false); | ||||
| @ -471,7 +496,7 @@ export class TestsBehatDomUtils { | ||||
|      * | ||||
|      * @param element Element to press. | ||||
|      */ | ||||
|     static async pressElement(element: HTMLElement): Promise<void> { | ||||
|     async pressElement(element: HTMLElement): Promise<void> { | ||||
|         await NgZone.run(async () => { | ||||
|             const promise = new CorePromisedValue<void>(); | ||||
| 
 | ||||
| @ -516,7 +541,7 @@ export class TestsBehatDomUtils { | ||||
|      * @param element HTML to set. | ||||
|      * @param value Value to be set. | ||||
|      */ | ||||
|     static async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> { | ||||
|     async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> { | ||||
|         await NgZone.run(async () => { | ||||
|             const promise = new CorePromisedValue<void>(); | ||||
| 
 | ||||
| @ -581,6 +606,8 @@ export class TestsBehatDomUtils { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const TestingBehatDomUtils = makeSingleton(TestingBehatDomUtilsService); | ||||
| 
 | ||||
| type ElementsWithExact = { | ||||
|     element: HTMLElement; | ||||
|     exact: boolean; | ||||
|  | ||||
| @ -12,127 +12,106 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { TestsBehatDomUtils } from './behat-dom'; | ||||
| import { TestsBehatBlocking } from './behat-blocking'; | ||||
| import { CoreCustomURLSchemes } from '@services/urlschemes'; | ||||
| import { TestingBehatDomUtils } from './behat-dom'; | ||||
| import { TestingBehatBlocking } from './behat-blocking'; | ||||
| import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes'; | ||||
| import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; | ||||
| import { CoreConfig } from '@services/config'; | ||||
| import { EnvironmentConfig } from '@/types/config'; | ||||
| import { NgZone } from '@singletons'; | ||||
| import { CoreNetwork } from '@services/network'; | ||||
| import { | ||||
|     CorePushNotifications, | ||||
|     CorePushNotificationsNotificationBasicData, | ||||
| } from '@features/pushnotifications/services/pushnotifications'; | ||||
| import { CoreCronDelegate } from '@services/cron'; | ||||
| import { makeSingleton, NgZone } from '@singletons'; | ||||
| import { CoreNetwork, CoreNetworkService } from '@services/network'; | ||||
| import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications'; | ||||
| import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron'; | ||||
| import { CoreLoadingComponent } from '@components/loading/loading'; | ||||
| import { CoreComponentsRegistry } from '@singletons/components-registry'; | ||||
| import { CoreDom } from '@singletons/dom'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreCoursesDashboardPage } from '@features/courses/pages/dashboard/dashboard'; | ||||
| import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| /** | ||||
|  * Behat runtime servive with public API. | ||||
|  */ | ||||
| export class TestsBehatRuntime { | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class TestingBehatRuntimeService { | ||||
| 
 | ||||
|     protected initialized = false; | ||||
| 
 | ||||
|     get cronDelegate(): CoreCronDelegateService { | ||||
|         return CoreCronDelegate.instance; | ||||
|     } | ||||
| 
 | ||||
|     get customUrlSchemes(): CoreCustomURLSchemesProvider { | ||||
|         return CoreCustomURLSchemes.instance; | ||||
|     } | ||||
| 
 | ||||
|     get network(): CoreNetworkService { | ||||
|         return CoreNetwork.instance; | ||||
|     } | ||||
| 
 | ||||
|     get pushNotifications(): CorePushNotificationsProvider { | ||||
|         return CorePushNotifications.instance; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Init behat functions and set options like skipping onboarding. | ||||
|      * | ||||
|      * @param options Options to set on the app. | ||||
|      */ | ||||
|     static init(options?: TestsBehatInitOptions): void { | ||||
|         TestsBehatBlocking.init(); | ||||
| 
 | ||||
|         (window as BehatTestsWindow).behat = { | ||||
|             closePopup: TestsBehatRuntime.closePopup, | ||||
|             find: TestsBehatRuntime.find, | ||||
|             getAngularInstance: TestsBehatRuntime.getAngularInstance, | ||||
|             getHeader: TestsBehatRuntime.getHeader, | ||||
|             isSelected: TestsBehatRuntime.isSelected, | ||||
|             loadMoreItems: TestsBehatRuntime.loadMoreItems, | ||||
|             log: TestsBehatRuntime.log, | ||||
|             press: TestsBehatRuntime.press, | ||||
|             pressStandard: TestsBehatRuntime.pressStandard, | ||||
|             pullToRefresh: TestsBehatRuntime.pullToRefresh, | ||||
|             scrollTo: TestsBehatRuntime.scrollTo, | ||||
|             setField: TestsBehatRuntime.setField, | ||||
|             handleCustomURL: TestsBehatRuntime.handleCustomURL, | ||||
|             notificationClicked: TestsBehatRuntime.notificationClicked, | ||||
|             forceSyncExecution: TestsBehatRuntime.forceSyncExecution, | ||||
|             waitLoadingToFinish: TestsBehatRuntime.waitLoadingToFinish, | ||||
|             network: CoreNetwork.instance, | ||||
|         }; | ||||
| 
 | ||||
|         if (!options) { | ||||
|     init(options: TestingBehatInitOptions = {}): void { | ||||
|         if (this.initialized) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (options.skipOnBoarding === true) { | ||||
|         this.initialized = true; | ||||
|         TestingBehatBlocking.init(); | ||||
| 
 | ||||
|         if (options.skipOnBoarding) { | ||||
|             CoreConfig.set(CoreLoginHelperProvider.ONBOARDING_DONE, 1); | ||||
|         } | ||||
| 
 | ||||
|         if (options.configOverrides) { | ||||
|             // Set the cookie so it's maintained between reloads.
 | ||||
|             document.cookie = 'MoodleAppConfig=' + JSON.stringify(options.configOverrides); | ||||
|             CoreConfig.patchEnvironment(options.configOverrides); | ||||
|             CoreConfig.patchEnvironment(options.configOverrides, { patchDefault: true }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles a custom URL. | ||||
|      * Check whether the service has been initialized or not. | ||||
|      * | ||||
|      * @param url Url to open. | ||||
|      * @returns Whether the service has been initialized or not. | ||||
|      */ | ||||
|     hasInitialized(): boolean { | ||||
|         return this.initialized; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Run an operation inside the angular zone and return result. | ||||
|      * | ||||
|      * @param operation Operation callback. | ||||
|      * @return OK if successful, or ERROR: followed by message. | ||||
|      */ | ||||
|     static async handleCustomURL(url: string): Promise<string> { | ||||
|     async runInZone(operation: () => unknown, blocking: boolean = false): Promise<string> { | ||||
|         const blockKey = blocking && TestingBehatBlocking.block(); | ||||
| 
 | ||||
|         try { | ||||
|             await NgZone.run(async () => { | ||||
|                 await CoreCustomURLSchemes.handleCustomURL(url); | ||||
|             }); | ||||
|             await NgZone.run(operation); | ||||
| 
 | ||||
|             return 'OK'; | ||||
|         } catch (error) { | ||||
|             return 'ERROR: ' + error.message; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function called when a push notification is clicked. Redirect the user to the right state. | ||||
|      * | ||||
|      * @param data Notification data. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     static async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise<void> { | ||||
|         const blockKey = TestsBehatBlocking.block(); | ||||
| 
 | ||||
|         try { | ||||
|             await NgZone.run(async () => { | ||||
|                 await CorePushNotifications.notificationClicked(data); | ||||
|             }); | ||||
|         } finally { | ||||
|             TestsBehatBlocking.unblock(blockKey); | ||||
|             blockKey && TestingBehatBlocking.unblock(blockKey); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Force execution of synchronization cron tasks without waiting for the scheduled time. | ||||
|      * Please notice that some tasks may not be executed depending on the network connection and sync settings. | ||||
|      * | ||||
|      * @return Promise resolved if all handlers are executed successfully, rejected otherwise. | ||||
|      */ | ||||
|     static async forceSyncExecution(): Promise<void> { | ||||
|         await NgZone.run(async () => { | ||||
|             await CoreCronDelegate.forceSyncExecution(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Wait all controlled components to be rendered. | ||||
|      * | ||||
|      * @return Promise resolved when all components have been rendered. | ||||
|      */ | ||||
|     static async waitLoadingToFinish(): Promise<void> { | ||||
|     async waitLoadingToFinish(): Promise<void> { | ||||
|         await NgZone.run(async () => { | ||||
|             const elements = Array.from(document.body.querySelectorAll<HTMLElement>('core-loading')) | ||||
|                 .filter((element) => CoreDom.isElementVisible(element)); | ||||
| @ -148,28 +127,32 @@ export class TestsBehatRuntime { | ||||
|      * @param button Type of button to press. | ||||
|      * @return OK if successful, or ERROR: followed by message. | ||||
|      */ | ||||
|     static async pressStandard(button: string): Promise<string> { | ||||
|     async pressStandard(button: string): Promise<string> { | ||||
|         this.log('Action - Click standard button: ' + button); | ||||
| 
 | ||||
|         // Find button
 | ||||
|         let foundButton: HTMLElement | undefined; | ||||
|         const options: TestingBehatFindOptions = { | ||||
|             onlyClickable: true, | ||||
|             containerName: '', | ||||
|         }; | ||||
| 
 | ||||
|         switch (button) { | ||||
|             case 'back': | ||||
|                 foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'Back' }); | ||||
|                 foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Back' }, options); | ||||
|                 break; | ||||
|             case 'main menu': // Deprecated name.
 | ||||
|             case 'more menu': | ||||
|                 foundButton = TestsBehatDomUtils.findElementBasedOnText({ | ||||
|                 foundButton = TestingBehatDomUtils.findElementBasedOnText({ | ||||
|                     text: 'More', | ||||
|                     selector: 'ion-tab-button', | ||||
|                 }); | ||||
|                 }, options); | ||||
|                 break; | ||||
|             case 'user menu' : | ||||
|                 foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'User account' }); | ||||
|                 foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'User account' }, options); | ||||
|                 break; | ||||
|             case 'page menu': | ||||
|                 foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'Display options' }); | ||||
|                 foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Display options' }, options); | ||||
|                 break; | ||||
|             default: | ||||
|                 return 'ERROR: Unsupported standard button type'; | ||||
| @ -180,7 +163,7 @@ export class TestsBehatRuntime { | ||||
|         } | ||||
| 
 | ||||
|         // Click button
 | ||||
|         await TestsBehatDomUtils.pressElement(foundButton); | ||||
|         await TestingBehatDomUtils.pressElement(foundButton); | ||||
| 
 | ||||
|         return 'OK'; | ||||
|     } | ||||
| @ -190,7 +173,7 @@ export class TestsBehatRuntime { | ||||
|      * | ||||
|      * @return OK if successful, or ERROR: followed by message | ||||
|      */ | ||||
|     static closePopup(): string { | ||||
|     closePopup(): string { | ||||
|         this.log('Action - Close popup'); | ||||
| 
 | ||||
|         let backdrops = Array.from(document.querySelectorAll('ion-backdrop')); | ||||
| @ -206,7 +189,7 @@ export class TestsBehatRuntime { | ||||
|         backdrop.click(); | ||||
| 
 | ||||
|         // Mark busy until the click finishes processing.
 | ||||
|         TestsBehatBlocking.delay(); | ||||
|         TestingBehatBlocking.delay(); | ||||
| 
 | ||||
|         return 'OK'; | ||||
|     } | ||||
| @ -215,20 +198,24 @@ export class TestsBehatRuntime { | ||||
|      * 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: TestBehatElementLocator, containerName: string): string { | ||||
|         this.log('Action - Find', { locator, containerName }); | ||||
|     find(locator: TestingBehatElementLocator, options: Partial<TestingBehatFindOptions> = {}): string { | ||||
|         this.log('Action - Find', { locator, ...options }); | ||||
| 
 | ||||
|         try { | ||||
|             const element = TestsBehatDomUtils.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) { | ||||
| @ -242,11 +229,11 @@ export class TestsBehatRuntime { | ||||
|      * @param locator Element locator. | ||||
|      * @return OK if successful, or ERROR: followed by message | ||||
|      */ | ||||
|     static scrollTo(locator: TestBehatElementLocator): string { | ||||
|     scrollTo(locator: TestingBehatElementLocator): string { | ||||
|         this.log('Action - scrollTo', { locator }); | ||||
| 
 | ||||
|         try { | ||||
|             let element = TestsBehatDomUtils.findElementBasedOnText(locator); | ||||
|             let element = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: false, containerName: '' }); | ||||
| 
 | ||||
|             if (!element) { | ||||
|                 return 'ERROR: No element matches element to scroll to.'; | ||||
| @ -269,7 +256,7 @@ export class TestsBehatRuntime { | ||||
|      * | ||||
|      * @return OK if successful, or ERROR: followed by message | ||||
|      */ | ||||
|     static async loadMoreItems(): Promise<string> { | ||||
|     async loadMoreItems(): Promise<string> { | ||||
|         this.log('Action - loadMoreItems'); | ||||
| 
 | ||||
|         try { | ||||
| @ -316,13 +303,13 @@ export class TestsBehatRuntime { | ||||
|      * @param locator Element locator. | ||||
|      * @return YES or NO if successful, or ERROR: followed by message | ||||
|      */ | ||||
|     static isSelected(locator: TestBehatElementLocator): string { | ||||
|     isSelected(locator: TestingBehatElementLocator): string { | ||||
|         this.log('Action - Is Selected', locator); | ||||
| 
 | ||||
|         try { | ||||
|             const element = TestsBehatDomUtils.findElementBasedOnText(locator); | ||||
|             const element = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: false, containerName: '' }); | ||||
| 
 | ||||
|             return TestsBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO'; | ||||
|             return TestingBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO'; | ||||
|         } catch (error) { | ||||
|             return 'ERROR: ' + error.message; | ||||
|         } | ||||
| @ -334,17 +321,17 @@ export class TestsBehatRuntime { | ||||
|      * @param locator Element locator. | ||||
|      * @return OK if successful, or ERROR: followed by message | ||||
|      */ | ||||
|     static async press(locator: TestBehatElementLocator): Promise<string> { | ||||
|     async press(locator: TestingBehatElementLocator): Promise<string> { | ||||
|         this.log('Action - Press', locator); | ||||
| 
 | ||||
|         try { | ||||
|             const found = TestsBehatDomUtils.findElementBasedOnText(locator); | ||||
|             const found = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: true, containerName: '' }); | ||||
| 
 | ||||
|             if (!found) { | ||||
|                 return 'ERROR: No element matches locator to press.'; | ||||
|             } | ||||
| 
 | ||||
|             await TestsBehatDomUtils.pressElement(found); | ||||
|             await TestingBehatDomUtils.pressElement(found); | ||||
| 
 | ||||
|             return 'OK'; | ||||
|         } catch (error) { | ||||
| @ -357,7 +344,7 @@ export class TestsBehatRuntime { | ||||
|      * | ||||
|      * @return OK if successful, or ERROR: followed by message | ||||
|      */ | ||||
|     static async pullToRefresh(): Promise<string> { | ||||
|     async pullToRefresh(): Promise<string> { | ||||
|         this.log('Action - pullToRefresh'); | ||||
| 
 | ||||
|         try { | ||||
| @ -390,11 +377,11 @@ export class TestsBehatRuntime { | ||||
|      * | ||||
|      * @return OK: followed by header text if successful, or ERROR: followed by message. | ||||
|      */ | ||||
|     static getHeader(): string { | ||||
|     getHeader(): string { | ||||
|         this.log('Action - Get header'); | ||||
| 
 | ||||
|         let titles = Array.from(document.querySelectorAll<HTMLElement>('.ion-page:not(.ion-page-hidden) > ion-header h1')); | ||||
|         titles = titles.filter((title) => TestsBehatDomUtils.isElementVisible(title, document.body)); | ||||
|         titles = titles.filter((title) => TestingBehatDomUtils.isElementVisible(title, document.body)); | ||||
| 
 | ||||
|         if (titles.length > 1) { | ||||
|             return 'ERROR: Too many possible titles ('+titles.length+').'; | ||||
| @ -416,18 +403,19 @@ export class TestsBehatRuntime { | ||||
|      * @param value New value | ||||
|      * @return OK or ERROR: followed by message | ||||
|      */ | ||||
|     static async setField(field: string, value: string): Promise<string> { | ||||
|     async setField(field: string, value: string): Promise<string> { | ||||
|         this.log('Action - Set field ' + field + ' to: ' + value); | ||||
| 
 | ||||
|         const found: HTMLElement | HTMLInputElement = TestsBehatDomUtils.findElementBasedOnText( | ||||
|         const found: HTMLElement | HTMLInputElement = TestingBehatDomUtils.findElementBasedOnText( | ||||
|             { text: field, selector: 'input, textarea, [contenteditable="true"], ion-select' }, | ||||
|             { onlyClickable: false, containerName: '' }, | ||||
|         ); | ||||
| 
 | ||||
|         if (!found) { | ||||
|             return 'ERROR: No element matches field to set.'; | ||||
|         } | ||||
| 
 | ||||
|         await TestsBehatDomUtils.setElementValue(found, value); | ||||
|         await TestingBehatDomUtils.setElementValue(found, value); | ||||
| 
 | ||||
|         return 'OK'; | ||||
|     } | ||||
| @ -439,7 +427,7 @@ export class TestsBehatRuntime { | ||||
|      * @param className Constructor class name | ||||
|      * @return Component instance | ||||
|      */ | ||||
|     static getAngularInstance<T = unknown>(selector: string, className: string): T | null { | ||||
|     getAngularInstance<T = unknown>(selector: string, className: string): T | null { | ||||
|         this.log('Action - Get Angular instance ' + selector + ', ' + className); | ||||
| 
 | ||||
|         // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
| @ -456,7 +444,7 @@ export class TestsBehatRuntime { | ||||
|      * Logs information from this Behat runtime JavaScript, including the time and the 'BEHAT' | ||||
|      * keyword so we can easily filter for it if needed. | ||||
|      */ | ||||
|     static log(...args: unknown[]): void { | ||||
|     log(...args: unknown[]): void { | ||||
|         const now = new Date(); | ||||
|         const nowFormatted = String(now.getHours()).padStart(2, '0') + ':' + | ||||
|                 String(now.getMinutes()).padStart(2, '0') + ':' + | ||||
| @ -468,24 +456,29 @@ export class TestsBehatRuntime { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const TestingBehatRuntime = makeSingleton(TestingBehatRuntimeService); | ||||
| 
 | ||||
| export type BehatTestsWindow = Window & { | ||||
|     M?: { // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|         util?: { | ||||
|             pending_js?: string[]; // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|         }; | ||||
|     }; | ||||
|     behatInit?: () => void; | ||||
|     behat?: unknown; | ||||
| }; | ||||
| 
 | ||||
| export type TestBehatElementLocator = { | ||||
| export type TestingBehatFindOptions = { | ||||
|     containerName: string; | ||||
|     onlyClickable: boolean; | ||||
| }; | ||||
| 
 | ||||
| export type TestingBehatElementLocator = { | ||||
|     text: string; | ||||
|     within?: TestBehatElementLocator; | ||||
|     near?: TestBehatElementLocator; | ||||
|     within?: TestingBehatElementLocator; | ||||
|     near?: TestingBehatElementLocator; | ||||
|     selector?: string; | ||||
| }; | ||||
| 
 | ||||
| export type TestsBehatInitOptions = { | ||||
| export type TestingBehatInitOptions = { | ||||
|     skipOnBoarding?: boolean; | ||||
|     configOverrides?: Partial<EnvironmentConfig>; | ||||
| }; | ||||
|  | ||||
| @ -18,4 +18,4 @@ import { NgModule } from '@angular/core'; | ||||
|  * Stub used in production to avoid including testing code in production bundles. | ||||
|  */ | ||||
| @NgModule({}) | ||||
| export class BehatTestingModule {} | ||||
| export class TestingModule {} | ||||
| @ -12,21 +12,25 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { CoreAppProvider } from '@services/app'; | ||||
| import { CoreDB, CoreDbProvider } from '@services/db'; | ||||
| import { TestingBehatRuntime, TestingBehatRuntimeService } from './services/behat-runtime'; | ||||
| 
 | ||||
| type AutomatedTestsWindow = Window & { | ||||
|     dbProvider?: CoreDbProvider; | ||||
|     behat?: TestingBehatRuntimeService; | ||||
| }; | ||||
| 
 | ||||
| function initializeAutomatedTestsWindow(window: AutomatedTestsWindow) { | ||||
|     window.dbProvider = CoreDB.instance; | ||||
| } | ||||
| 
 | ||||
| export default function(): void { | ||||
|     if (!CoreAppProvider.isAutomated()) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     initializeAutomatedTestsWindow(window); | ||||
|     window.behat = TestingBehatRuntime.instance; | ||||
| } | ||||
| 
 | ||||
| @NgModule({ | ||||
|     providers: [ | ||||
|         { provide: APP_INITIALIZER, multi: true, useValue: () => initializeAutomatedTestsWindow(window) }, | ||||
|     ], | ||||
| }) | ||||
| export class TestingModule {} | ||||
							
								
								
									
										16
									
								
								src/tests/behat/runtime.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/tests/behat/runtime.feature
									
									
									
									
									
										Normal 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 | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user