forked from EVOgeek/Vmeda.Online
		
	Merge pull request #3278 from crazyserver/MOBILE-4061
Mobile 4061: Include behat tests on app code
This commit is contained in:
		
						commit
						a9dddde79a
					
				
							
								
								
									
										65
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | name: Behat tests | ||||||
|  | 
 | ||||||
|  | on: | ||||||
|  |   workflow_dispatch: | ||||||
|  |     inputs: | ||||||
|  |       tags: | ||||||
|  |         description: 'Execute tags' | ||||||
|  |         required: true | ||||||
|  |         default: '~@performance' | ||||||
|  |       moodle_branch: | ||||||
|  |         description: 'Moodle branch' | ||||||
|  |         required: true | ||||||
|  |         default: 'master' | ||||||
|  |       moodle_repository: | ||||||
|  |         description: 'Moodle repository' | ||||||
|  |         required: true | ||||||
|  |         default: 'https://github.com/moodle/moodle' | ||||||
|  | 
 | ||||||
|  | jobs: | ||||||
|  |   behat: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     env: | ||||||
|  |       MOODLE_DOCKER_DB: pgsql | ||||||
|  |       MOODLE_DOCKER_BROWSER: chrome | ||||||
|  |       MOODLE_DOCKER_PHP_VERSION: 7.3 | ||||||
|  |     steps: | ||||||
|  |     - uses: actions/checkout@v2 | ||||||
|  |     - id: nvmrc | ||||||
|  |       uses: browniebroke/read-nvmrc-action@v1 | ||||||
|  |     - uses: actions/setup-node@v1 | ||||||
|  |       with: | ||||||
|  |         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 master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker | ||||||
|  |     - name: Install npm packages | ||||||
|  |       run: | | ||||||
|  |         npm install -g npm@7 | ||||||
|  |         npm ci --no-audit | ||||||
|  |     - name: Generate Behat tests plugin | ||||||
|  |       run: | | ||||||
|  |         export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle | ||||||
|  |         npx gulp behat | ||||||
|  |     - name: Configure & launch Moodle with Docker | ||||||
|  |       run: | | ||||||
|  |         export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle | ||||||
|  |         cp $GITHUB_WORKSPACE/moodle-docker/config.docker-template.php $GITHUB_WORKSPACE/moodle/config.php | ||||||
|  |         sed -i "61i\$CFG->behat_ionic_wwwroot = 'http://moodleapp';" $GITHUB_WORKSPACE/moodle/config.php | ||||||
|  |         $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 | ||||||
|  |       run: | | ||||||
|  |         docker build -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 | ||||||
|  |       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/init.php" | ||||||
|  |     - 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" | ||||||
							
								
								
									
										3
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,6 @@ | |||||||
| name: Performance | name: Performance | ||||||
| 
 | 
 | ||||||
| on: [push, pull_request] | on: [ pull_request, workflow_dispatch ] | ||||||
| 
 | 
 | ||||||
| jobs: | jobs: | ||||||
|   performance: |   performance: | ||||||
| @ -19,7 +19,6 @@ jobs: | |||||||
|     - name: Additional checkouts |     - name: Additional checkouts | ||||||
|       run: | |       run: | | ||||||
|         git clone --branch master --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle |         git clone --branch master --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle | ||||||
|         git clone --branch integration --depth 1 https://github.com/moodlehq/moodle-local_moodlemobileapp $GITHUB_WORKSPACE/moodle/local/moodlemobileapp |  | ||||||
|         git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker |         git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker | ||||||
|     - name: Install npm packages |     - name: Install npm packages | ||||||
|       run: | |       run: | | ||||||
|  | |||||||
| @ -71,5 +71,5 @@ gulp.task('watch', () => { | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| gulp.task('watch-behat', () => { | gulp.task('watch-behat', () => { | ||||||
|     gulp.watch(['./tests/behat'], { interval: 500 }, gulp.parallel('behat')); |     gulp.watch(['./src/**/*.feature', './local-moodleappbehat'], { interval: 500 }, gulp.parallel('behat')); | ||||||
| }); | }); | ||||||
|  | |||||||
							
								
								
									
										42
									
								
								local-moodleappbehat/classes/output/mobile.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								local-moodleappbehat/classes/output/mobile.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | // This file is part of Moodle - http://moodle.org/
 | ||||||
|  | //
 | ||||||
|  | // Moodle is free software: you can redistribute it and/or modify
 | ||||||
|  | // it under the terms of the GNU General Public License as published by
 | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or
 | ||||||
|  | // (at your option) any later version.
 | ||||||
|  | //
 | ||||||
|  | // Moodle is distributed in the hope that it will be useful,
 | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||||
|  | // GNU General Public License for more details.
 | ||||||
|  | //
 | ||||||
|  | // You should have received a copy of the GNU General Public License
 | ||||||
|  | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  | namespace local_moodleappbehat\output; | ||||||
|  | 
 | ||||||
|  | defined('MOODLE_INTERNAL') || die(); | ||||||
|  | 
 | ||||||
|  | class mobile { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Render index page. | ||||||
|  |      * | ||||||
|  |      * @return array View data. | ||||||
|  |      */ | ||||||
|  |     public static function view_index() { | ||||||
|  |         $templates = [ | ||||||
|  |             [ | ||||||
|  |                 'id' => 'main', | ||||||
|  |                 'html' => '<h1 class="text-center">Hello<span id="username"></span>!</h1>', | ||||||
|  |             ], | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         $javascript = file_get_contents(__DIR__ . '/../../js/mobile/index.js'); | ||||||
|  | 
 | ||||||
|  |         return compact('templates', 'javascript'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								local-moodleappbehat/db/mobile.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								local-moodleappbehat/db/mobile.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | // This file is part of Moodle - http://moodle.org/
 | ||||||
|  | //
 | ||||||
|  | // Moodle is free software: you can redistribute it and/or modify
 | ||||||
|  | // it under the terms of the GNU General Public License as published by
 | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or
 | ||||||
|  | // (at your option) any later version.
 | ||||||
|  | //
 | ||||||
|  | // Moodle is distributed in the hope that it will be useful,
 | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||||
|  | // GNU General Public License for more details.
 | ||||||
|  | //
 | ||||||
|  | // You should have received a copy of the GNU General Public License
 | ||||||
|  | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  | $addons = [ | ||||||
|  |     'local_moodleappbehat' => [ | ||||||
|  |         'handlers' => [ | ||||||
|  |             'index' => [ | ||||||
|  |                 'delegate' => 'CoreMainMenuDelegate', | ||||||
|  |                 'method' => 'view_index', | ||||||
|  |                 'displaydata' => [ | ||||||
|  |                     'title' => 'pluginname', | ||||||
|  |                     'icon' => 'language', | ||||||
|  |                 ], | ||||||
|  |             ], | ||||||
|  |         ], | ||||||
|  |         'lang' => [ | ||||||
|  |             ['pluginname', 'local_moodleappbehat'], | ||||||
|  |         ], | ||||||
|  |     ], | ||||||
|  | ]; | ||||||
							
								
								
									
										5
									
								
								local-moodleappbehat/js/mobile/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								local-moodleappbehat/js/mobile/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | this.CoreSitesProvider.getSite().then(site => { | ||||||
|  |     const username = site.infos.username; | ||||||
|  | 
 | ||||||
|  |     document.getElementById('username').innerText = `, ${username}`; | ||||||
|  | }); | ||||||
							
								
								
									
										800
									
								
								local-moodleappbehat/tests/behat/behat_app.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										800
									
								
								local-moodleappbehat/tests/behat/behat_app.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,800 @@ | |||||||
|  | <?php | ||||||
|  | // This file is part of Moodle - http://moodle.org/
 | ||||||
|  | //
 | ||||||
|  | // Moodle is free software: you can redistribute it and/or modify
 | ||||||
|  | // it under the terms of the GNU General Public License as published by
 | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or
 | ||||||
|  | // (at your option) any later version.
 | ||||||
|  | //
 | ||||||
|  | // Moodle is distributed in the hope that it will be useful,
 | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||||
|  | // GNU General Public License for more details.
 | ||||||
|  | //
 | ||||||
|  | // You should have received a copy of the GNU General Public License
 | ||||||
|  | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  | // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 | ||||||
|  | 
 | ||||||
|  | require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); | ||||||
|  | require_once(__DIR__ . '/behat_app_helper.php'); | ||||||
|  | 
 | ||||||
|  | use Behat\Gherkin\Node\TableNode; | ||||||
|  | use Behat\Mink\Exception\DriverException; | ||||||
|  | use Behat\Mink\Exception\ExpectationException; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Moodle App steps definitions. | ||||||
|  |  * | ||||||
|  |  * @package core | ||||||
|  |  * @category test | ||||||
|  |  * @copyright 2018 The Open University | ||||||
|  |  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||||||
|  |  */ | ||||||
|  | class behat_app extends behat_app_helper { | ||||||
|  | 
 | ||||||
|  |     /** @var string URL for running Ionic server */ | ||||||
|  |     protected $ionicurl = ''; | ||||||
|  | 
 | ||||||
|  |     /** @var array Config overrides */ | ||||||
|  |     protected $appconfig = ['disableUserTours' => true]; | ||||||
|  | 
 | ||||||
|  |     protected $windowsize = '360x720'; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Opens the Moodle App in the browser and optionally logs in. | ||||||
|  |      * | ||||||
|  |      * @When I enter the app | ||||||
|  |      * @Given I entered the app as :username | ||||||
|  |      * @throws DriverException Issue with configuration or feature file | ||||||
|  |      * @throws dml_exception Problem with Moodle setup | ||||||
|  |      * @throws ExpectationException Problem with resizing window | ||||||
|  |      */ | ||||||
|  |     public function i_enter_the_app(string $username = null) { | ||||||
|  |         $this->i_launch_the_app(); | ||||||
|  | 
 | ||||||
|  |         if (!is_null($username)) { | ||||||
|  |             $this->open_moodleapp_custom_login_url($username); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!$this->is_in_login_page()) { | ||||||
|  |             // Already in the site.
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         global $CFG; | ||||||
|  | 
 | ||||||
|  |         $this->i_set_the_field_in_the_app('Your site', $CFG->behat_wwwroot); | ||||||
|  |         $this->i_press_in_the_app('"Connect to your site"'); | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check whether the current page is the login form. | ||||||
|  |      */ | ||||||
|  |     protected function is_in_login_page(): bool { | ||||||
|  |         $page = $this->getSession()->getPage(); | ||||||
|  |         $logininput = $page->find('xpath', '//page-core-login-site//input[@name="url"]'); | ||||||
|  | 
 | ||||||
|  |         return !is_null($logininput); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Opens the Moodle App in the browser. | ||||||
|  |      * | ||||||
|  |      * @When I launch the app :runtime | ||||||
|  |      * @When I launch the app | ||||||
|  |      * @throws DriverException Issue with configuration or feature file | ||||||
|  |      * @throws dml_exception Problem with Moodle setup | ||||||
|  |      * @throws ExpectationException Problem with resizing window | ||||||
|  |      */ | ||||||
|  |     public function i_launch_the_app(string $runtime = '') { | ||||||
|  |         // Check the app tag was set.
 | ||||||
|  |         if (!$this->has_tag('app')) { | ||||||
|  |             throw new DriverException('Requires @app tag on scenario or feature.'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Go to page and prepare browser for app.
 | ||||||
|  |         $this->prepare_browser(['skiponboarding' => empty($runtime)]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @Then I wait the app to restart | ||||||
|  |      */ | ||||||
|  |     public function i_wait_the_app_to_restart() { | ||||||
|  |         // Wait window to reload.
 | ||||||
|  |         $this->spin(function() { | ||||||
|  |             $result = $this->evaluate_script("return !window.behat;"); | ||||||
|  | 
 | ||||||
|  |             if (!$result) { | ||||||
|  |                 throw new DriverException('Window is not reloading properly.'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Prepare testing runtime again.
 | ||||||
|  |         $this->prepare_browser(['restart' => false]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Finds elements in the app. | ||||||
|  |      * | ||||||
|  |      * @Then /^I should( not)? find (".+")( inside the .+)? in the app$/ | ||||||
|  |      */ | ||||||
|  |     public function i_find_in_the_app(bool $not, string $locator, string $containerName = '') { | ||||||
|  |         $locator = $this->parse_element_locator($locator); | ||||||
|  |         if (!empty($containerName)) { | ||||||
|  |             preg_match('/^ inside the (.+)$/', $containerName, $matches); | ||||||
|  |             $containerName = $matches[1]; | ||||||
|  |         } | ||||||
|  |         $containerName = json_encode($containerName); | ||||||
|  | 
 | ||||||
|  |         $this->spin(function() use ($not, $locator, $containerName) { | ||||||
|  |             $result = $this->evaluate_script("return window.behat.find($locator, $containerName);"); | ||||||
|  | 
 | ||||||
|  |             if ($not && $result === 'OK') { | ||||||
|  |                 throw new DriverException('Error, found an item that should not be found'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!$not && $result !== 'OK') { | ||||||
|  |                 throw new DriverException('Error finding item - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Scroll to an element in the app. | ||||||
|  |      * | ||||||
|  |      * @When /^I scroll to (".+") in the app$/ | ||||||
|  |      * @param string $locator | ||||||
|  |      */ | ||||||
|  |     public function i_scroll_to_in_the_app(string $locator) { | ||||||
|  |         $locator = $this->parse_element_locator($locator); | ||||||
|  | 
 | ||||||
|  |         $this->spin(function() use ($locator) { | ||||||
|  |             $result = $this->evaluate_script("return window.behat.scrollTo($locator);"); | ||||||
|  | 
 | ||||||
|  |             if ($result !== 'OK') { | ||||||
|  |                 throw new DriverException('Error finding item - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  | 
 | ||||||
|  |         // Wait scroll animation to finish.
 | ||||||
|  |         $this->getSession()->wait(300); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load more items in a list with an infinite loader. | ||||||
|  |      * | ||||||
|  |      * @When /^I (should not be able to )?load more items in the app$/ | ||||||
|  |      * @param bool $not | ||||||
|  |      */ | ||||||
|  |     public function i_load_more_items_in_the_app(bool $not = false) { | ||||||
|  |         $this->spin(function() use ($not) { | ||||||
|  |             $result = $this->evaluate_async_script('return window.behat.loadMoreItems();'); | ||||||
|  | 
 | ||||||
|  |             if ($not && $result !== 'ERROR: All items are already loaded.') { | ||||||
|  |                 throw new DriverException('It should not have been possible to load more items'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!$not && $result !== 'OK') { | ||||||
|  |                 throw new DriverException('Error loading more items - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Trigger swipe gesture. | ||||||
|  |      * | ||||||
|  |      * @When /^I swipe to the (left|right) in the app$/ | ||||||
|  |      * @param string $direction | ||||||
|  |      */ | ||||||
|  |     public function i_swipe_in_the_app(string $direction) { | ||||||
|  |         $method = 'swipe' . ucwords($direction); | ||||||
|  | 
 | ||||||
|  |         $this->evaluate_script("window.behat.getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()"); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  | 
 | ||||||
|  |         // Wait swipe animation to finish.
 | ||||||
|  |         $this->getSession()->wait(300); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if elements are selected in the app. | ||||||
|  |      * | ||||||
|  |      * @Then /^(".+") should( not)? be selected in the app$/ | ||||||
|  |      * @param string $locator | ||||||
|  |      * @param bool $not | ||||||
|  |      */ | ||||||
|  |     public function be_selected_in_the_app(string $locator, bool $not = false) { | ||||||
|  |         $locator = $this->parse_element_locator($locator); | ||||||
|  | 
 | ||||||
|  |         $this->spin(function() use ($locator, $not) { | ||||||
|  |             $result = $this->evaluate_script("return window.behat.isSelected($locator);"); | ||||||
|  | 
 | ||||||
|  |             switch ($result) { | ||||||
|  |                 case 'YES': | ||||||
|  |                     if ($not) { | ||||||
|  |                         throw new ExpectationException("Item 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()); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 default: | ||||||
|  |                     throw new DriverException('Error finding item - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Carries out the login steps for the app, assuming the user is on the app login page. Called | ||||||
|  |      * from behat_auth.php. | ||||||
|  |      * | ||||||
|  |      * @param string $username Username (and password) | ||||||
|  |      * @throws Exception Any error | ||||||
|  |      */ | ||||||
|  |     public function login(string $username) { | ||||||
|  |         $this->i_set_the_field_in_the_app('Username', $username); | ||||||
|  |         $this->i_set_the_field_in_the_app('Password', $username); | ||||||
|  | 
 | ||||||
|  |         // Note there are two 'Log in' texts visible (the title and the button) so we have to use
 | ||||||
|  |         // a 'near' value here.
 | ||||||
|  |         $this->i_press_in_the_app('"Log in" near "Forgotten"'); | ||||||
|  | 
 | ||||||
|  |         // Wait until the main page appears.
 | ||||||
|  |         $this->spin( | ||||||
|  |                 function($context, $args) { | ||||||
|  |                     $mainmenu = $context->getSession()->getPage()->find('xpath', '//page-core-mainmenu'); | ||||||
|  |                     if ($mainmenu) { | ||||||
|  |                         return true; | ||||||
|  |                     } | ||||||
|  |                     throw new DriverException('Moodle App main page not loaded after login'); | ||||||
|  |                 }, false, 30); | ||||||
|  | 
 | ||||||
|  |         // Wait for JS to finish as well.
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Shortcut to  let the user enter a course in the app. | ||||||
|  |      * | ||||||
|  |      * @Given I entered the course :coursename as :username in the app | ||||||
|  |      * @Given I entered the course :coursename in the app | ||||||
|  |      * @param string $coursename Course name | ||||||
|  |      * @throws DriverException If the button push doesn't work | ||||||
|  |      */ | ||||||
|  |     public function i_entered_the_course_in_the_app(string $coursename, ?string $username = null) { | ||||||
|  |         $courseid = $this->get_course_id($coursename); | ||||||
|  |         if (!$courseid) { | ||||||
|  |             throw new DriverException("Course '$coursename' not found"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($username) { | ||||||
|  |             $this->i_launch_the_app(); | ||||||
|  | 
 | ||||||
|  |             $this->open_moodleapp_custom_login_url($username, "/course/view.php?id=$courseid", '//page-core-course-index'); | ||||||
|  |         } else { | ||||||
|  |             $this->open_moodleapp_custom_url("/course/view.php?id=$courseid", '//page-core-course-index'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * User enters a course in the app. | ||||||
|  |      * | ||||||
|  |      * @Given I enter the course :coursename in the app | ||||||
|  |      * @param string $coursename Course name | ||||||
|  |      * @throws DriverException If the button push doesn't work | ||||||
|  |      */ | ||||||
|  |     public function i_enter_the_course_in_the_app(string $coursename, ?string $username = null) { | ||||||
|  |         if (!is_null($username)) { | ||||||
|  |             $this->i_enter_the_app(); | ||||||
|  |             $this->login($username); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $mycoursesfound = $this->evaluate_script("return window.behat.find({ text: 'My courses', selector: 'ion-tab-button'});"); | ||||||
|  | 
 | ||||||
|  |         if ($mycoursesfound !== 'OK') { | ||||||
|  |             // My courses not present enter from Dashboard.
 | ||||||
|  |             $this->i_press_in_the_app('"Home" "ion-tab-button"'); | ||||||
|  |             $this->i_press_in_the_app('"Dashboard"'); | ||||||
|  |             $this->i_press_in_the_app('"'.$coursename.'" near "Course overview"'); | ||||||
|  | 
 | ||||||
|  |             $this->wait_for_pending_js(); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $this->i_press_in_the_app('"My courses" "ion-tab-button"'); | ||||||
|  |         $this->i_press_in_the_app('"'.$coursename.'"'); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * User enters an activity in a course in the app. | ||||||
|  |      * | ||||||
|  |      * @Given I entered the :activity activity :activityname on course :course as :username in the app | ||||||
|  |      * @Given I entered the :activity activity :activityname on course :course in the app | ||||||
|  |      * @throws DriverException If the button push doesn't work | ||||||
|  |      */ | ||||||
|  |     public function i_enter_the_activity_in_the_app(string $activity, string $activityname, string $coursename, ?string $username = null) { | ||||||
|  |         $cm = $this->get_cm_by_activity_name_and_course($activity, $activityname, $coursename); | ||||||
|  |         if (!$cm) { | ||||||
|  |             throw new DriverException("'$activityname' activity '$activityname' not found"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $pageurl = "/mod/$activity/view.php?id={$cm->id}"; | ||||||
|  | 
 | ||||||
|  |         if ($username) { | ||||||
|  |             $this->i_launch_the_app(); | ||||||
|  | 
 | ||||||
|  |             $this->open_moodleapp_custom_login_url($username, $pageurl); | ||||||
|  |         } else { | ||||||
|  |             $this->open_moodleapp_custom_url($pageurl); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Presses standard buttons in the app. | ||||||
|  |      * | ||||||
|  |      * @When /^I press the (back|more menu|page menu|user menu|main menu) button in the app$/ | ||||||
|  |      * @param string $button Button type | ||||||
|  |      * @throws DriverException If the button push doesn't work | ||||||
|  |      */ | ||||||
|  |     public function i_press_the_standard_button_in_the_app(string $button) { | ||||||
|  |         $this->spin(function() use ($button) { | ||||||
|  |             $result = $this->evaluate_script("return window.behat.pressStandard('$button');"); | ||||||
|  | 
 | ||||||
|  |             if ($result !== 'OK') { | ||||||
|  |                 throw new DriverException('Error pressing standard button - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Receives push notifications. | ||||||
|  |      * | ||||||
|  |      * @When /^I receive a push notification in the app for:$/ | ||||||
|  |      * @param TableNode $data | ||||||
|  |      */ | ||||||
|  |     public function i_receive_a_push_notification(TableNode $data) { | ||||||
|  |         global $DB, $CFG; | ||||||
|  | 
 | ||||||
|  |         $data = (object) $data->getColumnsHash()[0]; | ||||||
|  |         $module = $DB->get_record('course_modules', ['idnumber' => $data->module]); | ||||||
|  |         $discussion = $DB->get_record('forum_discussions', ['name' => $data->discussion]); | ||||||
|  |         $notification = json_encode([ | ||||||
|  |             'site' => md5($CFG->behat_wwwroot . $data->username), | ||||||
|  |             'courseid' => $discussion->course, | ||||||
|  |             'moodlecomponent' => 'mod_forum', | ||||||
|  |             'name' => 'posts', | ||||||
|  |             'contexturl' => '', | ||||||
|  |             'notif' => 1, | ||||||
|  |             'customdata' => [ | ||||||
|  |                 'discussionid' => $discussion->id, | ||||||
|  |                 'cmid' => $module->id, | ||||||
|  |                 'instance' => $discussion->forum, | ||||||
|  |             ], | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |         $this->evaluate_script("return window.pushNotifications.notificationClicked($notification)"); | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Replace arguments from the content in the given activity field. | ||||||
|  |      * | ||||||
|  |      * @Given /^I replace the arguments in "([^"]+)" "([^"]+)"$/ | ||||||
|  |      */ | ||||||
|  |     public function i_replace_arguments_in_the_activity(string $idnumber, string $field) { | ||||||
|  |         global $DB; | ||||||
|  | 
 | ||||||
|  |         $coursemodule = $DB->get_record('course_modules', compact('idnumber')); | ||||||
|  |         $module = $DB->get_record('modules', ['id' => $coursemodule->module]); | ||||||
|  |         $activity = $DB->get_record($module->name, ['id' => $coursemodule->instance]); | ||||||
|  | 
 | ||||||
|  |         $DB->update_record($module->name, [ | ||||||
|  |             'id' => $coursemodule->instance, | ||||||
|  |             $field => $this->replace_arguments($activity->{$field}), | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Opens a custom link. | ||||||
|  |      * | ||||||
|  |      * @Given /^I open a custom link in the app for:$/ | ||||||
|  |      * @param TableNode $data | ||||||
|  |      */ | ||||||
|  |     public function i_open_a_custom_link(TableNode $data) { | ||||||
|  |         global $DB; | ||||||
|  | 
 | ||||||
|  |         $data = $data->getColumnsHash()[0]; | ||||||
|  |         $title = array_keys($data)[0]; | ||||||
|  |         $data = (object) $data; | ||||||
|  | 
 | ||||||
|  |         switch ($title) { | ||||||
|  |             case 'discussion': | ||||||
|  |                 $discussion = $DB->get_record('forum_discussions', ['name' => $data->discussion]); | ||||||
|  |                 $pageurl = "/mod/forum/discuss.php?d={$discussion->id}"; | ||||||
|  | 
 | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case 'assign': | ||||||
|  |             case 'bigbluebuttonbn': | ||||||
|  |             case 'book': | ||||||
|  |             case 'chat': | ||||||
|  |             case 'choice': | ||||||
|  |             case 'data': | ||||||
|  |             case 'feedback': | ||||||
|  |             case 'folder': | ||||||
|  |             case 'forum': | ||||||
|  |             case 'glossary': | ||||||
|  |             case 'h5pactivity': | ||||||
|  |             case 'imscp': | ||||||
|  |             case 'label': | ||||||
|  |             case 'lesson': | ||||||
|  |             case 'lti': | ||||||
|  |             case 'page': | ||||||
|  |             case 'quiz': | ||||||
|  |             case 'resource': | ||||||
|  |             case 'scorm': | ||||||
|  |             case 'survey': | ||||||
|  |             case 'url': | ||||||
|  |             case 'wiki': | ||||||
|  |             case 'workshop': | ||||||
|  |                 $name = $data->$title; | ||||||
|  |                 $module = $DB->get_record($title, ['name' => $name]); | ||||||
|  |                 $cm = get_coursemodule_from_instance($title, $module->id); | ||||||
|  |                 $pageurl = "/mod/$title/view.php?id={$cm->id}"; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             default: | ||||||
|  |                 throw new DriverException('Invalid custom link title - ' . $title); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $this->open_moodleapp_custom_url($pageurl); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Closes a popup by clicking on the 'backdrop' behind it. | ||||||
|  |      * | ||||||
|  |      * @When I close the popup in the app | ||||||
|  |      * @throws DriverException If there isn't a popup to close | ||||||
|  |      */ | ||||||
|  |     public function i_close_the_popup_in_the_app() { | ||||||
|  |         $this->spin(function()  { | ||||||
|  |             $result = $this->evaluate_script("return window.behat.closePopup();"); | ||||||
|  | 
 | ||||||
|  |             if ($result !== 'OK') { | ||||||
|  |                 throw new DriverException('Error closing popup - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Override app config. | ||||||
|  |      * | ||||||
|  |      * @Given /^the app has the following config:$/ | ||||||
|  |      * @param TableNode $data | ||||||
|  |      */ | ||||||
|  |     public function the_app_has_the_following_config(TableNode $data) { | ||||||
|  |         foreach ($data->getRows() as $configrow) { | ||||||
|  |             $this->appconfig[$configrow[0]] = json_decode($configrow[1]); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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. | ||||||
|  |      * | ||||||
|  |      * @Then /^I press (".+") in the app$/ | ||||||
|  |      * @param string $locator Element locator | ||||||
|  |      * @throws DriverException If the press doesn't work | ||||||
|  |      */ | ||||||
|  |     public function i_press_in_the_app(string $locator) { | ||||||
|  |         $locator = $this->parse_element_locator($locator); | ||||||
|  | 
 | ||||||
|  |         $this->spin(function() use ($locator) { | ||||||
|  |             $result = $this->evaluate_script("return window.behat.press($locator);"); | ||||||
|  | 
 | ||||||
|  |             if ($result !== 'OK') { | ||||||
|  |                 throw new DriverException('Error pressing item - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Select an item from a list of options, such as a radio button. | ||||||
|  |      * | ||||||
|  |      * It may be necessary to use this step instead of "I press..." because radio buttons in Ionic are initialized | ||||||
|  |      * with JavaScript, and clicks may not work until they are initialized properly which may cause flaky tests due | ||||||
|  |      * to race conditions. | ||||||
|  |      * | ||||||
|  |      * @Then /^I (unselect|select) (".+") in the app$/ | ||||||
|  |      * @param string $selectedtext | ||||||
|  |      * @param string $locator | ||||||
|  |      * @throws DriverException If the press doesn't work | ||||||
|  |      */ | ||||||
|  |     public function i_select_in_the_app(string $selectedtext, string $locator) { | ||||||
|  |         $selected = $selectedtext === 'select' ? 'YES' : 'NO'; | ||||||
|  |         $locator = $this->parse_element_locator($locator); | ||||||
|  | 
 | ||||||
|  |         $this->spin(function() use ($selectedtext, $selected, $locator) { | ||||||
|  |             // Don't do anything if the item is already in the expected state.
 | ||||||
|  |             $result = $this->evaluate_script("return window.behat.isSelected($locator);"); | ||||||
|  | 
 | ||||||
|  |             if ($result === $selected) { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Press item.
 | ||||||
|  |             $result = $this->evaluate_script("return window.behat.press($locator);"); | ||||||
|  | 
 | ||||||
|  |             if ($result !== 'OK') { | ||||||
|  |                 throw new DriverException('Error pressing item - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Check that it worked as expected.
 | ||||||
|  |             $this->wait_for_pending_js(); | ||||||
|  | 
 | ||||||
|  |             $result = $this->evaluate_script("return window.behat.isSelected($locator);"); | ||||||
|  | 
 | ||||||
|  |             switch ($result) { | ||||||
|  |                 case 'YES': | ||||||
|  |                 case 'NO': | ||||||
|  |                     if ($result !== $selected) { | ||||||
|  |                         throw new ExpectationException("Item wasn't $selectedtext after pressing it", $this->getSession()->getDriver()); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     return true; | ||||||
|  |                 default: | ||||||
|  |                     throw new DriverException('Error finding item - ' . $result); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sets a field to the given text value in the app. | ||||||
|  |      * | ||||||
|  |      * Currently this only works for input fields which must be identified using a partial or | ||||||
|  |      * exact match on the placeholder text. | ||||||
|  |      * | ||||||
|  |      * @Given /^I set the field "((?:[^"]|\\")+)" to "((?:[^"]|\\")*)" in the app$/ | ||||||
|  |      * @param string $field Text identifying field | ||||||
|  |      * @param string $value Value for field | ||||||
|  |      * @throws DriverException If the field set doesn't work | ||||||
|  |      */ | ||||||
|  |     public function i_set_the_field_in_the_app(string $field, string $value) { | ||||||
|  |         $field = addslashes_js($field); | ||||||
|  |         $value = addslashes_js($value); | ||||||
|  | 
 | ||||||
|  |         $this->spin(function() use ($field, $value) { | ||||||
|  |             $result = $this->evaluate_script("return window.behat.setField(\"$field\", \"$value\");");
 | ||||||
|  | 
 | ||||||
|  |             if ($result !== 'OK') { | ||||||
|  |                 throw new DriverException('Error setting field - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Checks that the current header stripe in the app contains the expected text. | ||||||
|  |      * | ||||||
|  |      * This can be used to see if the app went to the expected page. | ||||||
|  |      * | ||||||
|  |      * @Then /^the header should be "((?:[^"]|\\")+)" in the app$/ | ||||||
|  |      * @param string $text Expected header text | ||||||
|  |      * @throws DriverException If the header can't be retrieved | ||||||
|  |      * @throws ExpectationException If the header text is different to the expected value | ||||||
|  |      */ | ||||||
|  |     public function the_header_should_be_in_the_app(string $text) { | ||||||
|  |         $this->spin(function() use ($text) { | ||||||
|  |             $result = $this->evaluate_script('return window.behat.getHeader();'); | ||||||
|  | 
 | ||||||
|  |             if (substr($result, 0, 3) !== 'OK:') { | ||||||
|  |                 throw new DriverException('Error getting header - ' . $result); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $header = substr($result, 3); | ||||||
|  |             if (trim($header) !== trim($text)) { | ||||||
|  |                 throw new ExpectationException( | ||||||
|  |                     "The header text was not as expected: '$header'", | ||||||
|  |                     $this->getSession()->getDriver() | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check that the app opened a new browser tab. | ||||||
|  |      * | ||||||
|  |      * @Then /^the app should( not)? have opened a browser tab(?: with url "(?P<pattern>[^"]+)")?$/
 | ||||||
|  |      * @param bool $not | ||||||
|  |      * @param string $urlpattern | ||||||
|  |      */ | ||||||
|  |     public function the_app_should_have_opened_a_browser_tab(bool $not = false, ?string $urlpattern = null) { | ||||||
|  |         $this->spin(function() use ($not, $urlpattern) { | ||||||
|  |             $windowNames = $this->getSession()->getWindowNames(); | ||||||
|  |             $openedbrowsertab = count($windowNames) === 2; | ||||||
|  | 
 | ||||||
|  |             if ((!$not && !$openedbrowsertab) || ($not && $openedbrowsertab && is_null($urlpattern))) { | ||||||
|  |                 throw new ExpectationException( | ||||||
|  |                     $not | ||||||
|  |                         ? 'Did not expect the app to have opened a browser tab' | ||||||
|  |                         : 'Expected the app to have opened a browser tab', | ||||||
|  |                     $this->getSession()->getDriver() | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!is_null($urlpattern)) { | ||||||
|  |                 $this->getSession()->switchToWindow($windowNames[1]); | ||||||
|  |                 $windowurl = $this->getSession()->getCurrentUrl(); | ||||||
|  |                 $windowhaspattern = preg_match("/$urlpattern/", $windowurl); | ||||||
|  |                 $this->getSession()->switchToWindow($windowNames[0]); | ||||||
|  | 
 | ||||||
|  |                 if ($not === $windowhaspattern) { | ||||||
|  |                     throw new ExpectationException( | ||||||
|  |                         $not | ||||||
|  |                             ? "Did not expect the app to have opened a browser tab with pattern '$urlpattern'" | ||||||
|  |                             : "Browser tab url does not match pattern '$urlpattern', it is '$windowurl'", | ||||||
|  |                         $this->getSession()->getDriver() | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return true; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Switches to a newly-opened browser tab. | ||||||
|  |      * | ||||||
|  |      * This assumes the app opened a new tab. | ||||||
|  |      * | ||||||
|  |      * @Given I switch to the browser tab opened by the app | ||||||
|  |      * @throws DriverException If there aren't exactly 2 tabs open | ||||||
|  |      */ | ||||||
|  |     public function i_switch_to_the_browser_tab_opened_by_the_app() { | ||||||
|  |         $windowNames = $this->getSession()->getWindowNames(); | ||||||
|  |         if (count($windowNames) !== 2) { | ||||||
|  |             throw new DriverException('Expected to see 2 tabs open, not ' . count($windowNames)); | ||||||
|  |         } | ||||||
|  |         $this->getSession()->switchToWindow($windowNames[1]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Force cron tasks instead of waiting for the next scheduled execution. | ||||||
|  |      * | ||||||
|  |      * @When I run cron tasks in the app | ||||||
|  |      */ | ||||||
|  |     public function i_run_cron_tasks_in_the_app() { | ||||||
|  |         $session = $this->getSession(); | ||||||
|  | 
 | ||||||
|  |         // Force cron tasks execution and wait until they are completed.
 | ||||||
|  |         $operationid = random_string(); | ||||||
|  | 
 | ||||||
|  |         $session->executeScript( | ||||||
|  |             "cronProvider.forceSyncExecution().then(() => { window['behat_{$operationid}_completed'] = true; });" | ||||||
|  |         ); | ||||||
|  |         $this->spin( | ||||||
|  |             function() use ($session, $operationid) { | ||||||
|  |                 return $session->evaluateScript("window['behat_{$operationid}_completed'] || false"); | ||||||
|  |             }, | ||||||
|  |             false, | ||||||
|  |             60, | ||||||
|  |             new ExpectationException('Forced cron tasks in the app took too long to complete', $session) | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         // Trigger Angular change detection.
 | ||||||
|  |         $this->trigger_angular_change_detection(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Wait until loading has finished. | ||||||
|  |      * | ||||||
|  |      * @When I wait loading to finish in the app | ||||||
|  |      */ | ||||||
|  |     public function i_wait_loading_to_finish_in_the_app() { | ||||||
|  |         $session = $this->getSession(); | ||||||
|  | 
 | ||||||
|  |         $this->spin( | ||||||
|  |             function() use ($session) { | ||||||
|  |                 $this->trigger_angular_change_detection(); | ||||||
|  | 
 | ||||||
|  |                 $nodes = $this->find_all('css', 'core-loading ion-spinner'); | ||||||
|  | 
 | ||||||
|  |                 foreach ($nodes as $node) { | ||||||
|  |                     if (!$node->isVisible()) { | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return true; | ||||||
|  |             }, | ||||||
|  |             false, | ||||||
|  |             60, | ||||||
|  |             new ExpectationException('"Loading took too long to complete', $session) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Closes the current browser tab. | ||||||
|  |      * | ||||||
|  |      * This assumes it was opened by the app and you will now get back to the app. | ||||||
|  |      * | ||||||
|  |      * @Given I close the browser tab opened by the app | ||||||
|  |      * @throws DriverException If there aren't exactly 2 tabs open | ||||||
|  |      */ | ||||||
|  |     public function i_close_the_browser_tab_opened_by_the_app() { | ||||||
|  |         $names = $this->getSession()->getWindowNames(); | ||||||
|  |         if (count($names) !== 2) { | ||||||
|  |             throw new DriverException('Expected to see 2 tabs open, not ' . count($names)); | ||||||
|  |         } | ||||||
|  |         // Make sure the browser tab is selected.
 | ||||||
|  |         if ($this->getSession()->getWindowName() !== $names[1]) { | ||||||
|  |             $this->getSession()->switchToWindow($names[1]); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $this->execute_script('window.close();'); | ||||||
|  |         $this->getSession()->switchToWindow($names[0]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Switch navigator online mode. | ||||||
|  |      * | ||||||
|  |      * @Given /^I switch offline mode to "(true|false)"$/ | ||||||
|  |      * @param string $offline New value for navigator online mode | ||||||
|  |      * @throws DriverException If the navigator.online mode is not available | ||||||
|  |      */ | ||||||
|  |     public function i_switch_offline_mode(string $offline) { | ||||||
|  |         $this->execute_script("appProvider.setForceOffline($offline);"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										619
									
								
								local-moodleappbehat/tests/behat/behat_app_helper.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										619
									
								
								local-moodleappbehat/tests/behat/behat_app_helper.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,619 @@ | |||||||
|  | <?php | ||||||
|  | // This file is part of Moodle - http://moodle.org/
 | ||||||
|  | //
 | ||||||
|  | // Moodle is free software: you can redistribute it and/or modify
 | ||||||
|  | // it under the terms of the GNU General Public License as published by
 | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or
 | ||||||
|  | // (at your option) any later version.
 | ||||||
|  | //
 | ||||||
|  | // Moodle is distributed in the hope that it will be useful,
 | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||||
|  | // GNU General Public License for more details.
 | ||||||
|  | //
 | ||||||
|  | // You should have received a copy of the GNU General Public License
 | ||||||
|  | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
|  | 
 | ||||||
|  | // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 | ||||||
|  | 
 | ||||||
|  | require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); | ||||||
|  | 
 | ||||||
|  | use Behat\Mink\Exception\DriverException; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Behat app listener. | ||||||
|  |  */ | ||||||
|  | interface behat_app_listener { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called when the app is loaded. | ||||||
|  |      */ | ||||||
|  |     function on_app_load(): void; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called before the app is unloaded. | ||||||
|  |      */ | ||||||
|  |     function on_app_unload(): void; | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A trait containing functionality used by the behat app context. | ||||||
|  |  * | ||||||
|  |  * @package    core | ||||||
|  |  * @category   test | ||||||
|  |  * @copyright  2018 The Open University | ||||||
|  |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||||||
|  |  */ | ||||||
|  | class behat_app_helper extends behat_base { | ||||||
|  | 
 | ||||||
|  |     /** @var stdClass Object with data about launched Ionic instance (if any) */ | ||||||
|  |     protected static $ionicrunning = null; | ||||||
|  | 
 | ||||||
|  |     /** @var array */ | ||||||
|  |     protected static $listeners = []; | ||||||
|  | 
 | ||||||
|  |     /** @var bool Whether the app is running or not */ | ||||||
|  |     protected $apprunning = false; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Register listener. | ||||||
|  |      * | ||||||
|  |      * @param behat_app_listener $listener Listener. | ||||||
|  |      * @return Closure Unregister function. | ||||||
|  |      */ | ||||||
|  |     public static function listen(behat_app_listener $listener): Closure { | ||||||
|  |         self::$listeners[] = $listener; | ||||||
|  | 
 | ||||||
|  |         return function () use ($listener) { | ||||||
|  |             $index = array_search($listener, self::$listeners); | ||||||
|  | 
 | ||||||
|  |             if ($index !== false) { | ||||||
|  |                 array_splice(self::$listeners, $index, 1); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Checks if the current OS is Windows, from the point of view of task-executing-and-killing. | ||||||
|  |      * | ||||||
|  |      * @return bool True if Windows | ||||||
|  |      */ | ||||||
|  |     protected static function is_windows() : bool { | ||||||
|  |         return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called from behat_hooks when a new scenario starts, if it has the app tag. | ||||||
|  |      * | ||||||
|  |      * This updates Moodle configuration and starts Ionic running, if it isn't already. | ||||||
|  |      */ | ||||||
|  |     public function start_scenario() { | ||||||
|  |         $this->check_behat_setup(); | ||||||
|  |         $this->fix_moodle_setup(); | ||||||
|  |         $this->ionicurl = $this->start_or_reuse_ionic(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Checks the Behat setup - tags and configuration. | ||||||
|  |      * | ||||||
|  |      * @throws DriverException | ||||||
|  |      */ | ||||||
|  |     protected function check_behat_setup() { | ||||||
|  |         global $CFG; | ||||||
|  | 
 | ||||||
|  |         // Check JavaScript is enabled.
 | ||||||
|  |         if (!$this->running_javascript()) { | ||||||
|  |             throw new DriverException('The app requires JavaScript.'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Check the config settings are defined.
 | ||||||
|  |         if (empty($CFG->behat_ionic_wwwroot) && empty($CFG->behat_ionic_dirroot)) { | ||||||
|  |             throw new DriverException('$CFG->behat_ionic_wwwroot or $CFG->behat_ionic_dirroot must be defined.'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Fixes the Moodle admin settings to allow Moodle App use (if not already correct). | ||||||
|  |      * | ||||||
|  |      * @throws dml_exception If there is any problem changing Moodle settings | ||||||
|  |      */ | ||||||
|  |     protected function fix_moodle_setup() { | ||||||
|  |         global $CFG, $DB; | ||||||
|  | 
 | ||||||
|  |         // Configure Moodle settings to enable app web services.
 | ||||||
|  |         if (!$CFG->enablewebservices) { | ||||||
|  |             set_config('enablewebservices', 1); | ||||||
|  |         } | ||||||
|  |         if (!$CFG->enablemobilewebservice) { | ||||||
|  |             set_config('enablemobilewebservice', 1); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Add 'Create token' and 'Use REST webservice' permissions to authenticated user role.
 | ||||||
|  |         $userroleid = $DB->get_field('role', 'id', ['shortname' => 'user']); | ||||||
|  |         $systemcontext = \context_system::instance(); | ||||||
|  |         role_change_permission($userroleid, $systemcontext, 'moodle/webservice:createtoken', CAP_ALLOW); | ||||||
|  |         role_change_permission($userroleid, $systemcontext, 'webservice/rest:use', CAP_ALLOW); | ||||||
|  | 
 | ||||||
|  |         // Check the value of the 'webserviceprotocols' config option. Due to weird behaviour
 | ||||||
|  |         // in Behat with regard to config variables that aren't defined in a settings.php, the
 | ||||||
|  |         // value in $CFG here may reflect a previous run, so get it direct from the database
 | ||||||
|  |         // instead.
 | ||||||
|  |         $field = $DB->get_field('config', 'value', ['name' => 'webserviceprotocols'], IGNORE_MISSING); | ||||||
|  |         if (empty($field)) { | ||||||
|  |             $protocols = []; | ||||||
|  |         } else { | ||||||
|  |             $protocols = explode(',', $field); | ||||||
|  |         } | ||||||
|  |         if (!in_array('rest', $protocols)) { | ||||||
|  |             $protocols[] = 'rest'; | ||||||
|  |             set_config('webserviceprotocols', implode(',', $protocols)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Enable mobile service.
 | ||||||
|  |         require_once($CFG->dirroot . '/webservice/lib.php'); | ||||||
|  |         $webservicemanager = new webservice(); | ||||||
|  |         $service = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE, MUST_EXIST); | ||||||
|  | 
 | ||||||
|  |         if (!$service->enabled) { | ||||||
|  |             $service->enabled = 1; | ||||||
|  |             $webservicemanager->update_external_service($service); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Starts an Ionic server if necessary, or uses an existing one. | ||||||
|  |      * | ||||||
|  |      * @return string URL to Ionic server | ||||||
|  |      * @throws DriverException If there's a system error starting Ionic | ||||||
|  |      */ | ||||||
|  |     protected function start_or_reuse_ionic() { | ||||||
|  |         global $CFG; | ||||||
|  | 
 | ||||||
|  |         if (empty($CFG->behat_ionic_dirroot) && !empty($CFG->behat_ionic_wwwroot)) { | ||||||
|  |             // Use supplied Ionic server which should already be running.
 | ||||||
|  |             $url = $CFG->behat_ionic_wwwroot; | ||||||
|  |         } else if (self::$ionicrunning) { | ||||||
|  |             // Use existing Ionic instance launched previously.
 | ||||||
|  |             $url = self::$ionicrunning->url; | ||||||
|  |         } else { | ||||||
|  |             // Open Ionic process in relevant path.
 | ||||||
|  |             $path = realpath($CFG->behat_ionic_dirroot); | ||||||
|  |             $stderrfile = $CFG->dataroot . '/behat/ionic-stderr.log'; | ||||||
|  |             $prefix = ''; | ||||||
|  |             // Except on Windows, use 'exec' so that we get the pid of the actual Node process
 | ||||||
|  |             // and not the shell it uses to execute. You can't do exec on Windows; there is a
 | ||||||
|  |             // bypass_shell option but it is not the same thing and isn't usable here.
 | ||||||
|  |             if (!self::is_windows()) { | ||||||
|  |                 $prefix = 'exec '; | ||||||
|  |             } | ||||||
|  |             $process = proc_open($prefix . 'ionic serve --no-interactive --no-open', | ||||||
|  |                     [['pipe', 'r'], ['pipe', 'w'], ['file', $stderrfile, 'w']], $pipes, $path); | ||||||
|  |             if ($process === false) { | ||||||
|  |                 throw new DriverException('Error starting Ionic process'); | ||||||
|  |             } | ||||||
|  |             fclose($pipes[0]); | ||||||
|  | 
 | ||||||
|  |             // Get pid - we will need this to kill the process.
 | ||||||
|  |             $status = proc_get_status($process); | ||||||
|  |             $pid = $status['pid']; | ||||||
|  | 
 | ||||||
|  |             // Read data from stdout until the server comes online.
 | ||||||
|  |             // Note: On Windows it is impossible to read simultaneously from stderr and stdout
 | ||||||
|  |             // because stream_select and non-blocking I/O don't work on process pipes, so that is
 | ||||||
|  |             // why stderr was redirected to a file instead. Also, this code is simpler.
 | ||||||
|  |             $url = null; | ||||||
|  |             $stdoutlog = ''; | ||||||
|  |             while (true) { | ||||||
|  |                 $line = fgets($pipes[1], 4096); | ||||||
|  |                 if ($line === false) { | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 $stdoutlog .= $line; | ||||||
|  | 
 | ||||||
|  |                 if (preg_match('~^\s*Local: (http\S*)~', $line, $matches)) { | ||||||
|  |                     $url = $matches[1]; | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // If it failed, close the pipes and the process.
 | ||||||
|  |             if (!$url) { | ||||||
|  |                 fclose($pipes[1]); | ||||||
|  |                 proc_close($process); | ||||||
|  |                 $logpath = $CFG->dataroot . '/behat/ionic-start.log'; | ||||||
|  |                 $stderrlog = file_get_contents($stderrfile); | ||||||
|  |                 @unlink($stderrfile); | ||||||
|  |                 file_put_contents($logpath, | ||||||
|  |                         "Ionic startup log from " . date('c') . | ||||||
|  |                         "\n\n----STDOUT----\n$stdoutlog\n\n----STDERR----\n$stderrlog"); | ||||||
|  |                 throw new DriverException('Unable to start Ionic. See ' . $logpath); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Remember the URL, so we can reuse it next time, and other details so we can kill
 | ||||||
|  |             // the process.
 | ||||||
|  |             self::$ionicrunning = (object)['url' => $url, 'process' => $process, 'pipes' => $pipes, | ||||||
|  |                     'pid' => $pid]; | ||||||
|  |             $url = self::$ionicrunning->url; | ||||||
|  |         } | ||||||
|  |         return $url; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Closes Ionic (if it was started) at end of test suite. | ||||||
|  |      * | ||||||
|  |      * @AfterSuite | ||||||
|  |      */ | ||||||
|  |     public static function close_ionic() { | ||||||
|  |         if (self::$ionicrunning) { | ||||||
|  |             fclose(self::$ionicrunning->pipes[1]); | ||||||
|  | 
 | ||||||
|  |             if (self::is_windows()) { | ||||||
|  |                 // Using proc_terminate here does not work. It terminates the process but not any
 | ||||||
|  |                 // other processes it might have launched. Instead, we need to use an OS-specific
 | ||||||
|  |                 // mechanism to kill the process and children based on its pid.
 | ||||||
|  |                 exec('taskkill /F /T /PID ' . self::$ionicrunning->pid); | ||||||
|  |             } else { | ||||||
|  |                 // On Unix this actually works, although only due to the 'exec' command inserted
 | ||||||
|  |                 // above.
 | ||||||
|  |                 proc_terminate(self::$ionicrunning->process); | ||||||
|  |             } | ||||||
|  |             self::$ionicrunning = null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Goes to the app page and then sets up some initial JavaScript so we can use it. | ||||||
|  |      * | ||||||
|  |      * @param string $url App URL | ||||||
|  |      * @throws DriverException If the app fails to load properly | ||||||
|  |      */ | ||||||
|  |     protected function prepare_browser(array $options = []) { | ||||||
|  |         $restart = $options['restart'] ?? true; | ||||||
|  | 
 | ||||||
|  |         if ($restart) { | ||||||
|  |             if ($this->apprunning) { | ||||||
|  |                 $this->notify_unload(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Restart the browser and set its size.
 | ||||||
|  |             $this->getSession()->restart(); | ||||||
|  |             $this->resize_window($this->windowsize, true); | ||||||
|  | 
 | ||||||
|  |             if (empty($this->ionicurl)) { | ||||||
|  |                 $this->ionicurl = $this->start_or_reuse_ionic(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Visit the Ionic URL.
 | ||||||
|  |             $this->getSession()->visit($this->ionicurl); | ||||||
|  |             $this->notify_load(); | ||||||
|  | 
 | ||||||
|  |             $this->apprunning = true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Wait the application to load.
 | ||||||
|  |         $this->spin(function($context) { | ||||||
|  |             $title = $context->getSession()->getPage()->find('xpath', '//title'); | ||||||
|  | 
 | ||||||
|  |             if ($title) { | ||||||
|  |                 $text = $title->getHtml(); | ||||||
|  | 
 | ||||||
|  |                 if ($text === 'Moodle App') { | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             throw new DriverException('Moodle App not found in browser'); | ||||||
|  |         }, false, 60); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             // Init Behat JavaScript runtime.
 | ||||||
|  | 
 | ||||||
|  |             $initOptions = new StdClass(); | ||||||
|  |             $initOptions->skipOnBoarding = $options['skiponboarding'] ?? true; | ||||||
|  |             $initOptions->configOverrides = $this->appconfig; | ||||||
|  | 
 | ||||||
|  |             $this->execute_script('window.behatInit(' . json_encode($initOptions) . ');'); | ||||||
|  |         } catch (Exception $error) { | ||||||
|  |             throw new DriverException('Moodle App not running or not running on Automated mode.'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($restart) { | ||||||
|  |             // Assert initial page.
 | ||||||
|  |             $this->spin(function($context) { | ||||||
|  |                 $page = $context->getSession()->getPage(); | ||||||
|  |                 $element = $page->find('xpath', '//page-core-login-site//input[@name="url"]'); | ||||||
|  | 
 | ||||||
|  |                 if ($element) { | ||||||
|  |                     // Login screen found.
 | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if ($page->find('xpath', '//page-core-mainmenu')) { | ||||||
|  |                     // Main menu found.
 | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 throw new DriverException('Moodle App not launched properly'); | ||||||
|  |             }, false, 60); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Continue only after JS finishes.
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Parse an element locator string. | ||||||
|  |      * | ||||||
|  |      * @param string $text Element locator string. | ||||||
|  |      * @return JSON of the locator. | ||||||
|  |      */ | ||||||
|  |     public function parse_element_locator(string $text): string { | ||||||
|  |         preg_match( | ||||||
|  |             '/^"((?:[^"]|\\")*?)"(?: "([^"]*?)")?(?: (near|within) "((?:[^"]|\\")*?)"(?: "([^"]*?)")?)?$/', | ||||||
|  |             $text, | ||||||
|  |             $matches | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         $locator = [ | ||||||
|  |             'text' => str_replace('\\"', '"', $matches[1]), | ||||||
|  |             'selector' => $matches[2] ?? null, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         if (!empty($matches[3])) { | ||||||
|  |             $locator[$matches[3]] = (object) [ | ||||||
|  |                 'text' => str_replace('\\"', '"', $matches[4]), | ||||||
|  |                 'selector' => $matches[5] ?? null, | ||||||
|  |             ]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return json_encode((object) $locator); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Replaces $WWWROOT for the url of the Moodle site. | ||||||
|  |      * | ||||||
|  |      * @Transform /^(.*\$WWWROOT.*)$/ | ||||||
|  |      * @param string $text Text. | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     public function replace_wwwroot($text) { | ||||||
|  |         global $CFG; | ||||||
|  | 
 | ||||||
|  |         return str_replace('$WWWROOT', $CFG->behat_wwwroot, $text); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Replace arguments with the format "${activity:field}" from a string, where "activity" is | ||||||
|  |      * the idnumber of an activity and "field" is the activity's field to get replacement from. | ||||||
|  |      * | ||||||
|  |      * At the moment, the only field supported is "cmid", the id of the course module for this activity. | ||||||
|  |      * | ||||||
|  |      * @param string $text Original text. | ||||||
|  |      * @return string Text with arguments replaced. | ||||||
|  |      */ | ||||||
|  |     protected function replace_arguments(string $text): string { | ||||||
|  |         global $DB; | ||||||
|  | 
 | ||||||
|  |         preg_match_all("/\\$\\{([^:}]+):([^}]+)\\}/", $text, $matches); | ||||||
|  | 
 | ||||||
|  |         foreach ($matches[0] as $index => $match) { | ||||||
|  |             switch ($matches[2][$index]) { | ||||||
|  |                 case 'cmid': | ||||||
|  |                     $coursemodule = $DB->get_record('course_modules', ['idnumber' => $matches[1][$index]]); | ||||||
|  |                     $text = str_replace($match, $coursemodule->id, $text); | ||||||
|  | 
 | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $text; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Notify to listeners that the app was just loaded. | ||||||
|  |      */ | ||||||
|  |     protected function notify_load(): void { | ||||||
|  |         foreach (self::$listeners as $listener) { | ||||||
|  |             $listener->on_app_load(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Notify to listeners that the app is about to be unloaded. | ||||||
|  |      */ | ||||||
|  |     protected function notify_unload(): void { | ||||||
|  |         foreach (self::$listeners as $listener) { | ||||||
|  |             $listener->on_app_unload(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Trigger Angular change detection. | ||||||
|  |      */ | ||||||
|  |     protected function trigger_angular_change_detection() { | ||||||
|  |         $this->getSession()->executeScript('ngZone.run(() => {});'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Evaluate a script that returns a Promise. | ||||||
|  |      * | ||||||
|  |      * @param string $script | ||||||
|  |      * @return mixed Resolved promise result. | ||||||
|  |      */ | ||||||
|  |     protected function evaluate_async_script(string $script) { | ||||||
|  |         $script = preg_replace('/^return\s+/', '', $script); | ||||||
|  |         $script = preg_replace('/;$/', '', $script); | ||||||
|  |         $start = microtime(true); | ||||||
|  |         $promisevariable = 'PROMISE_RESULT_' . time(); | ||||||
|  |         $timeout = self::get_timeout(); | ||||||
|  | 
 | ||||||
|  |         $this->evaluate_script("Promise.resolve($script)
 | ||||||
|  |             .then(result => window.$promisevariable = result) | ||||||
|  |             .catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message);");
 | ||||||
|  | 
 | ||||||
|  |         do { | ||||||
|  |             if (microtime(true) - $start > $timeout) { | ||||||
|  |                 throw new DriverException("Async script not resolved after $timeout seconds"); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             usleep(100000); | ||||||
|  |         } while (!$this->evaluate_script("return '$promisevariable' in window;")); | ||||||
|  | 
 | ||||||
|  |         $result = $this->evaluate_script("return window.$promisevariable;"); | ||||||
|  | 
 | ||||||
|  |         $this->evaluate_script("delete window.$promisevariable;"); | ||||||
|  | 
 | ||||||
|  |         return $result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Opens a custom URL for automatic login and redirect from the Moodle App (and waits to finish.) | ||||||
|  |      * | ||||||
|  |      * @param string $username Of the user that needs to be logged in. | ||||||
|  |      * @param string $path To redirect the user. | ||||||
|  |      * @param string $successXPath If a path is declared, the XPath of the element to lookat after redirect. | ||||||
|  |      */ | ||||||
|  |     protected function open_moodleapp_custom_login_url($username, $path = '', string $successXPath = '') { | ||||||
|  |         global $CFG, $DB; | ||||||
|  | 
 | ||||||
|  |         require_once($CFG->libdir.'/externallib.php'); | ||||||
|  |         require_once($CFG->libdir.'/moodlelib.php'); | ||||||
|  | 
 | ||||||
|  |         // Ensure the user exists.
 | ||||||
|  |         $userid = $DB->get_field('user', 'id', [ 'username' => $username ]); | ||||||
|  |         if (!$userid) { | ||||||
|  |             throw new DriverException("User '$username' not found"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Get or create the user token.
 | ||||||
|  |         $service = $DB->get_record('external_services', ['shortname' => 'moodle_mobile_app']); | ||||||
|  | 
 | ||||||
|  |         $token_params = [ | ||||||
|  |             'userid' => $userid, | ||||||
|  |             'externalserviceid' => $service->id, | ||||||
|  |         ]; | ||||||
|  |         $usertoken = $DB->get_record('external_tokens', $token_params); | ||||||
|  |         if (!$usertoken) { | ||||||
|  |             $context = context_system::instance(); | ||||||
|  |             $token = external_generate_token(EXTERNAL_TOKEN_PERMANENT, $service, $userid, $context); | ||||||
|  |             $token_params['token'] = $token; | ||||||
|  |             $privatetoken = $DB->get_field('external_tokens', 'privatetoken', $token_params); | ||||||
|  |         } else { | ||||||
|  |             $token = $usertoken->token; | ||||||
|  |             $privatetoken = $usertoken->privatetoken; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Generate custom URL.
 | ||||||
|  |         $parsed_url = parse_url($CFG->behat_wwwroot); | ||||||
|  |         $domain = $parsed_url['host']; | ||||||
|  |         $url = $this->get_mobile_url_scheme() . "://$username@$domain?token=$token&privatetoken=$privatetoken"; | ||||||
|  | 
 | ||||||
|  |         if (!empty($path)) { | ||||||
|  |             $url .= '&redirect='.urlencode($CFG->behat_wwwroot.$path); | ||||||
|  |         } else { | ||||||
|  |             $successXPath = '//page-core-mainmenu'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $this->handle_url_and_wait_page_to_load($url, $successXPath); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Opens a custom URL on the Moodle App (and waits to finish.) | ||||||
|  |      * | ||||||
|  |      * @param string $path To navigate. | ||||||
|  |      * @param string $successXPath The XPath of the element to lookat after navigation. | ||||||
|  |      */ | ||||||
|  |     protected function open_moodleapp_custom_url(string $path, string $successXPath = '') { | ||||||
|  |         global $CFG; | ||||||
|  | 
 | ||||||
|  |         $urlscheme = $this->get_mobile_url_scheme(); | ||||||
|  |         $url = "$urlscheme://link=" . urlencode($CFG->behat_wwwroot.$path); | ||||||
|  | 
 | ||||||
|  |         $this->handle_url_and_wait_page_to_load($url); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles the custom URL on the Moodle App (and waits to finish.) | ||||||
|  |      * | ||||||
|  |      * @param string $customurl To navigate. | ||||||
|  |      * @param string $successXPath The XPath of the element to lookat after navigation. | ||||||
|  |      */ | ||||||
|  |     protected function handle_url_and_wait_page_to_load(string $customurl, string $successXPath = '') { | ||||||
|  |         // Instead of using evaluate_async_script, we wait for the path to load.
 | ||||||
|  |         $this->evaluate_script("return window.behat.handleCustomURL('$customurl')"); | ||||||
|  | 
 | ||||||
|  |         $this->wait_for_pending_js(); | ||||||
|  | 
 | ||||||
|  |         if (!empty($successXPath)) { | ||||||
|  |             // Wait until the page appears.
 | ||||||
|  |             $this->spin( | ||||||
|  |                 function($context, $args) use ($successXPath) { | ||||||
|  |                     $found = $context->getSession()->getPage()->find('xpath', $successXPath); | ||||||
|  |                     if ($found) { | ||||||
|  |                         return true; | ||||||
|  |                     } | ||||||
|  |                     throw new DriverException('Moodle App custom URL page not loaded'); | ||||||
|  |                 }, false, 30); | ||||||
|  | 
 | ||||||
|  |             // Wait for JS to finish as well.
 | ||||||
|  |             $this->wait_for_pending_js(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns the current mobile url scheme of the site. | ||||||
|  |      */ | ||||||
|  |     protected function get_mobile_url_scheme() { | ||||||
|  |         $mobilesettings = get_config('tool_mobile'); | ||||||
|  | 
 | ||||||
|  |         return !empty($mobilesettings->forcedurlscheme) ? $mobilesettings->forcedurlscheme : 'moodlemobile'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get a coursemodule from an activity name or idnumber with course. | ||||||
|  |      * | ||||||
|  |      * @param string $activity | ||||||
|  |      * @param string $identifier | ||||||
|  |      * @param string $coursename | ||||||
|  |      * @return cm_info | ||||||
|  |      */ | ||||||
|  |     protected function get_cm_by_activity_name_and_course(string $activity, string $identifier, string $coursename): cm_info { | ||||||
|  |         global $DB; | ||||||
|  | 
 | ||||||
|  |         $courseid = $this->get_course_id($coursename); | ||||||
|  |         if (!$courseid) { | ||||||
|  |             throw new DriverException("Course '$coursename' not found"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($activity === 'assignment') { | ||||||
|  |             $activity = 'assign'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $cmtable = new \core\dml\table('course_modules', 'cm', 'cm'); | ||||||
|  |         $cmfrom = $cmtable->get_from_sql(); | ||||||
|  | 
 | ||||||
|  |         $acttable = new \core\dml\table($activity, 'a', 'a'); | ||||||
|  |         $actselect = $acttable->get_field_select(); | ||||||
|  |         $actfrom = $acttable->get_from_sql(); | ||||||
|  | 
 | ||||||
|  |         $sql = <<<EOF | ||||||
|  |     SELECT cm.id as cmid | ||||||
|  |       FROM {$cmfrom} | ||||||
|  | INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname | ||||||
|  | INNER JOIN {$actfrom} ON cm.instance = a.id AND cm.course = :courseid | ||||||
|  |      WHERE cm.idnumber = :idnumber OR a.name = :name | ||||||
|  | EOF; | ||||||
|  | 
 | ||||||
|  |         $result = $DB->get_record_sql($sql, [ | ||||||
|  |             'modname' => $activity, | ||||||
|  |             'idnumber' => $identifier, | ||||||
|  |             'name' => $identifier, | ||||||
|  |             'courseid' => $courseid, | ||||||
|  |         ], MUST_EXIST); | ||||||
|  | 
 | ||||||
|  |         return get_fast_modinfo($courseid)->get_cm($result->cmid); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -18,6 +18,8 @@ use Behat\Mink\Exception\DriverException; | |||||||
| use Facebook\WebDriver\Exception\InvalidArgumentException; | use Facebook\WebDriver\Exception\InvalidArgumentException; | ||||||
| use Moodle\BehatExtension\Driver\WebDriver; | use Moodle\BehatExtension\Driver\WebDriver; | ||||||
| 
 | 
 | ||||||
|  | require_once(__DIR__ . '/../behat_app.php'); | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Performance measures for one particular metric. |  * Performance measures for one particular metric. | ||||||
|  */ |  */ | ||||||
| @ -334,7 +336,7 @@ class performance_measure implements behat_app_listener { | |||||||
|                     "            ],", |                     "            ],", | ||||||
|                     "        ],", |                     "        ],", | ||||||
|                     "    ],", |                     "    ],", | ||||||
|                     ");", |                     "];", | ||||||
|                     "", |                     "", | ||||||
|                 ]) |                 ]) | ||||||
|             ); |             ); | ||||||
| @ -46,23 +46,20 @@ async function main() { | |||||||
| 
 | 
 | ||||||
|     // Copy plugin template.
 |     // Copy plugin template.
 | ||||||
|     const { version: appVersion } = require(projectPath('package.json')); |     const { version: appVersion } = require(projectPath('package.json')); | ||||||
|     const templatePath = projectPath('scripts/templates/behat-plugin'); |     const templatePath = projectPath('local-moodleappbehat'); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     copySync(templatePath, pluginPath); | ||||||
|  | 
 | ||||||
|  |     // Update version.php
 | ||||||
|  |     const pluginFilePath = pluginPath + '/version.php'; | ||||||
|  |     const fileContents = readFileSync(pluginFilePath).toString(); | ||||||
|  | 
 | ||||||
|     const replacements = { |     const replacements = { | ||||||
|         appVersion, |         appVersion, | ||||||
|         pluginVersion: getMoodlePluginVersion(), |         pluginVersion: getMoodlePluginVersion(), | ||||||
|     }; |     }; | ||||||
| 
 |  | ||||||
|     copySync(templatePath, pluginPath); |  | ||||||
| 
 |  | ||||||
|     for await (const templateFilePath of getDirectoryFiles(templatePath)) { |  | ||||||
|         const pluginFilePath = pluginPath + templateFilePath.substr(templatePath.length); |  | ||||||
|         const fileContents = readFileSync(pluginFilePath).toString(); |  | ||||||
| 
 |  | ||||||
|     writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements)); |     writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements)); | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Copy plugin files.
 |  | ||||||
|     copySync(projectPath('tests/behat'), `${pluginPath}/tests/behat`); |  | ||||||
| 
 | 
 | ||||||
|     // Copy feature files.
 |     // Copy feature files.
 | ||||||
|     const behatTempFeaturesPath = `${pluginPath}/behat-tmp`; |     const behatTempFeaturesPath = `${pluginPath}/behat-tmp`; | ||||||
| @ -80,7 +77,8 @@ async function main() { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const newPath = featurePath.substring(0, featurePath.length - ('/tests/behat'.length)); |         const newPath = featurePath.substring(0, featurePath.length - ('/tests/behat'.length)); | ||||||
|         const prefix = relative(behatTempFeaturesPath, newPath).replace('/','-') || 'core'; |         const searchRegExp = new RegExp('/', 'g'); | ||||||
|  |         const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core'; | ||||||
|         const featureFilename = prefix + '-' + basename(featureFile); |         const featureFilename = prefix + '-' + basename(featureFile); | ||||||
|         renameSync(featureFile, behatFeaturesPath + '/' + featureFilename); |         renameSync(featureFile, behatFeaturesPath + '/' + featureFilename); | ||||||
|     } |     } | ||||||
|  | |||||||
							
								
								
									
										396
									
								
								src/addons/messages/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										396
									
								
								src/addons/messages/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,396 @@ | |||||||
|  | @core @core_message @app @javascript | ||||||
|  | Feature: Test basic usage of messages in app | ||||||
|  |   In order to participate with messages while using the mobile app | ||||||
|  |   As a student | ||||||
|  |   I need basic message functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname  | lastname  | email                | | ||||||
|  |       | teacher1 | Teacher    | teacher   | teacher1@example.com | | ||||||
|  |       | student1 | Student1   | student1  | student1@example.com | | ||||||
|  |       | student2 | Student2   | student2  | student2@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  |       | student2 | C1 | student | | ||||||
|  | 
 | ||||||
|  |   Scenario: View recent conversations and contacts | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Contacts" in the app | ||||||
|  |     Then I should find "No contacts" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I set the field "New message" to "heeey student" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     And I press "Display options" in the app | ||||||
|  |     And I press "Add to contacts" in the app | ||||||
|  |     And I press "Add" near "Are you sure you want to add Student1 student1 to your contacts?" in the app | ||||||
|  |     Then I should find "Contact request sent" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Contacts" in the app | ||||||
|  |     And I press "Requests" in the app | ||||||
|  |     And I press "Teacher teacher" in the app | ||||||
|  |     And I press "Accept and add to contacts" in the app | ||||||
|  |     Then I should not find "Teacher teacher would like to contact you" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Contacts" near "No contact requests" in the app | ||||||
|  |     Then the header should be "Contacts" in the app | ||||||
|  |     And I should find "Teacher teacher" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Teacher teacher" in the app | ||||||
|  |     Then the header should be "Teacher teacher" in the app | ||||||
|  |     And I should find "heeey student" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Search users | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student2" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     Then I should find "Student2 student2" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "Search" to "Teacher" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     Then I should find "Teacher teacher" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Send/receive messages in existing conversations | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Contacts" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student1" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I set the field "New message" to "heeey student" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Contacts" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "teacher" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Teacher teacher" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "New message" to "hi" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "hi" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student1" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  |     And I should find "hi" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "New message" to "byee" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  |     And I should find "hi" in the app | ||||||
|  |     And I should find "byee" in the app | ||||||
|  | 
 | ||||||
|  |   # TODO Fix this test in all Moodle versions | ||||||
|  |   Scenario: User profile: send message, add/remove contact | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Contacts" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I set the field "New message" to "heeey student" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" in the app | ||||||
|  |     And I press "Add to contacts" in the app | ||||||
|  |     And I press "Add" in the app | ||||||
|  |     Then I should find "Contact request sent" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Contacts" in the app | ||||||
|  |     And I press "Requests" in the app | ||||||
|  |     And I press "Teacher teacher" in the app | ||||||
|  |     Then I should find "Teacher teacher would like to contact you" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Accept and add to contacts" in the app | ||||||
|  |     Then I should not find "Teacher teacher would like to contact you" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" in the app | ||||||
|  |     And I press "User info" in the app | ||||||
|  |     And I press "Message" in the app | ||||||
|  |     And I set the field "New message" to "hi" in the app | ||||||
|  |     And I press "Send" "button" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  |     And I should find "hi" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" in the app | ||||||
|  |     And I press "Remove from contacts" in the app | ||||||
|  |     And I press "Remove" in the app | ||||||
|  |     And I wait loading to finish in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Display options" in the app | ||||||
|  |     Then I should find "Add to contacts" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Delete conversation" in the app | ||||||
|  |     And I press "Delete" near "Are you sure you would like to delete this entire conversation?" in the app | ||||||
|  |     Then I should not find "heeey student" in the app | ||||||
|  |     And I should not find "hi" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Send message offline | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Contacts" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student1" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I set the field "New message" to "heeey student" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "New message" to "byee" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "byee" in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  |     And I should find "byee" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Teacher teacher" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  |     And I should find "byee" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Auto-sync messages | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Contacts" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student1" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I set the field "New message" to "heeey student" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     And I set the field "New message" to "byee" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "byee" in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I run cron tasks in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Teacher teacher" in the app | ||||||
|  |     Then I should find "heeey student" in the app | ||||||
|  |     And I should find "byee" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Search for messages | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student1" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I set the field "New message" to "test message" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "test message" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "New message" to "search this message" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "search this message" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "search this message" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     Then I should find "Messages" in the app | ||||||
|  |     And I should find "search this message" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "search this message" near "Teacher teacher" in the app | ||||||
|  |     Then I should find "test message" in the app | ||||||
|  |     And I should find "search this message" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Star/Unstar | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student1" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I set the field "New message" to "star message" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "star message" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student1" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I set the field "New message" to "test message student2" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "test message student2" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     Then I should find "Private (2)" in the app | ||||||
|  |     And I should find "Starred (1)" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "star message" in the app | ||||||
|  |     And I press "Display options" in the app | ||||||
|  |     And I press "Star conversation" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     Then I should find "Private (1)" in the app | ||||||
|  |     And I should find "Starred (2)" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Starred (2)" in the app | ||||||
|  |     Then I should find "Teacher teacher" in the app | ||||||
|  |     And I should find "Student1 student1" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: User blocking feature | ||||||
|  |     Given I entered the course "Course 1" as "student2" in the app | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I press "Message" in the app | ||||||
|  |     And I press "Display options" in the app | ||||||
|  |     And I press "Block user" in the app | ||||||
|  |     And I press "Block user" near "Are you sure you want to block Student1 student1?" in the app | ||||||
|  |     Then I should find "You have blocked this user" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Student2 student2" in the app | ||||||
|  |     And I press "Message" in the app | ||||||
|  |     Then I should find "You are unable to message this user" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the course "Course 1" as "student2" in the app | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I press "Message" in the app | ||||||
|  |     And I press "Display options" in the app | ||||||
|  |     Then I should find "Unblock user" in the app | ||||||
|  |     But I should not find "Block user" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Unblock user" in the app | ||||||
|  |     And I press "Unblock user" near "Are you sure you want to unblock Student1 student1?" in the app | ||||||
|  |     Then I should not find "You have blocked this user" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Student2 student2" in the app | ||||||
|  |     And I press "Message" in the app | ||||||
|  |     And I set the field "New message" to "test message" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "test message" in the app | ||||||
|  |     But I should not find "You are unable to message this user" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Mute Unmute conversations | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Student2 student2" in the app | ||||||
|  |     And I press "Message" in the app | ||||||
|  |     And I set the field "New message" to "test message" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "test message" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" in the app | ||||||
|  |     And I press "Mute" in the app | ||||||
|  |     Then I should find "Muted conversation" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" in the app | ||||||
|  |     And I press "Unmute" in the app | ||||||
|  |     Then I should not find "Muted conversation" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" in the app | ||||||
|  |     When I press "Mute" in the app | ||||||
|  |     Then I should find "Muted conversation" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Private (1)" in the app | ||||||
|  |     And I press "Student2 student2" in the app | ||||||
|  |     Then I should find "test message" in the app | ||||||
|  |     And I should find "Muted conversation" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Self conversations | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     Then I should find "Starred (1)" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Student1 student1" in the app | ||||||
|  |     And I set the field "New message" to "self conversation online" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "self conversation online" in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "true" | ||||||
|  |     And I set the field "New message" to "self conversation offline" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "self conversation offline" in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I press "Display options" in the app | ||||||
|  |     Then I should find "Show delete messages" in the app | ||||||
|  |     And I should find "Delete conversation" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Unstar conversation" in the app | ||||||
|  |     And I press "Display options" in the app | ||||||
|  |     Then I should find "Star conversation" in the app | ||||||
|  |     And I should find "Delete conversation" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Show delete messages" in the app | ||||||
|  |     And I close the popup in the app | ||||||
|  |     Then I should find "self conversation online" in the app | ||||||
|  |     And I should find "self conversation offline" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Delete message" near "self conversation offline" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "self conversation online" in the app | ||||||
|  |     But I should not find "self conversation offline" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" in the app | ||||||
|  |     And I press "Delete conversation" in the app | ||||||
|  |     And I press "Delete" near "Are you sure you would like to delete this entire personal conversation?" in the app | ||||||
|  |     Then I should not find "self conversation online" in the app | ||||||
|  |     And I should not find "self conversation offline" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "Student1 student1" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     And I set the field "New message" to "auto search test" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "auto search test" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Private" in the app | ||||||
|  |     And I press "Student1 student1" in the app | ||||||
|  |     Then I should find "auto search test" in the app | ||||||
							
								
								
									
										42
									
								
								src/addons/messages/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/addons/messages/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | @core @core_message @app @javascript | ||||||
|  | Feature: Test messages navigation in the app | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname  | | ||||||
|  |       | teacher  | Teacher    | | ||||||
|  |       | student  | Student    | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user    | course | role           | | ||||||
|  |       | teacher | C1     | editingteacher | | ||||||
|  |       | student | C1     | student        | | ||||||
|  | 
 | ||||||
|  |   Scenario: Avoid recursive links to profile | ||||||
|  |     Given I entered the app as "teacher" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Contacts" in the app | ||||||
|  |     And I press "Search people and messages" in the app | ||||||
|  |     And I set the field "Search" to "student" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     And I press "Student" in the app | ||||||
|  |     And I set the field "New message" to "Hi there" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "Hi there" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" in the app | ||||||
|  |     And I press "User info" in the app | ||||||
|  |     Then I should find "Details" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Message" in the app | ||||||
|  |     Then I should find "Hi there" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" in the app | ||||||
|  |     Then I should not find "User info" in the app | ||||||
|  | 
 | ||||||
|  |     When I close the popup in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     Then I should find "Hi there" in the app | ||||||
							
								
								
									
										17
									
								
								src/addons/messages/tests/behat/settings.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/addons/messages/tests/behat/settings.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | @core @core_message @app @javascript | ||||||
|  | Feature: Test messages settings | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  | 
 | ||||||
|  |   Scenario: Modify settings | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     And I press "Message preferences" in the app | ||||||
|  |     And I select "My contacts only" in the app | ||||||
|  |     Then "My contacts only" should be selected in the app | ||||||
|  | 
 | ||||||
|  |     And I select "My contacts and anyone in my courses" in the app | ||||||
|  |     Then "My contacts and anyone in my courses" should be selected in the app | ||||||
							
								
								
									
										58
									
								
								src/addons/mod/assign/tests/behat/basic_usage-310.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										58
									
								
								src/addons/mod/assign/tests/behat/basic_usage-310.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | @mod @mod_assign @app @javascript @lms_upto3.10 | ||||||
|  | Feature: Test basic usage of assignment activity in app | ||||||
|  |   In order to participate in the assignment while using the mobile app | ||||||
|  |   I need basic assignment functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email | | ||||||
|  |       | teacher1 | Teacher | teacher | teacher1@example.com | | ||||||
|  |       | student1 | Student | student | student1@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | course | idnumber | name                | intro                        | assignsubmission_onlinetext_enabled | duedate    | attemptreopenmethod | | ||||||
|  |       | assign   | C1     | assign1  | assignment1         | Test assignment description1 | 1                                   | 1029844800 | manual              | | ||||||
|  | 
 | ||||||
|  |   Scenario: View assign description, due date & View list of student submissions (as teacher) & View own submission or student submission | ||||||
|  |     # Create, edit and submit as a student | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app | ||||||
|  |     Then the header should be "assignment1" in the app | ||||||
|  |     And I should find "Test assignment description1" in the app | ||||||
|  |     And I should find "Due date" in the app | ||||||
|  |     And I should find "Tuesday, 20 August 2002, 12:00 PM" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Add submission" in the app | ||||||
|  |     And I set the field "Online text submissions" to "Submission test" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     Then I should find "Draft (not submitted)" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Edit submission" in the app | ||||||
|  |     And I set the field "Online text submissions" to "Submission test edited" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "Submission test edited" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submit assignment" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "Submitted for grading" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
|  |     And I should find "Submission test edited" in the app | ||||||
|  | 
 | ||||||
|  |     # View as a teacher | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "teacher1" in the app | ||||||
|  |     Then the header should be "assignment1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submitted" in the app | ||||||
|  |     Then I should find "Student student" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Student student" near "assignment1" in the app | ||||||
|  |     Then I should find "Online text submissions" in the app | ||||||
|  |     And I should find "Submission test edited" in the app | ||||||
							
								
								
									
										147
									
								
								src/addons/mod/assign/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										147
									
								
								src/addons/mod/assign/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,147 @@ | |||||||
|  | @mod @mod_assign @app @javascript | ||||||
|  | Feature: Test basic usage of assignment activity in app | ||||||
|  |   In order to participate in the assignment while using the mobile app | ||||||
|  |   I need basic assignment functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email | | ||||||
|  |       | teacher1 | Teacher | teacher | teacher1@example.com | | ||||||
|  |       | student1 | Student | student | student1@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | course | idnumber | name                | intro                        | assignsubmission_onlinetext_enabled | duedate    | attemptreopenmethod | | ||||||
|  |       | assign   | C1     | assign1  | assignment1         | Test assignment description1 | 1                                   | 1029844800 | manual              | | ||||||
|  | 
 | ||||||
|  |   @lms_from3.11 | ||||||
|  |   Scenario: View assign description, due date & View list of student submissions (as teacher) & View own submission or student submission | ||||||
|  |     # Create, edit and submit as a student | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app | ||||||
|  |     Then the header should be "assignment1" in the app | ||||||
|  |     And I should find "Test assignment description1" in the app | ||||||
|  |     And I should find "Due:" in the app | ||||||
|  |     And I should find "20 August 2002, 12:00 PM" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Add submission" in the app | ||||||
|  |     And I set the field "Online text submissions" to "Submission test" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     Then I should find "Draft (not submitted)" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Edit submission" in the app | ||||||
|  |     And I set the field "Online text submissions" to "Submission test edited" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "Submission test edited" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submit assignment" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "Submitted for grading" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
|  |     And I should find "Submission test edited" in the app | ||||||
|  | 
 | ||||||
|  |     # View as a teacher | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "teacher1" in the app | ||||||
|  |     Then the header should be "assignment1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submitted" in the app | ||||||
|  |     Then I should find "Student student" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Student student" near "assignment1" in the app | ||||||
|  |     Then I should find "Online text submissions" in the app | ||||||
|  |     And I should find "Submission test edited" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Edit/Add submission (online text) & Add new attempt from previous submission & Submit for grading | ||||||
|  |     # Submit first attempt as a student | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add submission" in the app | ||||||
|  |     Then I set the field "Online text submissions" to "Submission test 1st attempt" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I press "Submit assignment" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  | 
 | ||||||
|  |     # Allow more attempts as a teacher | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "teacher1" in the app | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Student student" near "assignment1" in the app | ||||||
|  |     And I press "Grade" in the app | ||||||
|  |     And I press "Allow another attempt" in the app | ||||||
|  |     And I press "Done" in the app | ||||||
|  |     Then I should find "Reopened" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
|  | 
 | ||||||
|  |     # Submit second attempt as a student | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app | ||||||
|  |     Then I should find "Reopened" in the app | ||||||
|  |     And I should find "2 out of Unlimited" in the app | ||||||
|  |     And I should find "Add a new attempt based on previous submission" in the app | ||||||
|  |     And I should find "Add a new attempt" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Add a new attempt based on previous submission" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "Submission test 1st attempt" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "Online text submissions" to "Submission test 2nd attempt" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     And I press "Submit assignment" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  | 
 | ||||||
|  |     # View second attempt as a teacher | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "teacher1" in the app | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Student student" near "assignment1" in the app | ||||||
|  |     Then I should find "Online text submissions" in the app | ||||||
|  |     And I should find "Submission test 2nd attempt" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Add submission offline (online text) & Submit for grading offline & Sync submissions | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add submission" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I set the field "Online text submissions" to "Submission test" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I press "Submit assignment" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "This Assignment has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "assignment1" in the app | ||||||
|  |     And I press "Information" in the app | ||||||
|  |     And I press "Refresh" in the app | ||||||
|  |     Then I should find "Submitted for grading" in the app | ||||||
|  |     But I should not find "This Assignment has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Edit an offline submission before synchronising it | ||||||
|  |     Given I entered the assign activity "assignment1" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add submission" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I set the field "Online text submissions" to "Submission test original offline" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     Then I should find "This Assignment has offline data to be synchronised." in the app | ||||||
|  |     And I should find "Submission test original offline" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Edit submission" in the app | ||||||
|  |     And I set the field "Online text submissions" to "Submission test edited offline" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     Then I should find "This Assignment has offline data to be synchronised." in the app | ||||||
|  |     And I should find "Submission test edited offline" in the app | ||||||
|  |     But I should not find "Submission test original offline" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submit assignment" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "This Assignment has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "assignment1" in the app | ||||||
|  |     Then I should find "Submitted for grading" in the app | ||||||
|  |     And I should find "Submission test edited offline" in the app | ||||||
|  |     But I should not find "This Assignment has offline data to be synchronised." in the app | ||||||
							
								
								
									
										224
									
								
								src/addons/mod/assign/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								src/addons/mod/assign/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,224 @@ | |||||||
|  | @mod @mod_assign @app @javascript | ||||||
|  | Feature: Test assignments navigation | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | | ||||||
|  |       | teacher1 | Teacher   | teacher  | | ||||||
|  |       | student1 | First     | Student  | | ||||||
|  |       | student2 | Second    | Student  | | ||||||
|  |       | student3 | Third     | Student  | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role           | | ||||||
|  |       | teacher1 | C1     | editingteacher | | ||||||
|  |       | student1 | C1     | student        | | ||||||
|  |       | student2 | C1     | student        | | ||||||
|  |       | student3 | C1     | student        | | ||||||
|  |     And the following "groups" exist: | ||||||
|  |       | name    | course | idnumber | | ||||||
|  |       | Group 1 | C1     | G1       | | ||||||
|  |       | Group 2 | C1     | G2       | | ||||||
|  |     And the following "group members" exist: | ||||||
|  |       | user     | group | | ||||||
|  |       | student1 | G1    | | ||||||
|  |       | student2 | G1    | | ||||||
|  |       | student2 | G2    | | ||||||
|  |       | student3 | G2    | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name       | course | idnumber   | assignsubmission_onlinetext_enabled | duedate | groupmode | | ||||||
|  |       | assign   | Assignment | C1     | assignment | 1                                   | 0       | 1         | | ||||||
|  |     And the following "mod_assign > submissions" exist: | ||||||
|  |       | assign     | user      | onlinetext | | ||||||
|  |       | assignment | student1  | Lorem      | | ||||||
|  |       | assignment | student3  | Ipsum      | | ||||||
|  | 
 | ||||||
|  |   Scenario: Mobile navigation on assignment | ||||||
|  |     Given I entered the course "Course 1" as "teacher1" in the app | ||||||
|  | 
 | ||||||
|  |     # Initial status | ||||||
|  |     When I press "Assignment" in the app | ||||||
|  |     Then I should find "3" near "Participants" in the app | ||||||
|  |     And I should find "2" near "Drafts" in the app | ||||||
|  | 
 | ||||||
|  |     # Participants | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     And I should find "Second Student" in the app | ||||||
|  |     And I should find "Third Student" in the app | ||||||
|  | 
 | ||||||
|  |     # Participants — swipe | ||||||
|  |     When I press "First Student" in the app | ||||||
|  |     And I swipe to the right in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     But I should not find "Second Student" in the app | ||||||
|  |     And I should not find "Third Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Second Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Third Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Third Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Second Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Third Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Second Student" in the app | ||||||
|  | 
 | ||||||
|  |     # Drafts | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Drafts" in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     And I should find "Third Student" in the app | ||||||
|  |     But I should not find "Second Student" in the app | ||||||
|  | 
 | ||||||
|  |     # Drafts — swipe | ||||||
|  |     When I press "First Student" in the app | ||||||
|  |     And I swipe to the right in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     But I should not find "Second Student" in the app | ||||||
|  |     And I should not find "Third Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Third Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Second Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Third Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Second Student" in the app | ||||||
|  | 
 | ||||||
|  |     # Filter groups in assignment page | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Separate groups" in the app | ||||||
|  |     And I press "Group 1" in the app | ||||||
|  |     Then I should find "2" near "Participants" in the app | ||||||
|  |     And I should find "1" near "Drafts" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     And I should find "Second Student" in the app | ||||||
|  |     But I should not find "Third Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "First Student" in the app | ||||||
|  |     And I swipe to the right in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     But I should not find "Second Student" in the app | ||||||
|  |     And I should not find "Third Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Second Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Third Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Second Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Third Student" in the app | ||||||
|  | 
 | ||||||
|  |     # Filter groups in submissions page | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Separate groups" in the app | ||||||
|  |     And I press "Group 2" in the app | ||||||
|  |     Then I should find "Second Student" in the app | ||||||
|  |     And I should find "Third Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Second Student" in the app | ||||||
|  |     And I swipe to the right in the app | ||||||
|  |     Then I should find "Second Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Third Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Third Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Second Student" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Third Student" in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Second Student" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Tablet navigation on assignment | ||||||
|  |     Given I entered the course "Course 1" as "teacher1" in the app | ||||||
|  |     And I change viewport size to "1200x640" | ||||||
|  | 
 | ||||||
|  |     # Initial status | ||||||
|  |     When I press "Assignment" in the app | ||||||
|  |     Then I should find "3" near "Participants" in the app | ||||||
|  |     And I should find "2" near "Drafts" in the app | ||||||
|  | 
 | ||||||
|  |     # Participants | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     And I should find "Second Student" in the app | ||||||
|  |     And I should find "Third Student" in the app | ||||||
|  |     And "First Student" near "Third Student" should be selected in the app | ||||||
|  |     And I should find "First Student" inside the split-view content in the app | ||||||
|  |     But I should not find "Second Student" inside the split-view content in the app | ||||||
|  |     And I should not find "Third Student" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     # Participants — Split view | ||||||
|  |     When I press "Second Student" in the app | ||||||
|  |     Then "Second Student" near "Third Student" should be selected in the app | ||||||
|  |     And I should find "Second Student" inside the split-view content in the app | ||||||
|  |     But I should not find "First Student" inside the split-view content in the app | ||||||
|  |     And I should not find "Third Student" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     # Drafts | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Drafts" in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     And I should find "Third Student" in the app | ||||||
|  |     And "First Student" near "Third Student" should be selected in the app | ||||||
|  |     And I should find "First Student" inside the split-view content in the app | ||||||
|  |     But I should not find "Second Student" in the app | ||||||
|  |     And I should not find "Third Student" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     # Drafts — Split view | ||||||
|  |     When I press "Third Student" in the app | ||||||
|  |     Then "Third Student" near "First Student" should be selected in the app | ||||||
|  |     And I should find "Third Student" inside the split-view content in the app | ||||||
|  |     But I should not find "First Student" inside the split-view content in the app | ||||||
|  |     And I should not find "Second Student" in the app | ||||||
|  | 
 | ||||||
|  |     # Filter groups in assignment page | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Separate groups" in the app | ||||||
|  |     And I press "Group 1" in the app | ||||||
|  |     Then I should find "2" near "Participants" in the app | ||||||
|  |     And I should find "1" near "Drafts" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     And I should find "Second Student" in the app | ||||||
|  |     And "First Student" near "Second Student" should be selected in the app | ||||||
|  |     And I should find "First Student" inside the split-view content in the app | ||||||
|  |     But I should not find "Third Student" in the app | ||||||
|  |     And I should not find "Second Student" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     # Filter groups in submissions page | ||||||
|  |     When I press "Separate groups" in the app | ||||||
|  |     And I press "Group 2" in the app | ||||||
|  |     Then I should find "Second Student" in the app | ||||||
|  |     And I should find "Third Student" in the app | ||||||
|  |     And "Second Student" near "Third Student" should be selected in the app | ||||||
|  |     And I should find "Second Student" inside the split-view content in the app | ||||||
|  |     But I should not find "First Student" in the app | ||||||
|  |     And I should not find "Third Student" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Third Student" in the app | ||||||
|  |     Then "Third Student" near "Second Student" should be selected in the app | ||||||
|  |     And I should find "Third Student" inside the split-view content in the app | ||||||
|  |     But I should not find "Second Student" inside the split-view content in the app | ||||||
|  |     And I should not find "First Student" in the app | ||||||
							
								
								
									
										72
									
								
								src/addons/mod/chat/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										72
									
								
								src/addons/mod/chat/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,72 @@ | |||||||
|  | @mod @mod_chat @app @javascript | ||||||
|  | Feature: Test basic usage of chat in app | ||||||
|  |   As a student | ||||||
|  |   I need basic chat functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "users" exist: | ||||||
|  |       | username | firstname | lastname | | ||||||
|  |       | student1 | david     | student  | | ||||||
|  |       | student2 | pau       | student2 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role           | | ||||||
|  |       | student1 | C1     | student        | | ||||||
|  |       | student2 | C1     | student        | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name            | intro       | course | idnumber | groupmode | | ||||||
|  |       | chat       | Test chat name  | Test chat   | C1     | chat     | 0         | | ||||||
|  | 
 | ||||||
|  |   Scenario: Receive and send messages & See connected users, beep and talk to | ||||||
|  |     # Send messages as student1 | ||||||
|  |     Given I entered the chat activity "Test chat name" on course "Course 1" as "student1" in the app | ||||||
|  |     Then I should find "Enter the chat" in the app | ||||||
|  |     And I should find "Past sessions" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Enter the chat" in the app | ||||||
|  |     And I set the field "New message" to "Hi!" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "Hi!" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "New message" to "I am David" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "Hi!" in the app | ||||||
|  |     And I should find "I am David" in the app | ||||||
|  | 
 | ||||||
|  |     # Read messages, view connected users, send beep and reply as student2 | ||||||
|  |     Given I entered the chat activity "Test chat name" on course "Course 1" as "student2" in the app | ||||||
|  |     And I press "Enter the chat" in the app | ||||||
|  |     Then I should find "Hi!" in the app | ||||||
|  |     And I should find "I am David" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Users" in the app | ||||||
|  |     Then I should find "david student" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Beep" in the app | ||||||
|  |     Then I should find "You beeped david student" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "New message" to "Hi David, I am Pau." in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "Hi David, I am Pau." in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Past sessions shown | ||||||
|  |     # Send messages as student1 | ||||||
|  |     Given I entered the chat activity "Test chat name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Enter the chat" in the app | ||||||
|  |     And I set the field "New message" to "Hi!" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "Hi!" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "New message" to "I am David" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "I am David" in the app | ||||||
|  | 
 | ||||||
|  |     # Read messages from past sessions as student2 | ||||||
|  |     Given I entered the chat activity "Test chat name" on course "Course 1" as "student2" in the app | ||||||
|  |     When I press "Past sessions" in the app | ||||||
|  |     And I press "Show incomplete sessions" in the app | ||||||
|  |     And I press "david student" near "(2)" in the app | ||||||
|  |     Then I should find "Hi!" in the app | ||||||
|  |     And I should find "I am David" in the app | ||||||
							
								
								
									
										43
									
								
								src/addons/mod/chat/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/addons/mod/chat/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | @mod @mod_chat @app @javascript | ||||||
|  | Feature: Test chat navigation | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "users" exist: | ||||||
|  |       | username | firstname | lastname | | ||||||
|  |       | student1 | Student   | first    | | ||||||
|  |       | student2 | Student   | second   | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role     | | ||||||
|  |       | student1 | C1     | student  | | ||||||
|  |       | student2 | C1     | student  | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name            | intro       | course | idnumber | groupmode | | ||||||
|  |       | chat       | Test chat name  | Test chat   | C1     | chat     | 0         | | ||||||
|  |     # Create sessions | ||||||
|  |     # TODO use generator instead | ||||||
|  |     And I entered the chat activity "Test chat name" on course "Course 1" as "student1" in the app | ||||||
|  |     And I press "Enter the chat" in the app | ||||||
|  |     And I set the field "New message" to "Test message" in the app | ||||||
|  |     And I press "Send" in the app | ||||||
|  |     Then I should find "Test message" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Tablet navigation on chat | ||||||
|  |     Given I entered the course "Course 1" as "student2" in the app | ||||||
|  |     And I change viewport size to "1200x640" | ||||||
|  | 
 | ||||||
|  |     # Sessions | ||||||
|  |     When I press "Test chat name" in the app | ||||||
|  |     And I press "Past sessions" in the app | ||||||
|  |     Then I should find "No sessions found" in the app | ||||||
|  | 
 | ||||||
|  |     # Sessions — split view | ||||||
|  |     When I press "Show incomplete sessions" in the app | ||||||
|  |     Then "Student first" should be selected in the app | ||||||
|  |     And I should find "Test message" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Show incomplete sessions" in the app | ||||||
|  |     Then I should not find "Student first" in the app | ||||||
|  |     And I should not find "Test message" in the app | ||||||
							
								
								
									
										41
									
								
								src/addons/mod/choice/tests/behat/basic_usage-311.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										41
									
								
								src/addons/mod/choice/tests/behat/basic_usage-311.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | @mod @mod_choice @app @javascript @lms_upto3.11 | ||||||
|  | Feature: Test basic usage of choice activity in app | ||||||
|  |   In order to participate in the choice while using the mobile app | ||||||
|  |   As a student | ||||||
|  |   I need basic choice functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email | | ||||||
|  |       | teacher1 | Teacher | teacher | teacher1@example.com | | ||||||
|  |       | student1 | Student | student | student1@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  | 
 | ||||||
|  |   Scenario: Download students choice in text format | ||||||
|  |     # Submit answer as student | ||||||
|  |     Given the following "activities" exist: | ||||||
|  |       | activity | name        | intro                   | course | idnumber | option | | ||||||
|  |       | choice   | Choice name | Test choice description | C1     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |     And I entered the choice activity "Choice name" on course "Course 1" as "student1" in the app | ||||||
|  |     Then I select "Option 2" in the app | ||||||
|  |     And I press "Save my choice" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  | 
 | ||||||
|  |     # Download answers as teacher | ||||||
|  |     Given I entered the choice activity "Choice name" on course "Course 1" as "teacher1" in the app | ||||||
|  |     Then I should find "Test choice description" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Information" 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 press "Actions menu" | ||||||
|  |     And I follow "View 1 responses" | ||||||
|  |     And I press "Download in text format" | ||||||
|  |     # TODO Then I should find "..." in the downloads folder | ||||||
							
								
								
									
										185
									
								
								src/addons/mod/choice/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										185
									
								
								src/addons/mod/choice/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,185 @@ | |||||||
|  | @mod @mod_choice @app @javascript | ||||||
|  | Feature: Test basic usage of choice activity in app | ||||||
|  |   In order to participate in the choice while using the mobile app | ||||||
|  |   As a student | ||||||
|  |   I need basic choice functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email | | ||||||
|  |       | teacher1 | Teacher | teacher | teacher1@example.com | | ||||||
|  |       | student1 | Student | student | student1@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  | 
 | ||||||
|  |   Scenario: Answer a choice (multi or single, update answer) & View results | ||||||
|  |     Given the following "activities" exist: | ||||||
|  |       | activity | name                    | intro                          | course | idnumber | option                       | allowmultiple | allowupdate | showresults | | ||||||
|  |       | choice   | Test single choice name | Test single choice description | C1     | choice1  | Option 1, Option 2, Option 3 | 0             | 0           | 1           | | ||||||
|  |     And I entered the choice activity "Test single choice name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I select "Option 1" in the app | ||||||
|  |     And I select "Option 2" in the app | ||||||
|  |     And I press "Save my choice" in the app | ||||||
|  |     Then I should find "Are you sure" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "OK" in the app | ||||||
|  |     Then I should find "Option 1: 0" in the app | ||||||
|  |     And I should find "Option 2: 1" in the app | ||||||
|  |     And I should find "Option 3: 0" in the app | ||||||
|  |     But I should not find "Remove my choice" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test single choice name" in the app | ||||||
|  |     Then I should find "Option 1: 0" in the app | ||||||
|  |     And I should find "Option 2: 1" in the app | ||||||
|  |     And I should find "Option 3: 0" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Answer a choice (multi or single, update answer) & View results & Delete choice | ||||||
|  |     Given the following "activities" exist: | ||||||
|  |       | activity | name                    | intro                          | course | idnumber | option                       | allowmultiple | allowupdate | showresults | | ||||||
|  |       | choice   | Test multi choice name  | Test multi choice description  | C1     | choice2  | Option 1, Option 2, Option 3 | 1             | 1           | 1           | | ||||||
|  |     And I entered the choice activity "Test multi choice name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I select "Option 1" in the app | ||||||
|  |     And I select "Option 2" in the app | ||||||
|  |     And I press "Save my choice" in the app | ||||||
|  |     Then I should find "Option 1: 1" in the app | ||||||
|  |     And I should find "Option 2: 1" in the app | ||||||
|  |     And I should find "Option 3: 0" in the app | ||||||
|  |     And I should find "Remove my choice" in the app | ||||||
|  | 
 | ||||||
|  |     When I unselect "Option 1" in the app | ||||||
|  |     And I select "Option 3" in the app | ||||||
|  |     And I press "Save my choice" in the app | ||||||
|  |     Then I should find "Option 1: 0" in the app | ||||||
|  |     And I should find "Option 2: 1" in the app | ||||||
|  |     And I should find "Option 3: 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Remove my choice" in the app | ||||||
|  |     Then I should find "Are you sure" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Delete" in the app | ||||||
|  |     Then I should find "The results are not currently viewable" in the app | ||||||
|  |     But I should not find "Remove my choice" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Answer and change answer offline & Sync choice | ||||||
|  |     Given the following "activities" exist: | ||||||
|  |       | activity | name                    | intro                          | course | idnumber | option                       | allowmultiple | allowupdate | showresults | | ||||||
|  |       | choice   | Test single choice name | Test single choice description | C1     | choice1  | Option 1, Option 2, Option 3 | 0             | 0           | 1           | | ||||||
|  |     And I entered the choice activity "Test single choice name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I select "Option 1" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I select "Option 2" in the app | ||||||
|  |     And I press "Save my choice" in the app | ||||||
|  |     Then I should find "Are you sure" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "OK" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Test single choice name" in the app | ||||||
|  |     Then I should find "This Choice has offline data to be synchronised." in the app | ||||||
|  |     But I should not find "Option 1: 0" in the app | ||||||
|  |     And I should not find "Option 2: 1" in the app | ||||||
|  |     And I should not find "Option 3: 0" in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Test single choice name" in the app | ||||||
|  |     Then I should find "Test single choice description" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Information" in the app | ||||||
|  |     And I press "Refresh" in the app | ||||||
|  |     Then I should find "Option 1: 0" in the app | ||||||
|  |     And I should find "Option 2: 1" in the app | ||||||
|  |     And I should find "Option 3: 0" in the app | ||||||
|  |     But I should not find "This Choice has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Answer and change answer offline & Auto-sync choice | ||||||
|  |     Given the following "activities" exist: | ||||||
|  |       | activity | name                    | intro                          | course | idnumber | option                       | allowmultiple | allowupdate | showresults | | ||||||
|  |       | choice   | Test single choice name | Test single choice description | C1     | choice1  | Option 1, Option 2, Option 3 | 0             | 0           | 1           | | ||||||
|  |     And I entered the choice activity "Test single choice name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I select "Option 1" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I select "Option 2" in the app | ||||||
|  |     And I press "Save my choice" in the app | ||||||
|  |     Then I should find "Are you sure" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "OK" in the app | ||||||
|  |     Then I should find "This Choice has offline data to be synchronised." in the app | ||||||
|  |     But I should not find "Option 1: 0" in the app | ||||||
|  |     And I should not find "Option 2: 1" in the app | ||||||
|  |     And I should not find "Option 3: 0" in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I run cron tasks in the app | ||||||
|  |     And I wait loading to finish in the app | ||||||
|  |     Then I should find "Option 1: 0" in the app | ||||||
|  |     And I should find "Option 2: 1" in the app | ||||||
|  |     And I should find "Option 3: 0" in the app | ||||||
|  |     But I should not find "This Choice has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Prefetch | ||||||
|  |     Given the following "activities" exist: | ||||||
|  |       | activity | name                    | intro                          | course | idnumber | option                       | allowmultiple | allowupdate | showresults | | ||||||
|  |       | choice   | Test multi choice name  | Test multi choice description  | C1     | choice2  | Option 1, Option 2, Option 3 | 1             | 1           | 1           | | ||||||
|  |       | choice   | Test single choice name | Test single choice description | C1     | choice1  | Option 1, Option 2, Option 3 | 0             | 0           | 1           | | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Course downloads" in the app | ||||||
|  |     When I press "Download" within "Test single choice name" "ion-item" in the app | ||||||
|  |     Then I should find "Downloaded" within "Test single choice name" "ion-item" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "true" | ||||||
|  |     And I press "Test multi choice name" in the app | ||||||
|  |     Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app | ||||||
|  | 
 | ||||||
|  |     When I press "OK" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Test single choice name" in the app | ||||||
|  |     And I select "Option 2" in the app | ||||||
|  |     And I press "Save my choice" in the app | ||||||
|  |     Then I should find "Are you sure" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "OK" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Test single choice name" in the app | ||||||
|  |     Then I should find "This Choice has offline data to be synchronised." in the app | ||||||
|  |     But I should not find "Option 1: 0" in the app | ||||||
|  |     And I should not find "Option 2: 1" in the app | ||||||
|  |     And I should not find "Option 3: 0" in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Test single choice name" in the app | ||||||
|  |     Then I should find "Option 1: 0" in the app | ||||||
|  |     And I should find "Option 2: 1" in the app | ||||||
|  |     And I should find "Option 3: 0" in the app | ||||||
|  |     But I should not find "This Choice has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |   # TODO remove LMS UI steps in app tests | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: Download students choice in text format | ||||||
|  |     # Submit answer as student | ||||||
|  |     Given the following "activities" exist: | ||||||
|  |       | activity | name        | intro                   | course | idnumber | option | | ||||||
|  |       | choice   | Choice name | Test choice description | C1     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |     And I entered the choice activity "Choice name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I select "Option 2" in the app | ||||||
|  |     And I press "Save my choice" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  | 
 | ||||||
|  |     # Download answers as teacher | ||||||
|  |     Given I entered the choice activity "Choice name" on course "Course 1" as "teacher1" in the app | ||||||
|  |     Then I should find "Test choice description" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Information" 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 follow "Responses" | ||||||
|  |     And I press "Download in text format" | ||||||
|  |     # TODO Then I should find "..." in the downloads folder | ||||||
							
								
								
									
										379
									
								
								src/addons/mod/forum/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										379
									
								
								src/addons/mod/forum/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,379 @@ | |||||||
|  | @mod @mod_forum @app @javascript | ||||||
|  | Feature: Test basic usage of forum activity in app | ||||||
|  |   In order to participate in the forum while using the mobile app | ||||||
|  |   As a student | ||||||
|  |   I need basic forum functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  |       | student2 | | ||||||
|  |       | teacher1 | | ||||||
|  |       | teacher2 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |       | student2 | C1     | student | | ||||||
|  |       | teacher1 | C1     | editingteacher | | ||||||
|  |       | teacher2 | C1     | editingteacher | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name            | intro       | course | idnumber | groupmode | assessed | scale | | ||||||
|  |       | forum      | Test forum name | Test forum  | C1     | forum    | 0         | 1        | 1     | | ||||||
|  | 
 | ||||||
|  |   Scenario: Create new discussion | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "My happy subject" in the app | ||||||
|  |     And I set the field "Message" to "An awesome message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "My happy subject" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "My happy subject" in the app | ||||||
|  |     Then I should find "An awesome message" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Reply a post | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "DiscussionSubject" in the app | ||||||
|  |     And I set the field "Message" to "DiscussionMessage" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "DiscussionSubject" in the app | ||||||
|  |     Then I should find "Reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Reply" in the app | ||||||
|  |     And I set the field "Message" to "ReplyMessage" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "DiscussionMessage" in the app | ||||||
|  |     And I should find "ReplyMessage" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Star and pin discussions (student) | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "starred subject" in the app | ||||||
|  |     And I set the field "Message" to "starred message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "normal subject" in the app | ||||||
|  |     And I set the field "Message" to "normal message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "starred subject" in the app | ||||||
|  |     Then I should find "starred message" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Display options" near "starred subject" in the app | ||||||
|  |     And I press "Star this discussion" in the app | ||||||
|  |     And I press "starred subject" in the app | ||||||
|  |     Then I should find "starred message" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "normal subject" in the app | ||||||
|  |     Then I should find "normal message" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Display options" near "starred subject" in the app | ||||||
|  |     And I press "Unstar this discussion" in the app | ||||||
|  |     And I press "starred subject" in the app | ||||||
|  |     Then I should find "starred message" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "normal subject" in the app | ||||||
|  |     Then I should find "normal message" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Star and pin discussions (teacher) | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "teacher1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Auto-test star" in the app | ||||||
|  |     And I set the field "Message" to "Auto-test star message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Auto-test pin" in the app | ||||||
|  |     And I set the field "Message" to "Auto-test pin message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Auto-test plain" in the app | ||||||
|  |     And I set the field "Message" to "Auto-test plain message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Display options" near "Auto-test star" in the app | ||||||
|  |     And I press "Star this discussion" in the app | ||||||
|  |     And I press "Display options" near "Auto-test pin" in the app | ||||||
|  |     And I press "Pin this discussion" in the app | ||||||
|  |     Then I should find "Auto-test pin" in the app | ||||||
|  |     And I should find "Auto-test star" in the app | ||||||
|  |     And I should find "Auto-test plain" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" near "Auto-test pin" in the app | ||||||
|  |     And I press "Unpin this discussion" in the app | ||||||
|  |     And I press "Display options" near "Auto-test star" in the app | ||||||
|  |     And I press "Unstar this discussion" in the app | ||||||
|  |     Then I should find "Auto-test star" in the app | ||||||
|  |     And I should find "Auto-test pin" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Edit a not sent reply offline | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Auto-test" in the app | ||||||
|  |     And I set the field "Message" to "Auto-test message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Auto-test" near "Sort by last post creation date in descending order" in the app | ||||||
|  |     And I should find "Reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I press "Auto-test" near "Sort by last post creation date in descending order" in the app | ||||||
|  |     Then I should find "Reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Reply" in the app | ||||||
|  |     And I set the field "Message" to "not sent reply" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Display options" within "not sent reply" "ion-card" in the app | ||||||
|  |     Then I should find "Edit" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Edit" in the app | ||||||
|  |     And I set the field "Message" to "not sent reply edited" in the app | ||||||
|  |     And I press "Save changes" in the app | ||||||
|  |     Then I should find "Not sent" in the app | ||||||
|  |     And I should find "This Discussion has offline data to be synchronised" in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Auto-test" near "Sort by last post creation date in descending order" in the app | ||||||
|  |     Then I should not find "Not sent" in the app | ||||||
|  |     And I should not find "This Discussion has offline data to be synchronised" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Edit a not sent new discussion offline | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I switch offline mode to "true" | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Auto-test" in the app | ||||||
|  |     And I set the field "Message" to "Auto-test message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Auto-test" in the app | ||||||
|  |     And I set the field "Message" to "Auto-test message edited" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "This Forum has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press "Auto-test" in the app | ||||||
|  |     Then I should find "Post to forum" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Post to forum" in the app | ||||||
|  |     Then I should not find "This Forum has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Auto-test" near "Sort by last post creation date in descending order" in the app | ||||||
|  |     And I should find "Auto-test message edited" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Edit a forum post (only online) | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Auto-test" in the app | ||||||
|  |     And I set the field "Message" to "Auto-test message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "Auto-test" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course downloads" in the app | ||||||
|  |     And I press "Download" within "Test forum name" "ion-item" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Test forum name" in the app | ||||||
|  |     And I press "Auto-test" near "Sort by last post creation date in descending order" in the app | ||||||
|  |     Then I should find "Reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" near "Reply" in the app | ||||||
|  |     Then I should find "Edit" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Edit" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I set the field "Message" to "Auto-test message edited" in the app | ||||||
|  |     And I press "Save changes" in the app | ||||||
|  |     Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app | ||||||
|  | 
 | ||||||
|  |   # TODO Fix this test in all Moodle versions | ||||||
|  |   Scenario: Delete a forum post (only online) | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Auto-test" in the app | ||||||
|  |     And I set the field "Message" to "Auto-test message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "Auto-test" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course downloads" in the app | ||||||
|  |     And I press "Download" within "Test forum name" "ion-item" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Test forum name" in the app | ||||||
|  |     And I press "Auto-test" near "Sort by last post creation date in descending order" in the app | ||||||
|  |     Then I should find "Reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Display options" near "Reply" in the app | ||||||
|  |     Then I should find "Delete" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Delete" in the app | ||||||
|  |     And I press "Cancel" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I press "Display options" near "Reply" in the app | ||||||
|  |     Then I should not find "Delete" in the app | ||||||
|  | 
 | ||||||
|  |     When I close the popup in the app | ||||||
|  |     And I switch offline mode to "false" | ||||||
|  |     And I press "Display options" near "Reply" in the app | ||||||
|  |     And I press "Delete" in the app | ||||||
|  |     And I press "Delete" in the app | ||||||
|  |     Then I should not find "Auto-test" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Add/view ratings | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Auto-test" in the app | ||||||
|  |     And I set the field "Message" to "Auto-test message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Auto-test" in the app | ||||||
|  |     Then I should find "Reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Reply" in the app | ||||||
|  |     And I set the field "Message" to "test2" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "test2" "ion-card" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "teacher1" in the app | ||||||
|  |     When I press "Auto-test" in the app | ||||||
|  |     Then I should find "Reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "None" near "Auto-test message" in the app | ||||||
|  |     And I press "1" near "Cancel" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I press "None" near "test2" in the app | ||||||
|  |     And I press "0" near "Cancel" in the app | ||||||
|  |     Then I should find "Data stored in the device because it couldn't be sent. It will be sent automatically later." inside the toast in the app | ||||||
|  |     And I should find "Average of ratings: -" in the app | ||||||
|  |     And I should find "Average of ratings: 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press the back button in the app | ||||||
|  |     Then I should find "This Forum has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Information" in the app | ||||||
|  |     And I press "Synchronise now" in the app | ||||||
|  |     Then I should not find "This Forum has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Auto-test" in the app | ||||||
|  |     Then I should find "Average of ratings: 1" in the app | ||||||
|  |     And I should find "Average of ratings: 0" in the app | ||||||
|  |     But I should not find "Average of ratings: -" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Auto-test" in the app | ||||||
|  |     Then I should find "Average of ratings: 1" in the app | ||||||
|  |     And I should find "Average of ratings: 0" in the app | ||||||
|  |     But I should not find "Average of ratings: -" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Reply a post offline | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "DiscussionSubject" in the app | ||||||
|  |     And I set the field "Message" to "DiscussionMessage" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Course downloads" in the app | ||||||
|  |     And I press "Download" within "Test forum name" "ion-item" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Test forum name" in the app | ||||||
|  |     And I press "DiscussionSubject" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     Then I should find "Reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Reply" in the app | ||||||
|  |     And I set the field "Message" to "ReplyMessage" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "DiscussionMessage" in the app | ||||||
|  |     And I should find "ReplyMessage" in the app | ||||||
|  |     And I should find "Not sent" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I switch offline mode to "false" | ||||||
|  |     And I press "DiscussionSubject" in the app | ||||||
|  |     Then I should find "DiscussionMessage" in the app | ||||||
|  |     And I should find "ReplyMessage" in the app | ||||||
|  |     But I should not find "Not sent" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: New discussion offline & Sync Forum | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I switch offline mode to "true" | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "DiscussionSubject" in the app | ||||||
|  |     And I set the field "Message" to "DiscussionMessage" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "DiscussionSubject" in the app | ||||||
|  |     And I should find "Not sent" in the app | ||||||
|  |     And I should find "This Forum has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Test forum name" in the app | ||||||
|  |     And I press "Information" in the app | ||||||
|  |     And I press "Refresh" in the app | ||||||
|  |     And I press "DiscussionSubject" near "Sort by last post creation date in descending order" in the app | ||||||
|  |     Then I should find "DiscussionSubject" in the app | ||||||
|  |     And I should find "DiscussionMessage" in the app | ||||||
|  |     But I should not find "Not sent" in the app | ||||||
|  |     And I should not find "This Forum has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: New discussion offline & Auto-sync forum | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I switch offline mode to "true" | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "DiscussionSubject" in the app | ||||||
|  |     And I set the field "Message" to "DiscussionMessage" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "DiscussionSubject" in the app | ||||||
|  |     And I should find "Not sent" in the app | ||||||
|  |     And I should find "This Forum has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |     When I switch offline mode to "false" | ||||||
|  |     And I run cron tasks in the app | ||||||
|  |     And I wait loading to finish in the app | ||||||
|  |     Then I should not find "Not sent" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "DiscussionSubject" near "Sort by last post creation date in descending order" in the app | ||||||
|  |     Then I should find "DiscussionSubject" in the app | ||||||
|  |     And I should find "DiscussionMessage" in the app | ||||||
|  |     But I should not find "Not sent" in the app | ||||||
|  |     And I should not find "This Forum has offline data to be synchronised." in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Prefetch | ||||||
|  |     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "DiscussionSubject 1" in the app | ||||||
|  |     And I set the field "Message" to "DiscussionMessage 1" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "DiscussionSubject 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course downloads" in the app | ||||||
|  |     And I press "Download" within "Test forum name" "ion-item" in the app | ||||||
|  |     Then I should find "Downloaded" within "Test forum name" "ion-item" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Test forum name" in the app | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "DiscussionSubject 2" in the app | ||||||
|  |     And I set the field "Message" to "DiscussionMessage 2" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "DiscussionSubject 1" in the app | ||||||
|  |     And I should find "DiscussionSubject 2" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I press "Test forum name" in the app | ||||||
|  |     And I press "DiscussionSubject 2" in the app | ||||||
|  |     Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app | ||||||
|  | 
 | ||||||
|  |     When I press "OK" in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "DiscussionSubject 1" in the app | ||||||
|  |     Then I should find "DiscussionSubject 1" in the app | ||||||
|  |     And I should find "DiscussionMessage 1" in the app | ||||||
|  |     But I should not find "There was a problem connecting to the site. Please check your connection and try again." in the app | ||||||
							
								
								
									
										231
									
								
								src/addons/mod/forum/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/addons/mod/forum/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,231 @@ | |||||||
|  | @mod @mod_forum @app @javascript | ||||||
|  | Feature: Test forum navigation | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | | ||||||
|  |       | student1 | First     | Student  | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role           | | ||||||
|  |       | student1 | C1     | student        | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name  | course | idnumber | | ||||||
|  |       | forum    | Forum | C1     | forum    | | ||||||
|  |     And the following "mod_forum > discussions" exist: | ||||||
|  |       | forum | name | message | timenow | | ||||||
|  |       | forum | Discussion 01 | Discussion 01 message | 1638200100 | | ||||||
|  |       | forum | Discussion 02 | Discussion 02 message | 1638200200 | | ||||||
|  |       | forum | Discussion 03 | Discussion 03 message | 1638200300 | | ||||||
|  |       | forum | Discussion 04 | Discussion 04 message | 1638200400 | | ||||||
|  |       | forum | Discussion 05 | Discussion 05 message | 1638200500 | | ||||||
|  |       | forum | Discussion 06 | Discussion 06 message | 1638200600 | | ||||||
|  |       | forum | Discussion 07 | Discussion 07 message | 1638200700 | | ||||||
|  |       | forum | Discussion 08 | Discussion 08 message | 1638200800 | | ||||||
|  |       | forum | Discussion 09 | Discussion 09 message | 1638200900 | | ||||||
|  |       | forum | Discussion 10 | Discussion 10 message | 1638201000 | | ||||||
|  |       | forum | Discussion 11 | Discussion 11 message | 1638201100 | | ||||||
|  |       | forum | Discussion 12 | Discussion 12 message | 1638201200 | | ||||||
|  |       | forum | Discussion 13 | Discussion 13 message | 1638201300 | | ||||||
|  |       | forum | Discussion 14 | Discussion 14 message | 1638201400 | | ||||||
|  |       | forum | Discussion 15 | Discussion 15 message | 1638201500 | | ||||||
|  |       | forum | Discussion 16 | Discussion 16 message | 1638201600 | | ||||||
|  |       | forum | Discussion 17 | Discussion 17 message | 1638201700 | | ||||||
|  |       | forum | Discussion 18 | Discussion 18 message | 1638201800 | | ||||||
|  |       | forum | Discussion 19 | Discussion 19 message | 1638201900 | | ||||||
|  |       | forum | Discussion 20 | Discussion 20 message | 1638202000 | | ||||||
|  |     And the following "mod_forum > posts" exist: | ||||||
|  |       | discussion | parentsubject | message | | ||||||
|  |       | Discussion 04 | Discussion 04 | Discussion 04 first reply | | ||||||
|  |       | Discussion 05 | Discussion 05 | Discussion 05 first reply | | ||||||
|  | 
 | ||||||
|  |   Scenario: Mobile navigation on forum | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  | 
 | ||||||
|  |     # By last reply | ||||||
|  |     When I press "Forum" in the app | ||||||
|  |     Then I should find "Discussion 05" in the app | ||||||
|  |     And I should find "Discussion 04" in the app | ||||||
|  |     But I should not find "Discussion 12" in the app | ||||||
|  | 
 | ||||||
|  |     # By last reply — Infinite loading | ||||||
|  |     When I load more items in the app | ||||||
|  |     Then I should find "Discussion 12" in the app | ||||||
|  |     And I should find "Discussion 01" in the app | ||||||
|  |     But I should not be able to load more items in the app | ||||||
|  | 
 | ||||||
|  |     # By last reply — Swipe | ||||||
|  |     When I press "Discussion 05" in the app | ||||||
|  |     Then I should find "Discussion 05 first reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Discussion 05 first reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Discussion 04 first reply" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Discussion 20 message" in the app | ||||||
|  | 
 | ||||||
|  |     # By creation date | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I scroll to "Discussion 05" in the app | ||||||
|  |     And I press "Sort" in the app | ||||||
|  |     And I press "Sort by creation date in descending order" in the app | ||||||
|  |     Then I should find "Discussion 20" in the app | ||||||
|  |     And I should find "Discussion 19" in the app | ||||||
|  |     But I should not find "Discussion 10" in the app | ||||||
|  |     And I should not find "Discussion 04" in the app | ||||||
|  |     And I should not find "Discussion 05" in the app | ||||||
|  | 
 | ||||||
|  |     # By creation date — Infinite loading | ||||||
|  |     When I load more items in the app | ||||||
|  |     Then I should find "Discussion 10" in the app | ||||||
|  |     And I should find "Discussion 04" in the app | ||||||
|  |     And I should find "Discussion 05" in the app | ||||||
|  |     But I should not be able to load more items in the app | ||||||
|  | 
 | ||||||
|  |     # By creation date — Swipe | ||||||
|  |     When I press "Discussion 20" in the app | ||||||
|  |     Then I should find "Discussion 20 message" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Discussion 20 message" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Discussion 19 message" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Discussion 18 message" in the app | ||||||
|  | 
 | ||||||
|  |     # Offline | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I set the field "Subject" to "Offline discussion 1" in the app | ||||||
|  |     And I set the field "Message" to "Offline discussion 1 message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Offline discussion 2" in the app | ||||||
|  |     And I set the field "Message" to "Offline discussion 2 message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "Not sent" in the app | ||||||
|  |     And I should find "Offline discussion 1" in the app | ||||||
|  |     And I should find "Offline discussion 2" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Offline discussion 2" in the app | ||||||
|  |     And I set the field "Subject" to "Offline discussion 3" in the app | ||||||
|  |     And I set the field "Message" to "Offline discussion 3 message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "Not sent" in the app | ||||||
|  |     And I should find "Offline discussion 1" in the app | ||||||
|  |     And I should find "Offline discussion 3" in the app | ||||||
|  |     But I should not find "Offline discussion 2" in the app | ||||||
|  | 
 | ||||||
|  |     # TODO fix flaky test failing on CI but working locally | ||||||
|  |     # # Offline — Swipe | ||||||
|  |     # When I press "Offline discussion 3" in the app | ||||||
|  |     # Then I should find "Offline discussion 3 message" in the app | ||||||
|  | 
 | ||||||
|  |     # When I swipe to the right in the app | ||||||
|  |     # Then I should find "Offline discussion 3 message" in the app | ||||||
|  | 
 | ||||||
|  |     # When I swipe to the left in the app | ||||||
|  |     # Then I should find "Offline discussion 1 message" in the app | ||||||
|  | 
 | ||||||
|  |     # When I swipe to the left in the app | ||||||
|  |     # Then I should find "Discussion 20 message" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Tablet navigation on forum | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  |     And I change viewport size to "1200x640" | ||||||
|  | 
 | ||||||
|  |     # By last reply | ||||||
|  |     When I press "Forum" in the app | ||||||
|  |     Then I should find "Discussion 04" in the app | ||||||
|  |     And I should find "Discussion 05" in the app | ||||||
|  |     And "Discussion 05" near "Discussion 04" should be selected in the app | ||||||
|  |     And I should find "Discussion 05 first reply" inside the split-view content in the app | ||||||
|  |     But I should not find "Discussion 12" in the app | ||||||
|  | 
 | ||||||
|  |     # By last reply — Infinite loading | ||||||
|  |     When I load more items in the app | ||||||
|  |     Then I should find "Discussion 12" in the app | ||||||
|  |     And I should find "Discussion 01" in the app | ||||||
|  |     But I should not be able to load more items in the app | ||||||
|  | 
 | ||||||
|  |     # By last reply — Split view | ||||||
|  |     When I press "Discussion 04" in the app | ||||||
|  |     Then "Discussion 04" near "Discussion 05" should be selected in the app | ||||||
|  |     And I should find "Discussion 04 first reply" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Discussion 12" in the app | ||||||
|  |     Then "Discussion 12" near "Discussion 11" should be selected in the app | ||||||
|  |     And I should find "Discussion 12 message" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     # By creation date | ||||||
|  |     When I scroll to "Discussion 05" in the app | ||||||
|  |     And I press "Discussion 05" in the app | ||||||
|  |     And I press "Sort" in the app | ||||||
|  |     And I press "Sort by creation date in descending order" in the app | ||||||
|  |     Then I should find "Discussion 20" in the app | ||||||
|  |     And I should find "Discussion 19" in the app | ||||||
|  |     And "Discussion 20" near "Discussion 19" should be selected in the app | ||||||
|  |     And I should find "Discussion 20 message" inside the split-view content in the app | ||||||
|  |     But I should not find "Discussion 10" in the app | ||||||
|  |     And I should not find "Discussion 04" in the app | ||||||
|  |     And I should not find "Discussion 05" in the app | ||||||
|  | 
 | ||||||
|  |     # By creation date — Infinite loading | ||||||
|  |     When I load more items in the app | ||||||
|  |     Then I should find "Discussion 10" in the app | ||||||
|  |     And I should find "Discussion 04" in the app | ||||||
|  |     And I should find "Discussion 05" in the app | ||||||
|  |     But I should not be able to load more items in the app | ||||||
|  | 
 | ||||||
|  |     # By creation date — Split view | ||||||
|  |     When I press "Discussion 19" in the app | ||||||
|  |     Then "Discussion 19" near "Discussion 20" should be selected in the app | ||||||
|  |     And I should find "Discussion 19 message" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Discussion 05" in the app | ||||||
|  |     Then "Discussion 05" near "Discussion 04" should be selected in the app | ||||||
|  |     And I should find "Discussion 05 first reply" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     # Offline | ||||||
|  |     When I press "Add discussion topic" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I set the field "Subject" to "Offline discussion 1" in the app | ||||||
|  |     And I set the field "Message" to "Offline discussion 1 message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     And I press "Add discussion topic" in the app | ||||||
|  |     And I set the field "Subject" to "Offline discussion 2" in the app | ||||||
|  |     And I set the field "Message" to "Offline discussion 2 message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "Not sent" in the app | ||||||
|  |     And I should find "Offline discussion 1" in the app | ||||||
|  |     And I should find "Offline discussion 2" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Offline discussion 2" in the app | ||||||
|  |     And I set the field "Subject" to "Offline discussion 3" in the app | ||||||
|  |     And I set the field "Message" to "Offline discussion 3 message" in the app | ||||||
|  |     And I press "Post to forum" in the app | ||||||
|  |     Then I should find "Not sent" in the app | ||||||
|  |     And I should find "Offline discussion 1" in the app | ||||||
|  |     And I should find "Offline discussion 3" in the app | ||||||
|  |     But I should not find "Offline discussion 2" in the app | ||||||
|  | 
 | ||||||
|  |     # Offline — Split view | ||||||
|  |     When I press "Offline discussion 1" in the app | ||||||
|  |     Then "Offline discussion 1" near "Offline discussion 3" should be selected in the app | ||||||
|  |     And I should find "Offline discussion 1 message" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Offline discussion 3" in the app | ||||||
|  |     Then "Offline discussion 3" near "Offline discussion 1" should be selected in the app | ||||||
|  |     And I should find "Offline discussion 3 message" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Discussion 20" in the app | ||||||
|  |     Then "Discussion 20" near "Discussion 19" should be selected in the app | ||||||
|  |     And I should find "Discussion 20 message" inside the split-view content in the app | ||||||
							
								
								
									
										289
									
								
								src/addons/mod/glossary/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								src/addons/mod/glossary/tests/behat/navigation.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,289 @@ | |||||||
|  | @mod @mod_glossary @app @javascript | ||||||
|  | Feature: Test glossary navigation | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | | ||||||
|  |       | student1 | First     | Student  | | ||||||
|  |       | student2 | Second    | Student  | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name            | course | idnumber | displayformat | | ||||||
|  |       | glossary | Fruits glossary | C1     | glossary | entrylist     | | ||||||
|  |     And the following "mod_glossary > entries" exist: | ||||||
|  |       | glossary | concept | definition | user | | ||||||
|  |       | glossary | Acerola | Acerola is a fruit | student1 | | ||||||
|  |       | glossary | Apple | Apple is a fruit | student2 | | ||||||
|  |       | glossary | Apricots | Apricots are a fruit | student1 | | ||||||
|  |       | glossary | Avocado | Avocado is a fruit | student2 | | ||||||
|  |       | glossary | Banana | Banana is a fruit | student1 | | ||||||
|  |       | glossary | Blackberries | Blackberries is a fruit | student2 | | ||||||
|  |       | glossary | Blackcurrant | Blackcurrant is a fruit | student1 | | ||||||
|  |       | glossary | Blueberries | Blueberries is a fruit | student2 | | ||||||
|  |       | glossary | Breadfruit | Breadfruit is a fruit | student1 | | ||||||
|  |       | glossary | Cantaloupe | Cantaloupe is a fruit | student2 | | ||||||
|  |       | glossary | Carambola | Carambola is a fruit | student1 | | ||||||
|  |       | glossary | Cherimoya | Cherimoya is a fruit | student2 | | ||||||
|  |       | glossary | Cherries | Cherries is a fruit | student1 | | ||||||
|  |       | glossary | Clementine | Clementine is a fruit | student2 | | ||||||
|  |       | glossary | Coconut | Coconut is a fruit | student1 | | ||||||
|  |       | glossary | Cranberries | Cranberries is a fruit | student2 | | ||||||
|  |       | glossary | Date Fruit | Date Fruit is a fruit | student1 | | ||||||
|  |       | glossary | Durian | Durian is a fruit | student2 | | ||||||
|  |       | glossary | Elderberries | Elderberries is a fruit | student1 | | ||||||
|  |       | glossary | Feijoa | Feijoa is a fruit | student2 | | ||||||
|  |       | glossary | Figs | Figs is a fruit | student1 | | ||||||
|  |       | glossary | Gooseberries | Gooseberries are a fruit | student2 | | ||||||
|  |       | glossary | Grapefruit | Grapefruit is a fruit | student1 | | ||||||
|  |       | glossary | Grapes | Grapes are a fruit | student2 | | ||||||
|  |       | glossary | Guava | Guava is a fruit | student1 | | ||||||
|  |       | glossary | Honeydew Melon | Honeydew Melon is a fruit | student2 | | ||||||
|  |       | glossary | Jackfruit | Jackfruit is a fruit | student1 | | ||||||
|  |       | glossary | Java-Plum | Java-Plum is a fruit | student2 | | ||||||
|  |       | glossary | Jujube Fruit | Jujube Fruit is a fruit | student1 | | ||||||
|  |       | glossary | Kiwifruit | Kiwifruit is a fruit | student2 | | ||||||
|  |       | glossary | Kumquat | Kumquat is a fruit | student1 | | ||||||
|  |       | glossary | Lemon | Lemon is a fruit | student2 | | ||||||
|  |       | glossary | lime | lime is a fruit | student1 | | ||||||
|  |       | glossary | Lime | Lime is a fruit | student2 | | ||||||
|  |       | glossary | Longan | Longan is a fruit | student1 | | ||||||
|  |       | glossary | Loquat | Loquat is a fruit | student2 | | ||||||
|  |       | glossary | Lychee | Lychee is a fruit | student1 | | ||||||
|  |       | glossary | Mandarin | Mandarin is a fruit | student2 | | ||||||
|  |       | glossary | Mango | Mango is a fruit | student1 | | ||||||
|  |       | glossary | Mangosteen | Mangosteen is a fruit | student2 | | ||||||
|  |       | glossary | Mulberries | Mulberries are a fruit | student1 | | ||||||
|  |       | glossary | Nectarine | Nectarine is a fruit | student2 | | ||||||
|  |       | glossary | Olives | Olives are a fruit | student1 | | ||||||
|  |       | glossary | Orange | Orange is a fruit | student2 | | ||||||
|  |       | glossary | Papaya | Papaya is a fruit | student1 | | ||||||
|  |       | glossary | Passion Fruit | Passion Fruit is a fruit | student2 | | ||||||
|  |       | glossary | Peaches | Peaches is a fruit | student1 | | ||||||
|  |       | glossary | Pear | Pear is a fruit | student2 | | ||||||
|  |       | glossary | Persimmon | Persimmon is a fruit | student1 | | ||||||
|  |       | glossary | Pitaya | Pitaya is a fruit | student2 | | ||||||
|  |       | glossary | Pineapple | Pineapple is a fruit | student1 | | ||||||
|  |       | glossary | Pitanga | Pitanga is a fruit | student2 | | ||||||
|  |       | glossary | Plantain | Plantain is a fruit | student1 | | ||||||
|  |       | glossary | Plums | Plums are a fruit | student2 | | ||||||
|  |       | glossary | Pomegranate | Pomegranate is a fruit | student1 | | ||||||
|  |       | glossary | Prickly Pear | Prickly Pear is a fruit | student2 | | ||||||
|  |       | glossary | Prunes | Prunes is a fruit | student1 | | ||||||
|  |       | glossary | Pummelo | Pummelo is a fruit | student2 | | ||||||
|  |       | glossary | Quince | Quince is a fruit | student1 | | ||||||
|  |       | glossary | Raspberries | Raspberries are a fruit | student2 | | ||||||
|  |       | glossary | Rhubarb | Rhubarb is a fruit | student1 | | ||||||
|  |       | glossary | Rose-Apple | Rose-Apple is a fruit | student2 | | ||||||
|  |       | glossary | Sapodilla | Sapodilla is a fruit | student1 | | ||||||
|  |       | glossary | Sapote, Mamey | Sapote, Mamey is a fruit | student2 | | ||||||
|  |       | glossary | Soursop | Soursop is a fruit | student1 | | ||||||
|  |       | glossary | Strawberries | Strawberries is a fruit | student2 | | ||||||
|  |       | glossary | Tamarind | Tamarind is a fruit | student2 | | ||||||
|  |       | glossary | Tangerine | Tangerine is a fruit | student1 | | ||||||
|  |       | glossary | Watermelon | Watermelon is a fruit | student2 | | ||||||
|  | 
 | ||||||
|  |   Scenario: Mobile navigation on glossary | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  | 
 | ||||||
|  |     # Alphabetically | ||||||
|  |     When I press "Fruits glossary" in the app | ||||||
|  |     Then I should find "Acerola" in the app | ||||||
|  |     And I should find "Apple" in the app | ||||||
|  |     But I should not find "Honeydew Melon" in the app | ||||||
|  | 
 | ||||||
|  |     # Alphabetically — Infinite loading | ||||||
|  |     When I load more items in the app | ||||||
|  |     Then I should find "Honeydew Melon" in the app | ||||||
|  | 
 | ||||||
|  |     # Alphabetically — Swipe | ||||||
|  |     When I press "Acerola" in the app | ||||||
|  |     Then I should find "Acerola is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Acerola is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Apple is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Apricots are a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     # By author | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I scroll to "Acerola" in the app | ||||||
|  |     And I press "Browse entries" in the app | ||||||
|  |     And I press "Group by author" in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     And I should find "Acerola" in the app | ||||||
|  |     And I should find "Apricots" in the app | ||||||
|  |     But I should not find "Second Student" in the app | ||||||
|  |     And I should not find "Apple" in the app | ||||||
|  | 
 | ||||||
|  |     # By author — Infinite loading | ||||||
|  |     When I load more items in the app | ||||||
|  |     Then I should find "Second Student" in the app | ||||||
|  |     And I should find "Apple" in the app | ||||||
|  | 
 | ||||||
|  |     # By author — Swipe | ||||||
|  |     When I press "Acerola" in the app | ||||||
|  |     Then I should find "Acerola is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Acerola is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Apricots are a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Banana is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     # Search | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I scroll to "Acerola" in the app | ||||||
|  |     And I press "Search" in the app | ||||||
|  |     And I set the field "Search" to "melon" in the app | ||||||
|  |     And I press "Search" "button" near "Clear search" in the app | ||||||
|  |     Then I should find "Honeydew Melon" in the app | ||||||
|  |     And I should find "Watermelon" in the app | ||||||
|  |     But I should not find "Acerola" in the app | ||||||
|  | 
 | ||||||
|  |     # Search — Swipe | ||||||
|  |     When I press "Honeydew Melon" in the app | ||||||
|  |     Then I should find "Honeydew Melon is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Honeydew Melon is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Watermelon is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Watermelon is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     # Offline | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Clear search" in the app | ||||||
|  |     And I press "Add a new entry" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I set the field "Concept" to "Tomato" in the app | ||||||
|  |     And I set the field "Definition" to "Tomato is a fruit" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I press "Add a new entry" in the app | ||||||
|  |     And I set the field "Concept" to "Cashew" in the app | ||||||
|  |     And I set the field "Definition" to "Cashew is a fruit" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     Then I should find "Entries to be synced" in the app | ||||||
|  |     And I should find "Tomato" in the app | ||||||
|  |     And I should find "Cashew" in the app | ||||||
|  | 
 | ||||||
|  |     # Offline — Swipe | ||||||
|  |     When I press "Cashew" in the app | ||||||
|  |     Then I should find "Cashew is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Cashew is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Tomato is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Acerola is a fruit" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Tablet navigation on glossary | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  |     And I change viewport size to "1200x640" | ||||||
|  | 
 | ||||||
|  |     # Alphabetically | ||||||
|  |     When I press "Fruits glossary" in the app | ||||||
|  |     Then I should find "Acerola" in the app | ||||||
|  |     And I should find "Apple" in the app | ||||||
|  |     And "Acerola" near "Apple" should be selected in the app | ||||||
|  |     And I should find "Acerola is a fruit" inside the split-view content in the app | ||||||
|  |     But I should not find "Honeydew Melon" in the app | ||||||
|  | 
 | ||||||
|  |     # Alphabetically — Infinite loading | ||||||
|  |     When I load more items in the app | ||||||
|  |     Then I should find "Honeydew Melon" in the app | ||||||
|  | 
 | ||||||
|  |     # Alphabetically — Split view | ||||||
|  |     When I press "Apple" in the app | ||||||
|  |     Then "Apple" near "Acerola" should be selected in the app | ||||||
|  |     And I should find "Apple is a fruit" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Honeydew Melon" in the app | ||||||
|  |     Then "Honeydew Melon" near "Guava" should be selected in the app | ||||||
|  |     And I should find "Honeydew Melon is a fruit" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     # By author | ||||||
|  |     When I press "Apple" in the app | ||||||
|  |     When I scroll to "Apple" in the app | ||||||
|  |     And I press "Browse entries" in the app | ||||||
|  |     And I press "Group by author" in the app | ||||||
|  |     Then I should find "First Student" in the app | ||||||
|  |     And I should find "Acerola" in the app | ||||||
|  |     And I should find "Apricots" in the app | ||||||
|  |     And "Acerola" near "Apricots" should be selected in the app | ||||||
|  |     And I should find "Acerola is a fruit" inside the split-view content in the app | ||||||
|  |     But I should not find "Second Student" in the app | ||||||
|  |     And I should not find "Apple" in the app | ||||||
|  | 
 | ||||||
|  |     # By author — Infinite loading | ||||||
|  |     When I load more items in the app | ||||||
|  |     Then I should find "Second Student" in the app | ||||||
|  |     And I should find "Apple" in the app | ||||||
|  | 
 | ||||||
|  |     # By author — Split view | ||||||
|  |     When I press "Apricots" in the app | ||||||
|  |     And "Apricots" near "Acerola" should be selected in the app | ||||||
|  |     And I should find "Apricots are a fruit" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Apple" in the app | ||||||
|  |     And "Apple" near "Persimmon" should be selected in the app | ||||||
|  |     And I should find "Apple is a fruit" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     # Search | ||||||
|  |     When I press "Search" in the app | ||||||
|  |     And I set the field "Search" to "melon" in the app | ||||||
|  |     And I press "Search" "button" near "Clear search" in the app | ||||||
|  |     Then I should find "Honeydew Melon" in the app | ||||||
|  |     And I should find "Watermelon" in the app | ||||||
|  |     And "Honeydew Melon" near "Watermelon" should be selected in the app | ||||||
|  |     And I should find "Honeydew Melon is a fruit" inside the split-view content in the app | ||||||
|  |     But I should not find "Acerola" in the app | ||||||
|  | 
 | ||||||
|  |     # Search — Split view | ||||||
|  |     When I press "Watermelon" in the app | ||||||
|  |     Then "Watermelon" near "Honeydew Melon" should be selected in the app | ||||||
|  |     And I should find "Watermelon is a fruit" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     # Offline | ||||||
|  |     When I press "Clear search" in the app | ||||||
|  |     And I press "Add a new entry" in the app | ||||||
|  |     And I switch offline mode to "true" | ||||||
|  |     And I set the field "Concept" to "Tomato" in the app | ||||||
|  |     And I set the field "Definition" to "Tomato is a fruit" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I set the field "Concept" to "Cashew" in the app | ||||||
|  |     And I set the field "Definition" to "Cashew is a fruit" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     Then I should find "Entries to be synced" in the app | ||||||
|  |     And I should find "Tomato" in the app | ||||||
|  |     And I should find "Cashew" in the app | ||||||
|  | 
 | ||||||
|  |     # Offline — Split view | ||||||
|  |     When I press "Cashew" in the app | ||||||
|  |     Then "Cashew" near "Tomato" should be selected in the app | ||||||
|  |     And I should find "Cashew is a fruit" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Tomato" in the app | ||||||
|  |     Then "Tomato" near "Cashew" should be selected in the app | ||||||
|  |     And I should find "Tomato is a fruit" inside the split-view content in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Acerola" in the app | ||||||
|  |     Then "Acerola" near "Tomato" should be selected in the app | ||||||
|  |     And I should find "Acerola is a fruit" inside the split-view content in the app | ||||||
							
								
								
									
										162
									
								
								src/addons/mod/quiz/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										162
									
								
								src/addons/mod/quiz/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,162 @@ | |||||||
|  | @mod @mod_quiz @app @javascript | ||||||
|  | Feature: Attempt a quiz in app | ||||||
|  |   As a student | ||||||
|  |   In order to demonstrate what I know | ||||||
|  |   I need to be able to attempt quizzes | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  |       | teacher1 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role           | | ||||||
|  |       | student1 | C1     | student        | | ||||||
|  |       | teacher1 | C1     | editingteacher | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name   | intro              | course | idnumber | | ||||||
|  |       | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | | ||||||
|  |     And the following "question categories" exist: | ||||||
|  |       | contextlevel | reference | name           | | ||||||
|  |       | Course       | C1        | Test questions | | ||||||
|  |     And the following "questions" exist: | ||||||
|  |       | questioncategory | qtype       | name  | questiontext                | | ||||||
|  |       | Test questions   | truefalse   | TF1   | Text of the first question  | | ||||||
|  |       | Test questions   | truefalse   | TF2   | Text of the second question | | ||||||
|  |     And quiz "Quiz 1" contains the following questions: | ||||||
|  |       | question | page | | ||||||
|  |       | TF1      | 1    | | ||||||
|  |       | TF2      | 2    | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name   | intro              | course | idnumber | | ||||||
|  |       | quiz       | Quiz 2 | Quiz 2 description | C1     | quiz2    | | ||||||
|  |     And the following "question categories" exist: | ||||||
|  |       | contextlevel | reference | name            | | ||||||
|  |       | Course       | C1        | Test questions 2| | ||||||
|  |     And the following "questions" exist: | ||||||
|  |       | questioncategory | qtype            | name  | questiontext                | | ||||||
|  |       | Test questions   | multichoice      | TF3   | Text of the first question  | | ||||||
|  |       | Test questions   | shortanswer      | TF4   | Text of the second question | | ||||||
|  |       | Test questions   | numerical        | TF5   | Text of the third question  | | ||||||
|  |       | Test questions   | essay            | TF6   | Text of the fourth question | | ||||||
|  |       | Test questions   | ddwtos           | TF7   | The [[1]] brown [[2]] jumped over the [[3]] dog. | | ||||||
|  |       | Test questions   | truefalse        | TF8   | Text of the sixth question  | | ||||||
|  |       | Test questions   | match            | TF9   | Text of the seventh question  | | ||||||
|  |     And quiz "Quiz 2" contains the following questions: | ||||||
|  |       | question | page | | ||||||
|  |       | TF3      | 1    | | ||||||
|  |       | TF4      | 2    | | ||||||
|  |       | TF5      | 3    | | ||||||
|  |       | TF6      | 4    | | ||||||
|  |       | TF7      | 5    | | ||||||
|  |       | TF8      | 6    | | ||||||
|  |       | TF9      | 7    | | ||||||
|  | 
 | ||||||
|  |   Scenario: View a quiz entry page (attempts, status, etc.) | ||||||
|  |     Given I entered the quiz activity "Quiz 1" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Attempt quiz now" in the app | ||||||
|  |     Then I should find "Text of the first question" in the app | ||||||
|  |     But I should not find "Text of the second question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next" near "Question 1" in the app | ||||||
|  |     Then I should find "Text of the second question" in the app | ||||||
|  |     But I should not find "Text of the first question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Previous" near "Question 2" in the app | ||||||
|  |     Then I should find "Text of the first question" in the app | ||||||
|  |     But I should not find "Text of the second question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next" near "Quiz 1" in the app | ||||||
|  |     Then I should find "Text of the second question" in the app | ||||||
|  |     But I should not find "Text of the first question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Previous" near "Quiz 1" in the app | ||||||
|  |     Then I should find "Text of the first question" in the app | ||||||
|  |     But I should not find "Text of the second question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next" near "Question 1" in the app | ||||||
|  |     And I press "Submit" near "Quiz 1" in the app | ||||||
|  |     Then I should find "Summary of attempt" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Not yet answered" within "2" "ion-item" in the app | ||||||
|  |     Then I should find "Text of the second question" in the app | ||||||
|  |     But I should not find "Text of the first question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submit" in the app | ||||||
|  |     And I press "Submit all and finish" in the app | ||||||
|  |     Then I should find "Once you submit" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Cancel" near "Once you submit" in the app | ||||||
|  |     Then I should find "Summary of attempt" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submit all and finish" in the app | ||||||
|  |     And I press "OK" near "Once you submit" in the app | ||||||
|  |     Then I should find "Review" in the app | ||||||
|  |     And I should find "Started on" in the app | ||||||
|  |     And I should find "State" in the app | ||||||
|  |     And I should find "Completed on" in the app | ||||||
|  |     And I should find "Time taken" in the app | ||||||
|  |     And I should find "Marks" in the app | ||||||
|  |     And I should find "Grade" in the app | ||||||
|  |     And I should find "Question 1" in the app | ||||||
|  |     And I should find "Question 2" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Attempt a quiz (all question types) | ||||||
|  |     Given I entered the quiz activity "Quiz 2" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Attempt quiz now" in the app | ||||||
|  |     And I press "Four" in the app | ||||||
|  |     And I press "Three" in the app | ||||||
|  |     And I press "Next" near "Quiz 2" in the app | ||||||
|  |     And I set the field "Answer" to "testing" in the app | ||||||
|  |     And I press "Next" near "Question 2" in the app | ||||||
|  |     And I set the field "Answer" to "5" in the app | ||||||
|  |     And I press "Next" near "Question 3" in the app | ||||||
|  |     And I set the field "Answer" to "Testing an essay" in the app | ||||||
|  |     And I press "Next" "ion-button" near "Question 4" in the app | ||||||
|  |     And I press "quick" ".drag" in the app | ||||||
|  |     And I press "" ".place1.drop" in the app | ||||||
|  |     And I press "fox" ".drag" in the app | ||||||
|  |     And I press "" ".place2.drop" in the app | ||||||
|  |     And I press "lazy" ".drag" in the app | ||||||
|  |     And I press "" ".place3.drop" in the app | ||||||
|  |     And I press "Next" near "Question 5" in the app | ||||||
|  |     And I press "True" in the app | ||||||
|  |     And I press "Next" near "Question 6" in the app | ||||||
|  |     And I press "Choose... , frog" in the app | ||||||
|  |     And I press "amphibian" in the app | ||||||
|  |     And I press "Choose... , newt" in the app | ||||||
|  |     And I press "insect" in the app | ||||||
|  |     And I press "Choose... , cat" in the app | ||||||
|  |     And I press "mammal" in the app | ||||||
|  |     And I press "Submit" near "Question 7" in the app | ||||||
|  |     Then I should not find "Not yet answered" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submit all and finish" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "Review" in the app | ||||||
|  |     And I should find "Finished" in the app | ||||||
|  |     And I should find "Not yet graded" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Submit a quiz & Review a quiz attempt | ||||||
|  |     Given I entered the quiz activity "Quiz 1" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Attempt quiz now" in the app | ||||||
|  |     And I press "True" in the app | ||||||
|  |     And I press "Next" near "Question 1" in the app | ||||||
|  |     And I press "False" in the app | ||||||
|  |     And I press "Submit" near "Question 2" in the app | ||||||
|  |     And I press "Submit all and finish" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then I should find "Review" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the quiz activity "Quiz 1" on course "Course 1" as "teacher1" in the app | ||||||
|  |     When I press "Information" 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 follow "Attempts: 1" | ||||||
|  |     And I follow "Review attempt" | ||||||
|  |     Then I should see "Finished" | ||||||
|  |     And I should see "1.00/2.00" | ||||||
							
								
								
									
										71
									
								
								src/addons/mod/quiz/tests/behat/quiz_navigation.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/addons/mod/quiz/tests/behat/quiz_navigation.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | |||||||
|  | @mod @mod_quiz @app @javascript | ||||||
|  | Feature: Attempt a quiz in app | ||||||
|  |   As a student | ||||||
|  |   In order to demonstrate what I know | ||||||
|  |   I need to be able to attempt quizzes | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name   | intro              | course | idnumber | | ||||||
|  |       | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | | ||||||
|  |     And the following "question categories" exist: | ||||||
|  |       | contextlevel | reference | name           | | ||||||
|  |       | Course       | C1        | Test questions | | ||||||
|  |     And the following "questions" exist: | ||||||
|  |       | questioncategory | qtype       | name  | questiontext                | | ||||||
|  |       | Test questions   | truefalse   | TF1   | Text of the first question  | | ||||||
|  |       | Test questions   | truefalse   | TF2   | Text of the second question | | ||||||
|  |     And quiz "Quiz 1" contains the following questions: | ||||||
|  |       | question | page | | ||||||
|  |       | TF1      | 1    | | ||||||
|  |       | TF2      | 2    | | ||||||
|  | 
 | ||||||
|  |   Scenario: Next and previous navigation | ||||||
|  |     Given I entered the quiz activity "Quiz 1" on course "Course 1" as "student1" in the app | ||||||
|  |     And I press "Attempt quiz now" in the app | ||||||
|  |     Then I should find "Text of the first question" in the app | ||||||
|  |     But I should not find "Text of the second question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next" near "Question 1" in the app | ||||||
|  |     Then I should find "Text of the second question" in the app | ||||||
|  |     But I should not find "Text of the first question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Previous" near "Question 2" in the app | ||||||
|  |     Then I should find "Text of the first question" in the app | ||||||
|  |     But I should not find "Text of the second question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next" near "Quiz 1" in the app | ||||||
|  |     Then I should find "Text of the second question" in the app | ||||||
|  |     But I should not find "Text of the first question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Previous" near "Quiz 1" in the app | ||||||
|  |     Then I should find "Text of the first question" in the app | ||||||
|  |     But I should not find "Text of the second question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next" near "Question 1" in the app | ||||||
|  |     And I press "Submit" in the app | ||||||
|  |     Then I should find "Summary of attempt" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Not yet answered" within "2" "ion-item" in the app | ||||||
|  |     Then I should find "Text of the second question" in the app | ||||||
|  |     But I should not find "Text of the first question" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submit" in the app | ||||||
|  |     And I press "Submit all and finish" in the app | ||||||
|  |     Then I should find "Once you submit" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Cancel" near "Once you submit" in the app | ||||||
|  |     Then I should find "Summary of attempt" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Submit all and finish" in the app | ||||||
|  |     And I press "OK" near "Once you submit" in the app | ||||||
|  |     Then I should find "Review" in the app | ||||||
| @ -32,6 +32,7 @@ import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; | |||||||
| import { CoreCronDelegate } from '@services/cron'; | import { CoreCronDelegate } from '@services/cron'; | ||||||
| import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron'; | import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron'; | ||||||
| import { moodleTransitionAnimation } from '@classes/page-transition'; | import { moodleTransitionAnimation } from '@classes/page-transition'; | ||||||
|  | import { BehatTestingModule } from '@/testing/behat-testing.module'; | ||||||
| 
 | 
 | ||||||
| // For translate loader. AoT requires an exported function for factories.
 | // For translate loader. AoT requires an exported function for factories.
 | ||||||
| export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { | export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { | ||||||
| @ -59,6 +60,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { | |||||||
|         AppRoutingModule, |         AppRoutingModule, | ||||||
|         CoreModule, |         CoreModule, | ||||||
|         AddonsModule, |         AddonsModule, | ||||||
|  |         BehatTestingModule, | ||||||
|     ], |     ], | ||||||
|     providers: [ |     providers: [ | ||||||
|         { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, |         { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, | ||||||
|  | |||||||
| @ -208,10 +208,17 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { | |||||||
|                 await content.componentOnReady(); |                 await content.componentOnReady(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             parentPage = parentPage.parentElement.closest('.ion-page'); |             parentPage = parentPage.parentElement.closest('.ion-page, .ion-page-hidden, .ion-page-invisible'); | ||||||
| 
 | 
 | ||||||
|             // Check if the page has a header. If it doesn't, search the next parent page.
 |             // Check if the page has a header. If it doesn't, search the next parent page.
 | ||||||
|             const header  = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header'); |             let header  = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header'); | ||||||
|  | 
 | ||||||
|  |             if (header && getComputedStyle(header).display !== 'none') { | ||||||
|  |                 return header; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Find using content if any.
 | ||||||
|  |             header = content?.parentElement?.querySelector<HTMLIonHeaderElement>(':scope > ion-header'); | ||||||
| 
 | 
 | ||||||
|             if (header && getComputedStyle(header).display !== 'none') { |             if (header && getComputedStyle(header).display !== 'none') { | ||||||
|                 return header; |                 return header; | ||||||
|  | |||||||
							
								
								
									
										143
									
								
								src/core/features/course/tests/behat/basic_usage-311.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										143
									
								
								src/core/features/course/tests/behat/basic_usage-311.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,143 @@ | |||||||
|  | @core @core_course @app @javascript @lms_upto3.11 | ||||||
|  | Feature: Test basic usage of one course in app | ||||||
|  |   In order to participate in one course while using the mobile app | ||||||
|  |   As a student | ||||||
|  |   I need basic course functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email | | ||||||
|  |       | teacher1 | Teacher | teacher | teacher1@example.com | | ||||||
|  |       | student1 | Student | student | student1@example.com | | ||||||
|  |       | student2 | Student2 | student2 | student2@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name            | intro                   | course | idnumber | option                       | section | | ||||||
|  |       | choice   | Choice course 1 | Test choice description | C1     | choice1  | Option 1, Option 2, Option 3 | 1       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | course | idnumber | name                | intro                       | assignsubmission_onlinetext_enabled | section | | ||||||
|  |       | assign   | C1     | assign1  | assignment          | Test assignment description | 1                                   | 1       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name            | intro       | course | idnumber | groupmode | assessed | scale[modgrade_type] | | ||||||
|  |       | forum      | Test forum name | Test forum  | C1     | forum    | 0         | 5        | Point                | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name            | intro       | course | idnumber | groupmode | section | | ||||||
|  |       | chat       | Test chat name  | Test chat   | C1     | chat     | 0         | 2       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name      | intro        | course | idnumber | section | | ||||||
|  |       | data     | Web links | Useful links | C1     | data1    | 4       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name               | intro          | course | idnumber    | groupmode | section | | ||||||
|  |       | lti           | Test external name | Test external  | C1     | external    | 0         | 1       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name               | intro          | course | idnumber    | groupmode | section | | ||||||
|  |       | feedback      | Test feedback name | Test feedback  | C1     | feedback    | 0         | 3       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name          | intro                | course | idnumber  | section | | ||||||
|  |       | glossary | Test glossary | glossary description | C1     | gloss1    | 5       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name   | intro              | course | idnumber | section | | ||||||
|  |       | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | 2       | | ||||||
|  |     And the following "question categories" exist: | ||||||
|  |       | contextlevel | reference | name           | | ||||||
|  |       | Course       | C1        | Test questions | | ||||||
|  |     And the following "questions" exist: | ||||||
|  |       | questioncategory | qtype       | name  | questiontext                | | ||||||
|  |       | Test questions   | truefalse   | TF1   | Text of the first question  | | ||||||
|  |       | Test questions   | truefalse   | TF2   | Text of the second question | | ||||||
|  |     And quiz "Quiz 1" contains the following questions: | ||||||
|  |       | question | page | | ||||||
|  |       | TF1      | 1    | | ||||||
|  |       | TF2      | 2    | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity    | name             | intro        | course | idnumber  | groupmode | section | | ||||||
|  |       | survey      | Test survey name | Test survey  | C1     | survey    | 0         | 1       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity    | name             | intro        | course | idnumber  | groupmode | | ||||||
|  |       | wiki        | Test wiki name   | Test wiki    | C1     | wiki      | 0         | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name               | intro          | course | idnumber    | groupmode | section | | ||||||
|  |       | lesson        | Test lesson name   | Test lesson    | C1     | lesson      | 0         | 3       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name               | intro          | course | idnumber    | groupmode | section | | ||||||
|  |       | scorm         | Test scorm name    | Test scorm     | C1     | scorm       | 0         | 2       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name                  | intro             | course | idnumber       | groupmode | section | | ||||||
|  |       | workshop      | Test workshop name    | Test workshop     | C1     | workshop       | 0         | 3       | | ||||||
|  | 
 | ||||||
|  |   Scenario: Self enrol | ||||||
|  |     Given 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 press "Actions menu" | ||||||
|  |     And I follow "More..." | ||||||
|  |     And I follow "Users" | ||||||
|  |     And I follow "Enrolment methods" | ||||||
|  |     And I click on "Enable" "icon" in the "Self enrolment (Student)" "table_row" | ||||||
|  |     And I close the browser tab opened by the app | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I press "Site home" in the app | ||||||
|  |     And I press "Available courses" in the app | ||||||
|  |     And I press "Course 1" in the app | ||||||
|  |     And I press "Enrol me" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     And I wait loading to finish in the app | ||||||
|  |     Then the header should be "Course 1" in the app | ||||||
|  |     And I should find "Choice course 1" in the app | ||||||
|  |     And I should find "assignment" in the app | ||||||
|  |     And I should find "Test forum name" in the app | ||||||
|  |     And I should find "Test chat name" in the app | ||||||
|  |     And I should find "Web links" in the app | ||||||
|  |     And I should find "Test external name" in the app | ||||||
|  |     And I should find "Test feedback name" in the app | ||||||
|  |     And I should find "Test glossary" in the app | ||||||
|  |     And I should find "Quiz 1" in the app | ||||||
|  |     And I should find "Test survey name" in the app | ||||||
|  |     And I should find "Test wiki name" in the app | ||||||
|  |     And I should find "Test lesson name" in the app | ||||||
|  |     And I should find "Test scorm name" in the app | ||||||
|  |     And I should find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Guest access | ||||||
|  |     Given 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 press "Actions menu" | ||||||
|  |     And I follow "More..." | ||||||
|  |     And I follow "Users" | ||||||
|  |     And I follow "Enrolment methods" | ||||||
|  |     And I click on "Enable" "icon" in the "Guest access" "table_row" | ||||||
|  |     And I close the browser tab opened by the app | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I press "Site home" in the app | ||||||
|  |     And I press "Available courses" in the app | ||||||
|  |     And I press "Course 1" in the app | ||||||
|  | 
 | ||||||
|  |     Then I should find "Course summary" in the app | ||||||
|  |     And I should find "Course" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "View course" "ion-button" in the app | ||||||
|  |     Then the header should be "Course 1" in the app | ||||||
|  |     And I should find "Choice course 1" in the app | ||||||
|  |     And I should find "assignment" in the app | ||||||
|  |     And I should find "Test forum name" in the app | ||||||
|  |     And I should find "Test chat name" in the app | ||||||
|  |     And I should find "Web links" in the app | ||||||
|  |     And I should find "Test feedback name" in the app | ||||||
|  |     And I should find "Test glossary" in the app | ||||||
|  |     And I should find "Quiz 1" in the app | ||||||
|  |     And I should find "Test survey name" in the app | ||||||
|  |     And I should find "Test wiki name" in the app | ||||||
|  |     And I should find "Test lesson name" in the app | ||||||
|  |     And I should find "Test scorm name" in the app | ||||||
|  |     And I should find "Test workshop name" in the app | ||||||
							
								
								
									
										496
									
								
								src/core/features/course/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										496
									
								
								src/core/features/course/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,496 @@ | |||||||
|  | @core @core_course @app @javascript | ||||||
|  | Feature: Test basic usage of one course in app | ||||||
|  |   In order to participate in one course while using the mobile app | ||||||
|  |   As a student | ||||||
|  |   I need basic course functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email | | ||||||
|  |       | teacher1 | Teacher | teacher | teacher1@example.com | | ||||||
|  |       | student1 | Student | student | student1@example.com | | ||||||
|  |       | student2 | Student2 | student2 | student2@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name            | intro                   | course | idnumber | option                       | section | | ||||||
|  |       | choice   | Choice course 1 | Test choice description | C1     | choice1  | Option 1, Option 2, Option 3 | 1       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | course | idnumber | name                | intro                       | assignsubmission_onlinetext_enabled | section | | ||||||
|  |       | assign   | C1     | assign1  | assignment          | Test assignment description | 1                                   | 1       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name            | intro       | course | idnumber | groupmode | assessed | scale[modgrade_type] | | ||||||
|  |       | forum      | Test forum name | Test forum  | C1     | forum    | 0         | 5        | Point                | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name            | intro       | course | idnumber | groupmode | section | | ||||||
|  |       | chat       | Test chat name  | Test chat   | C1     | chat     | 0         | 2       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name      | intro        | course | idnumber | section | | ||||||
|  |       | data     | Web links | Useful links | C1     | data1    | 4       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name               | intro          | course | idnumber    | groupmode | section | | ||||||
|  |       | lti           | Test external name | Test external  | C1     | external    | 0         | 1       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name               | intro          | course | idnumber    | groupmode | section | | ||||||
|  |       | feedback      | Test feedback name | Test feedback  | C1     | feedback    | 0         | 3       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name          | intro                | course | idnumber  | section | | ||||||
|  |       | glossary | Test glossary | glossary description | C1     | gloss1    | 5       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name   | intro              | course | idnumber | section | | ||||||
|  |       | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | 2       | | ||||||
|  |     And the following "question categories" exist: | ||||||
|  |       | contextlevel | reference | name           | | ||||||
|  |       | Course       | C1        | Test questions | | ||||||
|  |     And the following "questions" exist: | ||||||
|  |       | questioncategory | qtype       | name  | questiontext                | | ||||||
|  |       | Test questions   | truefalse   | TF1   | Text of the first question  | | ||||||
|  |       | Test questions   | truefalse   | TF2   | Text of the second question | | ||||||
|  |     And quiz "Quiz 1" contains the following questions: | ||||||
|  |       | question | page | | ||||||
|  |       | TF1      | 1    | | ||||||
|  |       | TF2      | 2    | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity    | name             | intro        | course | idnumber  | groupmode | section | | ||||||
|  |       | survey      | Test survey name | Test survey  | C1     | survey    | 0         | 1       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity    | name             | intro        | course | idnumber  | groupmode | | ||||||
|  |       | wiki        | Test wiki name   | Test wiki    | C1     | wiki      | 0         | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name               | intro          | course | idnumber    | groupmode | section | | ||||||
|  |       | lesson        | Test lesson name   | Test lesson    | C1     | lesson      | 0         | 3       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name               | intro          | course | idnumber    | groupmode | section | | ||||||
|  |       | scorm         | Test scorm name    | Test scorm     | C1     | scorm       | 0         | 2       | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity      | name                  | intro             | course | idnumber       | groupmode | section | | ||||||
|  |       | workshop      | Test workshop name    | Test workshop     | C1     | workshop       | 0         | 3       | | ||||||
|  | 
 | ||||||
|  |   Scenario: View course contents | ||||||
|  |     When I entered the course "Course 1" as "student1" in the app | ||||||
|  |     Then the header should be "Course 1" in the app | ||||||
|  |     And I should find "Choice course 1" in the app | ||||||
|  |     And I should find "assignment" in the app | ||||||
|  |     And I should find "Test forum name" in the app | ||||||
|  |     And I should find "Test chat name" in the app | ||||||
|  |     And I should find "Web links" in the app | ||||||
|  |     And I should find "Test external name" in the app | ||||||
|  |     And I should find "Test feedback name" in the app | ||||||
|  |     And I should find "Test glossary" in the app | ||||||
|  |     And I should find "Quiz 1" in the app | ||||||
|  |     And I should find "Test survey name" in the app | ||||||
|  |     And I should find "Test wiki name" in the app | ||||||
|  |     And I should find "Test lesson name" in the app | ||||||
|  |     And I should find "Test scorm name" in the app | ||||||
|  |     And I should find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Choice course 1" in the app | ||||||
|  |     Then the header should be "Choice course 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "assignment" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test forum name" in the app | ||||||
|  |     Then the header should be "Test forum name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test chat name" in the app | ||||||
|  |     Then the header should be "Test chat name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Web links" in the app | ||||||
|  |     Then the header should be "Web links" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test external name" in the app | ||||||
|  |     Then the header should be "Test external name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test feedback name" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then the header should be "Test feedback name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test glossary" in the app | ||||||
|  |     Then the header should be "Test glossary" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Quiz 1" in the app | ||||||
|  |     Then the header should be "Quiz 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test survey name" in the app | ||||||
|  |     Then the header should be "Test survey name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test wiki name" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then the header should be "Test wiki name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test lesson name" in the app | ||||||
|  |     Then the header should be "Test lesson name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test scorm name" in the app | ||||||
|  |     Then the header should be "Test scorm name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test workshop name" in the app | ||||||
|  |     Then the header should be "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: View section contents | ||||||
|  |     When I entered the course "Course 1" as "student1" in the app | ||||||
|  |     Then the header should be "Course 1" in the app | ||||||
|  |     And I should find "Choice course 1" in the app | ||||||
|  |     And I should find "assignment" in the app | ||||||
|  |     And I should find "Test forum name" in the app | ||||||
|  |     And I should find "Test chat name" in the app | ||||||
|  |     And I should find "Web links" in the app | ||||||
|  |     And I should find "Test external name" in the app | ||||||
|  |     And I should find "Test feedback name" in the app | ||||||
|  |     And I should find "Test glossary" in the app | ||||||
|  |     And I should find "Quiz 1" in the app | ||||||
|  |     And I should find "Test survey name" in the app | ||||||
|  |     And I should find "Test wiki name" in the app | ||||||
|  |     And I should find "Test lesson name" in the app | ||||||
|  |     And I should find "Test scorm name" in the app | ||||||
|  |     And I should find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Course index" in the app | ||||||
|  |     And I press "General" in the app | ||||||
|  |     Then I should find "Test forum name" in the app | ||||||
|  |     And I should find "Test wiki name" in the app | ||||||
|  |     But I should not find "Choice course 1" in the app | ||||||
|  |     And I should not find "assignment" in the app | ||||||
|  |     And I should not find "Test chat name" in the app | ||||||
|  |     And I should not find "Web links" in the app | ||||||
|  |     And I should not find "Test external name" in the app | ||||||
|  |     And I should not find "Test feedback name" in the app | ||||||
|  |     And I should not find "Test glossary" in the app | ||||||
|  |     And I should not find "Quiz 1" in the app | ||||||
|  |     And I should not find "Test survey name" in the app | ||||||
|  |     And I should not find "Test lesson name" in the app | ||||||
|  |     And I should not find "Test scorm name" in the app | ||||||
|  |     And I should not find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Test forum name" in the app | ||||||
|  |     Then the header should be "Test forum name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test wiki name" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then the header should be "Test wiki name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course index" in the app | ||||||
|  |     And I press "Topic 1" in the app | ||||||
|  |     Then I should find "Choice course 1" in the app | ||||||
|  |     And I should find "assignment" in the app | ||||||
|  |     And I should find "Test external name" in the app | ||||||
|  |     And I should find "Test survey name" in the app | ||||||
|  |     But I should not find "Test forum name" in the app | ||||||
|  |     And I should not find "Test chat name" in the app | ||||||
|  |     And I should not find "Web links" in the app | ||||||
|  |     And I should not find "Test feedback name" in the app | ||||||
|  |     And I should not find "Test glossary" in the app | ||||||
|  |     And I should not find "Quiz 1" in the app | ||||||
|  |     And I should not find "Test wiki name" in the app | ||||||
|  |     And I should not find "Test lesson name" in the app | ||||||
|  |     And I should not find "Test scorm name" in the app | ||||||
|  |     And I should not find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Choice course 1" in the app | ||||||
|  |     Then the header should be "Choice course 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "assignment" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test external name" in the app | ||||||
|  |     Then the header should be "Test external name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test survey name" in the app | ||||||
|  |     Then the header should be "Test survey name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course index" in the app | ||||||
|  |     And I press "Topic 2" in the app | ||||||
|  |     Then I should find "Quiz 1" in the app | ||||||
|  |     And I should find "Test chat name" in the app | ||||||
|  |     And I should find "Test scorm name" in the app | ||||||
|  |     But I should not find "Choice course 1" in the app | ||||||
|  |     And I should not find "assignment" in the app | ||||||
|  |     And I should not find "Test forum name" in the app | ||||||
|  |     And I should not find "Web links" in the app | ||||||
|  |     And I should not find "Test external name" in the app | ||||||
|  |     And I should not find "Test feedback name" in the app | ||||||
|  |     And I should not find "Test glossary" in the app | ||||||
|  |     And I should not find "Test survey name" in the app | ||||||
|  |     And I should not find "Test wiki name" in the app | ||||||
|  |     And I should not find "Test lesson name" in the app | ||||||
|  |     And I should not find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Test chat name" in the app | ||||||
|  |     Then the header should be "Test chat name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Quiz 1" in the app | ||||||
|  |     Then the header should be "Quiz 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test scorm name" in the app | ||||||
|  |     Then the header should be "Test scorm name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course index" in the app | ||||||
|  |     And I press "Topic 3" in the app | ||||||
|  |     Then I should find "Test feedback name" in the app | ||||||
|  |     And I should find "Test lesson name" in the app | ||||||
|  |     And I should find "Test workshop name" in the app | ||||||
|  |     But I should not find "Choice course 1" in the app | ||||||
|  |     And I should not find "assignment" in the app | ||||||
|  |     And I should not find "Test forum name" in the app | ||||||
|  |     And I should not find "Test chat name" in the app | ||||||
|  |     And I should not find "Web links" in the app | ||||||
|  |     And I should not find "Test external name" in the app | ||||||
|  |     And I should not find "Test glossary" in the app | ||||||
|  |     And I should not find "Quiz 1" in the app | ||||||
|  |     And I should not find "Test survey name" in the app | ||||||
|  |     And I should not find "Test wiki name" in the app | ||||||
|  |     And I should not find "Test scorm name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Test feedback name" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then the header should be "Test feedback name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test lesson name" in the app | ||||||
|  |     Then the header should be "Test lesson name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Test workshop name" in the app | ||||||
|  |     Then the header should be "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course index" in the app | ||||||
|  |     And I press "Topic 4" in the app | ||||||
|  |     Then I should find "Web links" in the app | ||||||
|  |     But I should not find "Choice course 1" in the app | ||||||
|  |     And I should not find "assignment" in the app | ||||||
|  |     And I should not find "Test forum name" in the app | ||||||
|  |     And I should not find "Test chat name" in the app | ||||||
|  |     And I should not find "Test external name" in the app | ||||||
|  |     And I should not find "Test feedback name" in the app | ||||||
|  |     And I should not find "Test glossary" in the app | ||||||
|  |     And I should not find "Quiz 1" in the app | ||||||
|  |     And I should not find "Test survey name" in the app | ||||||
|  |     And I should not find "Test wiki name" in the app | ||||||
|  |     And I should not find "Test lesson name" in the app | ||||||
|  |     And I should not find "Test scorm name" in the app | ||||||
|  |     And I should not find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Web links" in the app | ||||||
|  |     Then the header should be "Web links" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course index" in the app | ||||||
|  |     And I press "Topic 5" in the app | ||||||
|  |     Then I should find "Test glossary" in the app | ||||||
|  |     But I should not find "Choice course 1" in the app | ||||||
|  |     And I should not find "assignment" in the app | ||||||
|  |     And I should not find "Test forum name" in the app | ||||||
|  |     And I should not find "Test chat name" in the app | ||||||
|  |     And I should not find "Web links" in the app | ||||||
|  |     And I should not find "Test external name" in the app | ||||||
|  |     And I should not find "Test feedback name" in the app | ||||||
|  |     And I should not find "Quiz 1" in the app | ||||||
|  |     And I should not find "Test survey name" in the app | ||||||
|  |     And I should not find "Test wiki name" in the app | ||||||
|  |     And I should not find "Test lesson name" in the app | ||||||
|  |     And I should not find "Test scorm name" in the app | ||||||
|  |     And I should not find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Test glossary" in the app | ||||||
|  |     Then the header should be "Test glossary" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Navigation between sections using the bottom arrows | ||||||
|  |     When I entered the course "Course 1" as "student1" in the app | ||||||
|  |     Then the header should be "Course 1" in the app | ||||||
|  |     And I should find "Choice course 1" in the app | ||||||
|  |     And I should find "assignment" in the app | ||||||
|  |     And I should find "Test forum name" in the app | ||||||
|  |     And I should find "Test chat name" in the app | ||||||
|  |     And I should find "Web links" in the app | ||||||
|  |     And I should find "Test external name" in the app | ||||||
|  |     And I should find "Test feedback name" in the app | ||||||
|  |     And I should find "Test glossary" in the app | ||||||
|  |     And I should find "Quiz 1" in the app | ||||||
|  |     And I should find "Test survey name" in the app | ||||||
|  |     And I should find "Test wiki name" in the app | ||||||
|  |     And I should find "Test lesson name" in the app | ||||||
|  |     And I should find "Test scorm name" in the app | ||||||
|  |     And I should find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Course index" in the app | ||||||
|  |     And I press "General" in the app | ||||||
|  |     Then I should find "General" in the app | ||||||
|  |     And I should find "Next: Topic 1" in the app | ||||||
|  |     But I should not find "Topic 2" in the app | ||||||
|  |     And I should not find "Topic 3" in the app | ||||||
|  |     And I should not find "Topic 4" in the app | ||||||
|  |     And I should not find "Topic 5" in the app | ||||||
|  |     And I should not find "Previous:" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next:" in the app | ||||||
|  |     Then I should find "Topic 1" in the app | ||||||
|  |     And I should find "Previous: General" in the app | ||||||
|  |     And I should find "Next: Topic 2" in the app | ||||||
|  |     But I should not find "Topic 3" in the app | ||||||
|  |     And I should not find "Topic 4" in the app | ||||||
|  |     And I should not find "Topic 5" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next:" in the app | ||||||
|  |     Then I should find "Topic 2" in the app | ||||||
|  |     And I should find "Previous: Topic 1" in the app | ||||||
|  |     And I should find "Next: Topic 3" in the app | ||||||
|  |     But I should not find "General" in the app | ||||||
|  |     And I should not find "Topic 4" in the app | ||||||
|  |     And I should not find "Topic 5" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next:" in the app | ||||||
|  |     Then I should find "Topic 3" in the app | ||||||
|  |     And I should find "Previous: Topic 2" in the app | ||||||
|  |     And I should find "Next: Topic 4" in the app | ||||||
|  |     But I should not find "General" in the app | ||||||
|  |     And I should not find "Topic 1" in the app | ||||||
|  |     And I should not find "Topic 5" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next:" in the app | ||||||
|  |     Then I should find "Topic 4" in the app | ||||||
|  |     And I should find "Previous: Topic 3" in the app | ||||||
|  |     And I should find "Next: Topic 5" in the app | ||||||
|  |     But I should not find "General" in the app | ||||||
|  |     And I should not find "Topic 1" in the app | ||||||
|  |     And I should not find "Topic 2" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Next:" in the app | ||||||
|  |     Then I should find "Topic 5" in the app | ||||||
|  |     And I should find "Previous: Topic 4" in the app | ||||||
|  |     But I should not find "General" in the app | ||||||
|  |     And I should not find "Topic 1" in the app | ||||||
|  |     And I should not find "Topic 2" in the app | ||||||
|  |     And I should not find "Topic 3" in the app | ||||||
|  |     And I should not find "Next:" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Previous:" in the app | ||||||
|  |     Then I should find "Topic 4" in the app | ||||||
|  |     And I should find "Previous: Topic 3" in the app | ||||||
|  |     And I should find "Next: Topic 5" in the app | ||||||
|  |     But I should not find "General" in the app | ||||||
|  |     And I should not find "Topic 1" in the app | ||||||
|  |     And I should not find "Topic 2" in the app | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: Self enrol | ||||||
|  |     Given 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 "Participants" "link" | ||||||
|  |     And I select "Enrolment methods" from the "jump" singleselect | ||||||
|  |     And I click on "Enable" "icon" in the "Self enrolment (Student)" "table_row" | ||||||
|  |     And I close the browser tab opened by the app | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I press "Site home" in the app | ||||||
|  |     And I press "Available courses" in the app | ||||||
|  |     And I press "Course 1" in the app | ||||||
|  |     And I press "Enrol me" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     And I wait loading to finish in the app | ||||||
|  |     Then the header should be "Course 1" in the app | ||||||
|  |     And I should find "Choice course 1" in the app | ||||||
|  |     And I should find "assignment" in the app | ||||||
|  |     And I should find "Test forum name" in the app | ||||||
|  |     And I should find "Test chat name" in the app | ||||||
|  |     And I should find "Web links" in the app | ||||||
|  |     And I should find "Test external name" in the app | ||||||
|  |     And I should find "Test feedback name" in the app | ||||||
|  |     And I should find "Test glossary" in the app | ||||||
|  |     And I should find "Quiz 1" in the app | ||||||
|  |     And I should find "Test survey name" in the app | ||||||
|  |     And I should find "Test wiki name" in the app | ||||||
|  |     And I should find "Test lesson name" in the app | ||||||
|  |     And I should find "Test scorm name" in the app | ||||||
|  |     And I should find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: Guest access | ||||||
|  |     Given 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 "Participants" "link" | ||||||
|  |     And I select "Enrolment methods" from the "jump" singleselect | ||||||
|  |     And I click on "Enable" "icon" in the "Guest access" "table_row" | ||||||
|  |     And I close the browser tab opened by the app | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I press "Site home" in the app | ||||||
|  |     And I press "Available courses" in the app | ||||||
|  |     And I press "Course 1" in the app | ||||||
|  | 
 | ||||||
|  |     Then I should find "Course summary" in the app | ||||||
|  |     And I should find "Course" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "View course" "ion-button" in the app | ||||||
|  |     Then the header should be "Course 1" in the app | ||||||
|  |     And I should find "Choice course 1" in the app | ||||||
|  |     And I should find "assignment" in the app | ||||||
|  |     And I should find "Test forum name" in the app | ||||||
|  |     And I should find "Test chat name" in the app | ||||||
|  |     And I should find "Web links" in the app | ||||||
|  |     And I should find "Test feedback name" in the app | ||||||
|  |     And I should find "Test glossary" in the app | ||||||
|  |     And I should find "Quiz 1" in the app | ||||||
|  |     And I should find "Test survey name" in the app | ||||||
|  |     And I should find "Test wiki name" in the app | ||||||
|  |     And I should find "Test lesson name" in the app | ||||||
|  |     And I should find "Test scorm name" in the app | ||||||
|  |     And I should find "Test workshop name" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: View blocks on drawer | ||||||
|  |     Given the following "blocks" exist: | ||||||
|  |       | blockname        | contextlevel | reference | pagetypepattern | defaultregion | configdata                                                                                                   | | ||||||
|  |       | html             | Course       | C1        | course-view-*   | site-pre      | Tzo4OiJzdGRDbGFzcyI6Mjp7czo1OiJ0aXRsZSI7czoxNToiSFRNTCB0aXRsZSB0ZXN0IjtzOjQ6InRleHQiO3M6OToiYm9keSB0ZXN0Ijt9 | | ||||||
|  |       | activity_modules | Course       | C1        | course-view-*   | site-pre      |                                                                                                              | | ||||||
|  |     And I entered the course "Course 1" as "student1" in the app | ||||||
|  |     Then the header should be "Course 1" in the app | ||||||
|  |     And I should find "Choice course 1" in the app | ||||||
|  |     And I should find "assignment" in the app | ||||||
|  |     And I should find "Test forum name" in the app | ||||||
|  |     And I should find "Test chat name" in the app | ||||||
|  |     And I should find "Web links" in the app | ||||||
|  |     And I should find "Test external name" in the app | ||||||
|  |     And I should find "Test feedback name" in the app | ||||||
|  |     And I should find "Test glossary" in the app | ||||||
|  |     And I should find "Quiz 1" in the app | ||||||
|  |     And I should find "Test survey name" in the app | ||||||
|  |     And I should find "Test wiki name" in the app | ||||||
|  |     And I should find "Test lesson name" in the app | ||||||
|  |     And I should find "Test scorm name" in the app | ||||||
|  |     And I should find "Test workshop name" in the app | ||||||
|  |     Then I press "Open block drawer" in the app | ||||||
|  |     And I should find "HTML title test" in the app | ||||||
|  |     And I should find "body test" in the app | ||||||
|  |     And I should find "Activities" in the app | ||||||
| @ -0,0 +1,34 @@ | |||||||
|  | @core @core_course @app @javascript @lms_upto3.10 | ||||||
|  | Feature: Check course completion feature. | ||||||
|  |   In order to track the progress of the course on mobile device | ||||||
|  |   As a student | ||||||
|  |   I need to be able to update the activity completion status. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email                | | ||||||
|  |       | student1 | Student   | 1        | student1@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | enablecompletion | | ||||||
|  |       | Course 1 | C1        | 0        | 1                | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  | 
 | ||||||
|  |   Scenario: Activity completion, marking the checkbox manually | ||||||
|  |     Given the following "activities" exist: | ||||||
|  |       | activity | name         | course | idnumber | completion | completionview | | ||||||
|  |       | forum    | First forum  | C1     | forum1   | 1          | 0              | | ||||||
|  |       | forum    | Second forum | C1     | forum2   | 1          | 0              | | ||||||
|  |     And I entered the course "Course 1" as "student1" in the app | ||||||
|  |     # Set activities as completed. | ||||||
|  |     Then I should find "0%" in the app | ||||||
|  |     And I click on "ion-button[title=\"Not completed: First forum. Select to mark as complete.\"]" "css" | ||||||
|  |     And I should find "50%" in the app | ||||||
|  |     And I click on "ion-button[title=\"Not completed: Second forum. Select to mark as complete.\"]" "css" | ||||||
|  |     And I should find "100%" in the app | ||||||
|  |     # Set activities as not completed. | ||||||
|  |     And I click on "ion-button[title=\"Completed: First forum. Select to mark as not complete.\"]" "css" | ||||||
|  |     And I should find "50%" in the app | ||||||
|  |     And I click on "ion-button[title=\"Completed: Second forum. Select to mark as not complete.\"]" "css" | ||||||
|  |     And I should find "0%" in the app | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | @core @core_course @app @javascript | ||||||
|  | Feature: Check course completion feature. | ||||||
|  |   In order to track the progress of the course on mobile device | ||||||
|  |   As a student | ||||||
|  |   I need to be able to update the activity completion status. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email                | | ||||||
|  |       | student1 | Student   | 1        | student1@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | enablecompletion | | ||||||
|  |       | Course 1 | C1        | 0        | 1                | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  | 
 | ||||||
|  |   @lms_from3.11 | ||||||
|  |   Scenario: Activity completion, marking the checkbox manually | ||||||
|  |     Given the following "activities" exist: | ||||||
|  |       | activity | name         | course | idnumber | completion | completionview | | ||||||
|  |       | forum    | First forum  | C1     | forum1   | 1          | 0              | | ||||||
|  |       | forum    | Second forum | C1     | forum2   | 1          | 0              | | ||||||
|  |     And I entered the course "Course 1" as "student1" in the app | ||||||
|  |     # Set activities as completed. | ||||||
|  |     Then I should find "0%" in the app | ||||||
|  |     And I press "Mark First forum as done" in the app | ||||||
|  |     And I should find "50%" in the app | ||||||
|  |     And I press "Mark Second forum as done" in the app | ||||||
|  |     And I should find "100%" in the app | ||||||
|  |     # Set activities as not completed. | ||||||
|  |     And I press "First forum is marked as done. Press to undo." in the app | ||||||
|  |     And I should find "50%" in the app | ||||||
|  |     And I press "Second forum is marked as done. Press to undo." in the app | ||||||
|  |     And I should find "0%" in the app | ||||||
							
								
								
									
										118
									
								
								src/core/features/course/tests/behat/courselist-311.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/core/features/course/tests/behat/courselist-311.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | |||||||
|  | @core @core_course @app @javascript @lms_upto3.11 | ||||||
|  | Feature: Test course list shown on app start tab | ||||||
|  |   In order to select a course | ||||||
|  |   As a student | ||||||
|  |   I need to see the correct list of courses | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |       | Course 2 | C2        | | ||||||
|  |     And the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  |       | student2 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |       | student2 | C1     | student | | ||||||
|  |       | student2 | C2     | student | | ||||||
|  | 
 | ||||||
|  |   Scenario: View courses (shortnames not displayed) | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I should find "Course 1" in the app | ||||||
|  |     But I should not find "Course 2" in the app | ||||||
|  |     But I should not find "C1" in the app | ||||||
|  |     But I should not find "C2" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     But I should not find "C1" in the app | ||||||
|  |     But I should not find "C2" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Filter courses | ||||||
|  |     Given the following config values are set as admin: | ||||||
|  |       | courselistshortnames | 1 | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Frog 3   | C3        | | ||||||
|  |       | Frog 4   | C4        | | ||||||
|  |       | Course 5 | C5        | | ||||||
|  |       | Toad 6   | C6        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student2 | C3     | student | | ||||||
|  |       | student2 | C4     | student | | ||||||
|  |       | student2 | C5     | student | | ||||||
|  |       | student2 | C6     | student | | ||||||
|  |     # Create bogus courses so that the main ones aren't shown in the 'recently accessed' part. | ||||||
|  |     # Because these come later in alphabetical order, they may not be displayed in the lower part | ||||||
|  |     # which is OK. | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Zogus 1  | Z1        | | ||||||
|  |       | Zogus 2  | Z2        | | ||||||
|  |       | Zogus 3  | Z3        | | ||||||
|  |       | Zogus 4  | Z4        | | ||||||
|  |       | Zogus 5  | Z5        | | ||||||
|  |       | Zogus 6  | Z6        | | ||||||
|  |       | Zogus 7  | Z7        | | ||||||
|  |       | Zogus 8  | Z8        | | ||||||
|  |       | Zogus 9  | Z9        | | ||||||
|  |       | Zogus 10 | Z10       | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student2 | Z1     | student | | ||||||
|  |       | student2 | Z2     | student | | ||||||
|  |       | student2 | Z3     | student | | ||||||
|  |       | student2 | Z4     | student | | ||||||
|  |       | student2 | Z5     | student | | ||||||
|  |       | student2 | Z6     | student | | ||||||
|  |       | student2 | Z7     | student | | ||||||
|  |       | student2 | Z8     | student | | ||||||
|  |       | student2 | Z9     | student | | ||||||
|  |       | student2 | Z10    | student | | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I should find "C1" in the app | ||||||
|  |     And I should find "C2" in the app | ||||||
|  |     And I should find "C3" in the app | ||||||
|  |     And I should find "C4" in the app | ||||||
|  |     And I should find "C5" in the app | ||||||
|  |     And I should find "C6" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Frog 3" in the app | ||||||
|  |     And I should find "Frog 4" in the app | ||||||
|  |     And I should find "Course 5" in the app | ||||||
|  |     And I should find "Toad 6" in the app | ||||||
|  | 
 | ||||||
|  |     And I set the field "search text" to "fr" in the app | ||||||
|  | 
 | ||||||
|  |     Then I should find "C3" in the app | ||||||
|  |     And I should find "C4" in the app | ||||||
|  |     And I should find "Frog 3" in the app | ||||||
|  |     And I should find "Frog 4" in the app | ||||||
|  |     But I should not find "C1" in the app | ||||||
|  |     And I should not find "C2" in the app | ||||||
|  |     And I should not find "C5" in the app | ||||||
|  |     And I should not find "C6" in the app | ||||||
|  |     And I should not find "Course 1" in the app | ||||||
|  |     And I should not find "Course 2" in the app | ||||||
|  |     And I should not find "Course 5" in the app | ||||||
|  |     And I should not find "Toad 6" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "search text" to "" in the app | ||||||
|  |     Then I should find "C1" in the app | ||||||
|  |     And I should find "C2" in the app | ||||||
|  |     And I should find "C3" in the app | ||||||
|  |     And I should find "C4" in the app | ||||||
|  |     And I should find "C5" in the app | ||||||
|  |     And I should find "C6" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Frog 3" in the app | ||||||
|  |     And I should find "Frog 4" in the app | ||||||
|  |     And I should find "Course 5" in the app | ||||||
|  |     And I should find "Toad 6" in the app | ||||||
							
								
								
									
										123
									
								
								src/core/features/course/tests/behat/courselist.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/core/features/course/tests/behat/courselist.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,123 @@ | |||||||
|  | @core @core_course @app @javascript | ||||||
|  | Feature: Test course list shown on app start tab | ||||||
|  |   In order to select a course | ||||||
|  |   As a student | ||||||
|  |   I need to see the correct list of courses | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |       | Course 2 | C2        | | ||||||
|  |     And the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  |       | student2 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |       | student2 | C1     | student | | ||||||
|  |       | student2 | C2     | student | | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: View courses (shortnames not displayed) | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "My courses" in the app | ||||||
|  |     Then I should find "Course 1" in the app | ||||||
|  |     But I should not find "Course 2" in the app | ||||||
|  |     But I should not find "C1" in the app | ||||||
|  |     But I should not find "C2" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I press "My courses" in the app | ||||||
|  |     Then I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     But I should not find "C1" in the app | ||||||
|  |     But I should not find "C2" in the app | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: Filter courses | ||||||
|  |     Given the following config values are set as admin: | ||||||
|  |       | courselistshortnames | 1 | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Frog 3   | C3        | | ||||||
|  |       | Frog 4   | C4        | | ||||||
|  |       | Course 5 | C5        | | ||||||
|  |       | Toad 6   | C6        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student2 | C3     | student | | ||||||
|  |       | student2 | C4     | student | | ||||||
|  |       | student2 | C5     | student | | ||||||
|  |       | student2 | C6     | student | | ||||||
|  |     # Create bogus courses so that the main ones aren't shown in the 'recently accessed' part. | ||||||
|  |     # Because these come later in alphabetical order, they may not be displayed in the lower part | ||||||
|  |     # which is OK. | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Zogus 1  | Z1        | | ||||||
|  |       | Zogus 2  | Z2        | | ||||||
|  |       | Zogus 3  | Z3        | | ||||||
|  |       | Zogus 4  | Z4        | | ||||||
|  |       | Zogus 5  | Z5        | | ||||||
|  |       | Zogus 6  | Z6        | | ||||||
|  |       | Zogus 7  | Z7        | | ||||||
|  |       | Zogus 8  | Z8        | | ||||||
|  |       | Zogus 9  | Z9        | | ||||||
|  |       | Zogus 10 | Z10       | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student2 | Z1     | student | | ||||||
|  |       | student2 | Z2     | student | | ||||||
|  |       | student2 | Z3     | student | | ||||||
|  |       | student2 | Z4     | student | | ||||||
|  |       | student2 | Z5     | student | | ||||||
|  |       | student2 | Z6     | student | | ||||||
|  |       | student2 | Z7     | student | | ||||||
|  |       | student2 | Z8     | student | | ||||||
|  |       | student2 | Z9     | student | | ||||||
|  |       | student2 | Z10    | student | | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I press "My courses" in the app | ||||||
|  |     Then I should find "C1" in the app | ||||||
|  |     And I should find "C2" in the app | ||||||
|  |     And I should find "C3" in the app | ||||||
|  |     And I should find "C4" in the app | ||||||
|  |     And I should find "C5" in the app | ||||||
|  |     And I should find "C6" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Frog 3" in the app | ||||||
|  |     And I should find "Frog 4" in the app | ||||||
|  |     And I should find "Course 5" in the app | ||||||
|  |     And I should find "Toad 6" in the app | ||||||
|  | 
 | ||||||
|  |     And I set the field "search text" to "fr" in the app | ||||||
|  | 
 | ||||||
|  |     Then I should find "C3" in the app | ||||||
|  |     And I should find "C4" in the app | ||||||
|  |     And I should find "Frog 3" in the app | ||||||
|  |     And I should find "Frog 4" in the app | ||||||
|  |     But I should not find "C1" in the app | ||||||
|  |     And I should not find "C2" in the app | ||||||
|  |     And I should not find "C5" in the app | ||||||
|  |     And I should not find "C6" in the app | ||||||
|  |     And I should not find "Course 1" in the app | ||||||
|  |     And I should not find "Course 2" in the app | ||||||
|  |     And I should not find "Course 5" in the app | ||||||
|  |     And I should not find "Toad 6" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "search text" to "" in the app | ||||||
|  |     Then I should find "C1" in the app | ||||||
|  |     And I should find "C2" in the app | ||||||
|  |     And I should find "C3" in the app | ||||||
|  |     And I should find "C4" in the app | ||||||
|  |     And I should find "C5" in the app | ||||||
|  |     And I should find "C6" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Frog 3" in the app | ||||||
|  |     And I should find "Frog 4" in the app | ||||||
|  |     And I should find "Course 5" in the app | ||||||
|  |     And I should find "Toad 6" in the app | ||||||
							
								
								
									
										81
									
								
								src/core/features/courses/tests/behat/basic_usage-310.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										81
									
								
								src/core/features/courses/tests/behat/basic_usage-310.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | @core @core_course @app @javascript @lms_upto3.10 | ||||||
|  | Feature: Test basic usage of courses in app | ||||||
|  |   In order to participate in the courses while using the mobile app | ||||||
|  |   As a student | ||||||
|  |   I need basic courses functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email | | ||||||
|  |       | teacher1 | Teacher | teacher | teacher1@example.com | | ||||||
|  |       | student1 | Student | student | student1@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |       | Course 2 | C2 | 0 | | ||||||
|  |       | Course 3 | C3 | 0 | | ||||||
|  |       | Course 4 | C4 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | teacher1 | C2 | editingteacher | | ||||||
|  |       | teacher1 | C3 | editingteacher | | ||||||
|  |       | teacher1 | C4 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  |       | student1 | C2 | student | | ||||||
|  |       | student1 | C3 | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name            | intro                   | course | idnumber | option                       | | ||||||
|  |       | choice   | Choice course 1 | Test choice description | C1     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |       | choice   | Choice course 2 | Test choice description | C2     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |       | choice   | Choice course 3 | Test choice description | C3     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |       | choice   | Choice course 4 | Test choice description | C4     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | course | idnumber | name                | intro                       | assignsubmission_onlinetext_enabled | | ||||||
|  |       | assign   | C1     | assign1  | assignment          | Test assignment description | 1                                   | | ||||||
|  | 
 | ||||||
|  |   Scenario: Links to actions in Timeline work for teachers/students | ||||||
|  |     # Configure assignment as teacher | ||||||
|  |     Given I entered the assign activity "assignment" on course "Course 1" as "teacher1" in the app | ||||||
|  |     When I press "Information" 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 press "Actions menu" | ||||||
|  |     And I follow "Edit settings" | ||||||
|  |     And I press "Expand all" | ||||||
|  |     And I click on "duedate[enabled]" "checkbox" | ||||||
|  |     And I click on "gradingduedate[enabled]" "checkbox" | ||||||
|  |     And I press "Save and return to course" | ||||||
|  |     And I close the browser tab opened by the app | ||||||
|  | 
 | ||||||
|  |     # Submit assignment as student | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Open block drawer" in the app | ||||||
|  |     And I press "Add submission" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  |     And I should find "Test assignment description" in the app | ||||||
|  |     And I should find "No attempt" in the app | ||||||
|  |     And I should find "Due date" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Add submission" in the app | ||||||
|  |     And I set the field "Online text submissions" to "test" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I press "Submit assignment" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  |     And I should find "Test assignment description" in the app | ||||||
|  |     And I should find "Submitted for grading" in the app | ||||||
|  |     And I should find "Due date" in the app | ||||||
|  | 
 | ||||||
|  |     # Grade assignment as teacher | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Open block drawer" in the app | ||||||
|  |     And I press "Grade" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  |     And I should find "Test assignment description" in the app | ||||||
|  |     And I should find "Time remaining" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Needs grading" in the app | ||||||
|  |     Then I should find "Student student" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
							
								
								
									
										126
									
								
								src/core/features/courses/tests/behat/basic_usage-311.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										126
									
								
								src/core/features/courses/tests/behat/basic_usage-311.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,126 @@ | |||||||
|  | @core @core_course @app @javascript @lms_upto3.11 | ||||||
|  | Feature: Test basic usage of courses in app | ||||||
|  |   In order to participate in the courses while using the mobile app | ||||||
|  |   As a student | ||||||
|  |   I need basic courses functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email | | ||||||
|  |       | teacher1 | Teacher | teacher | teacher1@example.com | | ||||||
|  |       | student1 | Student | student | student1@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |       | Course 2 | C2 | 0 | | ||||||
|  |       | Course 3 | C3 | 0 | | ||||||
|  |       | Course 4 | C4 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | teacher1 | C2 | editingteacher | | ||||||
|  |       | teacher1 | C3 | editingteacher | | ||||||
|  |       | teacher1 | C4 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  |       | student1 | C2 | student | | ||||||
|  |       | student1 | C3 | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name            | intro                   | course | idnumber | option                       | | ||||||
|  |       | choice   | Choice course 1 | Test choice description | C1     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |       | choice   | Choice course 2 | Test choice description | C2     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |       | choice   | Choice course 3 | Test choice description | C3     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |       | choice   | Choice course 4 | Test choice description | C4     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | course | idnumber | name                | intro                       | assignsubmission_onlinetext_enabled | | ||||||
|  |       | assign   | C1     | assign1  | assignment          | Test assignment description | 1                                   | | ||||||
|  | 
 | ||||||
|  |   Scenario: "Dashboard" tab displayed | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I should see "Dashboard" | ||||||
|  |     And the header should be "Acceptance test site" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Course 3" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Site home" in the app | ||||||
|  |     Then I should find "Dashboard" in the app | ||||||
|  |     And the header should be "Acceptance test site" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Dashboard" in the app | ||||||
|  |     Then I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Course 3" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: See my courses | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When the header should be "Acceptance test site" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Course 3" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Course 1" in the app | ||||||
|  |     Then I should find "Choice course 1" in the app | ||||||
|  |     And the header should be "Course 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Choice course 1" in the app | ||||||
|  |     Then I should find "Test choice description" in the app | ||||||
|  |     And the header should be "Choice course 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Course 2" in the app | ||||||
|  |     Then I should find "Choice course 2" in the app | ||||||
|  |     And the header should be "Course 2" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course 3" in the app | ||||||
|  |     Then I should find "Choice course 3" in the app | ||||||
|  |     And the header should be "Course 3" in the app | ||||||
|  | 
 | ||||||
|  |   @lms_from3.11 | ||||||
|  |   Scenario: Links to actions in Timeline work for teachers/students | ||||||
|  |     # Configure assignment as teacher | ||||||
|  |     Given I entered the course "Course 1" as "teacher1" in the app | ||||||
|  |     When I press "assignment" in the app | ||||||
|  |     And I press "Information" 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 press "Actions menu" | ||||||
|  |     And I follow "Edit settings" | ||||||
|  |     And I press "Expand all" | ||||||
|  |     And I click on "duedate[enabled]" "checkbox" | ||||||
|  |     And I click on "gradingduedate[enabled]" "checkbox" | ||||||
|  |     And I press "Save and return to course" | ||||||
|  |     And I close the browser tab opened by the app | ||||||
|  | 
 | ||||||
|  |     # Submit assignment as student | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Open block drawer" in the app | ||||||
|  |     And I press "Add submission" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  |     And I should find "Test assignment description" in the app | ||||||
|  |     And I should find "No attempt" in the app | ||||||
|  |     And I should find "Due:" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Add submission" in the app | ||||||
|  |     And I set the field "Online text submissions" to "test" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I press "Submit assignment" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  |     And I should find "Test assignment description" in the app | ||||||
|  |     And I should find "Submitted for grading" in the app | ||||||
|  |     And I should find "Due:" in the app | ||||||
|  | 
 | ||||||
|  |     # Grade assignment as teacher | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Open block drawer" in the app | ||||||
|  |     And I press "Grade" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  |     And I should find "Test assignment description" in the app | ||||||
|  |     And I should find "Time remaining" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Needs grading" in the app | ||||||
|  |     Then I should find "Student student" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
							
								
								
									
										143
									
								
								src/core/features/courses/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										143
									
								
								src/core/features/courses/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,143 @@ | |||||||
|  | @core @core_course @app @javascript | ||||||
|  | Feature: Test basic usage of courses in app | ||||||
|  |   In order to participate in the courses while using the mobile app | ||||||
|  |   As a student | ||||||
|  |   I need basic courses functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | email | | ||||||
|  |       | teacher1 | Teacher | teacher | teacher1@example.com | | ||||||
|  |       | student1 | Student | student | student1@example.com | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | category | | ||||||
|  |       | Course 1 | C1 | 0 | | ||||||
|  |       | Course 2 | C2 | 0 | | ||||||
|  |       | Course 3 | C3 | 0 | | ||||||
|  |       | Course 4 | C4 | 0 | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user | course | role | | ||||||
|  |       | teacher1 | C1 | editingteacher | | ||||||
|  |       | teacher1 | C2 | editingteacher | | ||||||
|  |       | teacher1 | C3 | editingteacher | | ||||||
|  |       | teacher1 | C4 | editingteacher | | ||||||
|  |       | student1 | C1 | student | | ||||||
|  |       | student1 | C2 | student | | ||||||
|  |       | student1 | C3 | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | name            | intro                   | course | idnumber | option                       | | ||||||
|  |       | choice   | Choice course 1 | Test choice description | C1     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |       | choice   | Choice course 2 | Test choice description | C2     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |       | choice   | Choice course 3 | Test choice description | C3     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |       | choice   | Choice course 4 | Test choice description | C4     | choice1  | Option 1, Option 2, Option 3 | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity | course | idnumber | name                | intro                       | assignsubmission_onlinetext_enabled | | ||||||
|  |       | assign   | C1     | assign1  | assignment          | Test assignment description | 1                                   | | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: "Dashboard" tab displayed | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I should see "Dashboard" | ||||||
|  |     And the header should be "Acceptance test site" in the app | ||||||
|  |     And I should see "Timeline" | ||||||
|  |     And I press "Site home" in the app | ||||||
|  |     Then I should find "Dashboard" in the app | ||||||
|  |     And the header should be "Acceptance test site" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "My courses" in the app | ||||||
|  |     Then I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Course 3" in the app | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: See my courses | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When the header should be "Acceptance test site" in the app | ||||||
|  |     And I press "My courses" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Course 3" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Course 1" in the app | ||||||
|  |     Then I should find "Choice course 1" in the app | ||||||
|  |     And the header should be "Course 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Choice course 1" in the app | ||||||
|  |     Then I should find "Test choice description" in the app | ||||||
|  |     And the header should be "Choice course 1" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Course 2" in the app | ||||||
|  |     Then I should find "Choice course 2" in the app | ||||||
|  |     And the header should be "Course 2" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Course 3" in the app | ||||||
|  |     Then I should find "Choice course 3" in the app | ||||||
|  |     And the header should be "Course 3" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Search for a course | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Search courses" in the app | ||||||
|  |     And I set the field "Search" to "Course 4" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     Then I should find "Course 4" in the app | ||||||
|  |     And the header should be "Available courses" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Course 4" in the app | ||||||
|  |     Then I should find "Course 4" in the app | ||||||
|  |     And I should find "Course summary" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I set the field "Search" to "Course" in the app | ||||||
|  |     And I press "Search" "button" in the app | ||||||
|  |     Then I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Course 3" in the app | ||||||
|  |     And I should find "Course 4" in the app | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   # TODO remove LMS UI steps in app tests | ||||||
|  |   Scenario: Links to actions in Timeline work for teachers/students | ||||||
|  |     # Configure assignment as teacher | ||||||
|  |     Given I entered the assign activity "assignment" on course "Course 1" as "teacher1" in the app | ||||||
|  |     When I press "Information" 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 navigate to "Settings" in current page administration | ||||||
|  |     And I click on "Expand all" "link" | ||||||
|  |     And I click on "duedate[enabled]" "checkbox" | ||||||
|  |     And I click on "gradingduedate[enabled]" "checkbox" | ||||||
|  |     And I press "Save and return to course" | ||||||
|  |     And I close the browser tab opened by the app | ||||||
|  | 
 | ||||||
|  |     # Submit assignment as student | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press "Add submission" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  |     And I should find "Test assignment description" in the app | ||||||
|  |     And I should find "No attempt" in the app | ||||||
|  |     And I should find "Due:" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Add submission" in the app | ||||||
|  |     And I set the field "Online text submissions" to "test" in the app | ||||||
|  |     And I press "Save" in the app | ||||||
|  |     And I press "Submit assignment" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  |     And I should find "Test assignment description" in the app | ||||||
|  |     And I should find "Submitted for grading" in the app | ||||||
|  |     And I should find "Due:" in the app | ||||||
|  | 
 | ||||||
|  |     # Grade assignment as teacher | ||||||
|  |     Given I entered the app as "teacher1" | ||||||
|  |     When I press "Grade" in the app | ||||||
|  |     Then the header should be "assignment" in the app | ||||||
|  |     And I should find "Test assignment description" in the app | ||||||
|  |     And I should find "Time remaining" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Needs grading" in the app | ||||||
|  |     Then I should find "Student student" in the app | ||||||
|  |     And I should find "Not graded" in the app | ||||||
							
								
								
									
										89
									
								
								src/core/features/login/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										89
									
								
								src/core/features/login/tests/behat/basic_usage.feature
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,89 @@ | |||||||
|  | @auth @core_auth @app @javascript | ||||||
|  | Feature: Test basic usage of login in app | ||||||
|  |   I need basic login functionality to work | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "users" exist: | ||||||
|  |       | username | firstname | lastname | | ||||||
|  |       | student1 | david     | student  | | ||||||
|  |       | student2 | pau       | student2 | | ||||||
|  |       | teacher1 | juan      | teacher  | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role           | | ||||||
|  |       | student1 | C1     | student        | | ||||||
|  |       | student2 | C1     | student        | | ||||||
|  |       | teacher1 | C1     | editingteacher | | ||||||
|  | 
 | ||||||
|  |   Scenario: Skip on boarding | ||||||
|  |     When I launch the app runtime | ||||||
|  |     Then I should find "Welcome to the Moodle App!" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Skip" in the app | ||||||
|  |     Then I should not find "Skip" in the app | ||||||
|  |     And I should find "Connect to Moodle" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Add a new account in the app & Site name in displayed when adding a new account | ||||||
|  |     When I enter the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I set the field "Your site" to "$WWWROOT" in the app | ||||||
|  |     And I press "Connect to your site" in the app | ||||||
|  |     Then I should find "Acceptance test site" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "Username" to "student1" in the app | ||||||
|  |     And I set the field "Password" to "student1" in the app | ||||||
|  |     And I press "Log in" near "Forgotten your username or password?" in the app | ||||||
|  |     Then I should find "Acceptance test site" in the app | ||||||
|  |     But I should not find "Log in" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Add a non existing account | ||||||
|  |     When I enter the app | ||||||
|  |     And I log in as "student1" | ||||||
|  |     And I press the user menu button in the app | ||||||
|  |     And I press "Log out" in the app | ||||||
|  |     And I wait the app to restart | ||||||
|  |     And I press "Add" in the app | ||||||
|  |     And I set the field "Your site" to "Wrong Site Address" in the app | ||||||
|  |     And I press enter in the app | ||||||
|  |     Then I should find "Cannot connect" in the app | ||||||
|  |     And I should find "Wrong Site Address" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Add a non existing account from accounts switcher | ||||||
|  |     When I enter the app | ||||||
|  |     And I log in as "student1" | ||||||
|  |     And I press the user menu button in the app | ||||||
|  |     And I press "Switch account" in the app | ||||||
|  |     And I press "Add" in the app | ||||||
|  |     And I wait the app to restart | ||||||
|  |     And I set the field "Your site" to "Wrong Site Address" in the app | ||||||
|  |     And I press enter in the app | ||||||
|  |     Then I should find "Cannot connect" in the app | ||||||
|  |     And I should find "Wrong Site Address" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Delete an account | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I press the user menu button in the app | ||||||
|  |     And I press "Log out" in the app | ||||||
|  |     And I wait the app to restart | ||||||
|  |     Then I should find "Acceptance test site" in the app | ||||||
|  |     And I press "Edit accounts list" in the app | ||||||
|  |     And I press "Remove account" near "Acceptance test site" in the app | ||||||
|  |     And I press "Delete" near "Are you sure you want to remove the account on Acceptance test site?" in the app | ||||||
|  |     Then I should find "Connect to Moodle" in the app | ||||||
|  |     But I should not find "Acceptance test site" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Require minium version of the app for a site | ||||||
|  | 
 | ||||||
|  |     # Log in with a previous required version | ||||||
|  |     Given the following config values are set as admin: | ||||||
|  |       | minimumversion | 3.8.1 | tool_mobile | | ||||||
|  |     When I enter the app | ||||||
|  |     Then I should not find "App update required" in the app | ||||||
|  | 
 | ||||||
|  |     # Log in with a future required version | ||||||
|  |     Given the following config values are set as admin: | ||||||
|  |       | minimumversion | 11.0.0 | tool_mobile | | ||||||
|  |     When I enter the app | ||||||
|  |     Then I should find "App update required" in the app | ||||||
							
								
								
									
										23
									
								
								src/core/features/mainmenu/tests/behat/mainmenu-311.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/core/features/mainmenu/tests/behat/mainmenu-311.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | @app @javascript @lms_upto3.11 | ||||||
|  | Feature: Main Menu opens the right page | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student  | | ||||||
|  | 
 | ||||||
|  |   Scenario: Opens Site Home when defaulthomepage is set to Site | ||||||
|  |     Given the following config values are set as admin: | ||||||
|  |       | defaulthomepage | 0 | | ||||||
|  |     Given I entered the app as "student" | ||||||
|  |     When "Site home" should be selected in the app | ||||||
|  |     And I should find "Available courses" in the app | ||||||
|  |     And "Site home" "text" should appear before "Dashboard" "text" in the ".core-tabs-bar" "css_element" | ||||||
|  | 
 | ||||||
|  |   Scenario: Opens Dashboard when defaulthomepage is set to Dashboard | ||||||
|  |     Given the following config values are set as admin: | ||||||
|  |       | defaulthomepage | 1 | | ||||||
|  |     Given I entered the app as "student" | ||||||
|  |     When "Dashboard" should be selected in the app | ||||||
|  |     And I should find "Course overview" in the app | ||||||
|  |     And "Dashboard" "text" should appear before "Site home" "text" in the ".core-tabs-bar" "css_element" | ||||||
							
								
								
									
										42
									
								
								src/core/features/mainmenu/tests/behat/mainmenu.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/core/features/mainmenu/tests/behat/mainmenu.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | @app @javascript | ||||||
|  | Feature: Main Menu opens the right page | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student  | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user    | course | role    | | ||||||
|  |       | student | C1     | student | | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: Opens Site Home when defaulthomepage is set to Site | ||||||
|  |     Given the following config values are set as admin: | ||||||
|  |       | defaulthomepage | 0 | | ||||||
|  |     Given I entered the app as "student" | ||||||
|  |     When "Site home" should be selected in the app | ||||||
|  |     And I should find "Available courses" in the app | ||||||
|  |     And "Site home" "text" should appear before "Dashboard" "text" in the ".core-tabs-bar" "css_element" | ||||||
|  |     And "Home" "text" should appear before "My courses" "text" in the ".mainmenu-tabs" "css_element" | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: Opens Dashboard when defaulthomepage is set to Dashboard | ||||||
|  |     Given the following config values are set as admin: | ||||||
|  |       | defaulthomepage | 1 | | ||||||
|  |     Given I entered the app as "student" | ||||||
|  |     When "Dashboard" should be selected in the app | ||||||
|  |     And I should find "Timeline" in the app | ||||||
|  |     And "Dashboard" "text" should appear before "Site home" "text" in the ".core-tabs-bar" "css_element" | ||||||
|  |     And "Home" "text" should appear before "My courses" "text" in the ".mainmenu-tabs" "css_element" | ||||||
|  | 
 | ||||||
|  |   @lms_from4.0 | ||||||
|  |   Scenario: Opens My Courses when defaulthomepage is set to My Courses | ||||||
|  |     Given the following config values are set as admin: | ||||||
|  |       | defaulthomepage | 3 | | ||||||
|  |     Given I entered the app as "student" | ||||||
|  |     When "My courses" near "Home" should be selected in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And "My courses" "text" should appear before "Home" "text" in the ".mainmenu-tabs" "css_element" | ||||||
| @ -0,0 +1,80 @@ | |||||||
|  | @app @javascript | ||||||
|  | Feature: It navigates properly within settings. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  | 
 | ||||||
|  |   Scenario: Mobile navigation on settings | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  | 
 | ||||||
|  |     # Settings | ||||||
|  |     When I press "More" in the app | ||||||
|  |     And I press "App settings" in the app | ||||||
|  |     Then I should find "General" in the app | ||||||
|  |     And I should find "Space usage" in the app | ||||||
|  |     And I should find "Synchronisation" in the app | ||||||
|  |     And I should find "About" in the app | ||||||
|  | 
 | ||||||
|  |     # Settings details | ||||||
|  |     When I press "General" in the app | ||||||
|  |     Then I should find "Language" in the app | ||||||
|  |     And I should find "Text size" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "About" in the app | ||||||
|  |     Then I should find "Moodle Mobile" in the app | ||||||
|  |     And I should find "Privacy policy" in the app | ||||||
|  | 
 | ||||||
|  |     # Preferences | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press the user menu button in the app | ||||||
|  |     And I press "Preferences" in the app | ||||||
|  |     Then I should find "Messages" in the app | ||||||
|  |     And I should find "Notifications" in the app | ||||||
|  |     And I should find "Manage downloads" in the app | ||||||
|  | 
 | ||||||
|  |     # Preferences details | ||||||
|  |     When I press "Messages" in the app | ||||||
|  |     Then I should find "Accept messages from" in the app | ||||||
|  |     And I should find "Notification preferences" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Manage downloads" in the app | ||||||
|  |     Then I should find "Total space used" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Tablet navigation on settings | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     And I change viewport size to "1200x640" | ||||||
|  | 
 | ||||||
|  |     # Settings | ||||||
|  |     When I press "More" in the app | ||||||
|  |     And I press "App settings" in the app | ||||||
|  |     Then I should find "General" in the app | ||||||
|  |     And I should find "Space usage" in the app | ||||||
|  |     And I should find "Synchronisation" in the app | ||||||
|  |     And I should find "About" in the app | ||||||
|  |     And "General" should be selected in the app | ||||||
|  |     And I should find "Language" in the app | ||||||
|  |     And I should find "Text size" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "About" in the app | ||||||
|  |     Then "About" should be selected in the app | ||||||
|  |     And I should find "Moodle Mobile" in the app | ||||||
|  |     And I should find "Privacy policy" in the app | ||||||
|  | 
 | ||||||
|  |     # Preferences | ||||||
|  |     When I press the user menu button in the app | ||||||
|  |     And I press "Preferences" in the app | ||||||
|  |     Then I should find "Messages" in the app | ||||||
|  |     And I should find "Notifications" in the app | ||||||
|  |     And I should find "Manage downloads" in the app | ||||||
|  |     And "Messages" should be selected in the app | ||||||
|  |     And I should find "Accept messages from" in the app | ||||||
|  |     And I should find "Notification preferences" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Manage downloads" in the app | ||||||
|  |     Then "Manage downloads" should be selected in the app | ||||||
|  |     And I should find "Total space used" in the app | ||||||
							
								
								
									
										15
									
								
								src/core/features/siteplugins/tests/behat/plugins.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/core/features/siteplugins/tests/behat/plugins.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | @app @javascript | ||||||
|  | Feature: Plugins work properly. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username         | | ||||||
|  |       | studentusername  | | ||||||
|  | 
 | ||||||
|  |   Scenario: See more menu button | ||||||
|  |     Given I entered the app as "studentusername" | ||||||
|  |     When I press the more menu button in the app | ||||||
|  |     Then I should find "Moodle App Behat (auto-generated)" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Moodle App Behat (auto-generated)" in the app | ||||||
|  |     Then I should find "studentusername" in the app | ||||||
							
								
								
									
										47
									
								
								src/core/features/usertours/tests/behat/usertours.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/core/features/usertours/tests/behat/usertours.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | @app @javascript | ||||||
|  | Feature: User Tours work properly. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | | ||||||
|  |       | student1 | Student   | First    | | ||||||
|  |       | student2 | Student   | Second   | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |       | student2 | C1     | student | | ||||||
|  |     And the app has the following config: | ||||||
|  |       | disableUserTours | false | | ||||||
|  | 
 | ||||||
|  |   Scenario: Acknowledge User Tours | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     When I should find "Explore your personal area" in the app | ||||||
|  |     But I should not find "Expand to explore" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Got it" in the app | ||||||
|  |     Then I should find "Expand to explore" in the app | ||||||
|  |     But I should not find "Explore your personal area" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Got it" in the app | ||||||
|  |     Then I should not find "Expand to explore" in the app | ||||||
|  |     And I should not find "Explore your personal area" in the app | ||||||
|  | 
 | ||||||
|  |     Given I entered the course "Course 1" in the app | ||||||
|  |     Then I should find "Find your way around" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Got it" in the app | ||||||
|  |     Then I should not find "Find your way around" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Student First" in the app | ||||||
|  |     Then I should find "Swipe left and right to navigate around" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Got it" in the app | ||||||
|  |     Then I should not find "Swipe left and right to navigate around" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     And I press "Student First" in the app | ||||||
|  |     Then I should not find "Swipe left and right to navigate around" in the app | ||||||
| @ -557,14 +557,14 @@ export class CoreSitesProvider { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Add site to sites list.
 |             // Add site to sites list.
 | ||||||
|             this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId); |             await this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId); | ||||||
|             this.sites[siteId] = candidateSite; |             this.sites[siteId] = candidateSite; | ||||||
| 
 | 
 | ||||||
|             if (login) { |             if (login) { | ||||||
|                 // Turn candidate site into current site.
 |                 // Turn candidate site into current site.
 | ||||||
|                 this.currentSite = candidateSite; |                 this.currentSite = candidateSite; | ||||||
|                 // Store session.
 |                 // Store session.
 | ||||||
|                 this.login(siteId); |                 await this.login(siteId); | ||||||
|             } else if (this.currentSite && this.currentSite.getId() == siteId) { |             } else if (this.currentSite && this.currentSite.getId() == siteId) { | ||||||
|                 // Current site has just been updated, trigger the event.
 |                 // Current site has just been updated, trigger the event.
 | ||||||
|                 CoreEvents.trigger(CoreEvents.SITE_UPDATED, info, siteId); |                 CoreEvents.trigger(CoreEvents.SITE_UPDATED, info, siteId); | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								src/testing/behat-testing.module.prod.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/testing/behat-testing.module.prod.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | // (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 { NgModule } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Stub used in production to avoid including testing code in production bundles. | ||||||
|  |  */ | ||||||
|  | @NgModule({}) | ||||||
|  | export class BehatTestingModule {} | ||||||
							
								
								
									
										34
									
								
								src/testing/behat-testing.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/testing/behat-testing.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | // (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 {} | ||||||
							
								
								
									
										242
									
								
								src/testing/services/behat-blocking.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								src/testing/services/behat-blocking.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,242 @@ | |||||||
|  | // (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 { Injectable } from '@angular/core'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { makeSingleton, NgZone } from '@singletons'; | ||||||
|  | import { BehatTestsWindow, TestsBehatRuntime } from './behat-runtime'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Behat block JS manager. | ||||||
|  |  */ | ||||||
|  | @Injectable({ providedIn: 'root' }) | ||||||
|  | export class TestsBehatBlockingService { | ||||||
|  | 
 | ||||||
|  |     protected waitingBlocked = false; | ||||||
|  |     protected recentMutation = false; | ||||||
|  |     protected lastMutation = 0; | ||||||
|  |     protected initialized = false; | ||||||
|  |     protected keyIndex = 0; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Listen to mutations and override XML Requests. | ||||||
|  |      */ | ||||||
|  |     init(): void { | ||||||
|  |         if (this.initialized) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.initialized = true; | ||||||
|  |         this.listenToMutations(); | ||||||
|  |         this.xmlRequestOverride(); | ||||||
|  | 
 | ||||||
|  |         const win = window as BehatTestsWindow; | ||||||
|  | 
 | ||||||
|  |         // Set up the M object - only pending_js is implemented.
 | ||||||
|  |         win.M = win.M ?? {}; | ||||||
|  |         win.M.util = win.M.util ?? {}; | ||||||
|  |         win.M.util.pending_js = win.M.util.pending_js ?? []; | ||||||
|  | 
 | ||||||
|  |         TestsBehatRuntime.log('Initialized!'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get pending list on window M object. | ||||||
|  |      */ | ||||||
|  |     protected get pendingList(): string[] { | ||||||
|  |         const win = window as BehatTestsWindow; | ||||||
|  | 
 | ||||||
|  |         return win.M?.util?.pending_js || []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Set pending list on window M object. | ||||||
|  |      */ | ||||||
|  |     protected set pendingList(values: string[]) { | ||||||
|  |         const win = window as BehatTestsWindow; | ||||||
|  | 
 | ||||||
|  |         if (!win.M?.util?.pending_js) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         win.M.util.pending_js = values; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Adds a pending key to the array. | ||||||
|  |      * | ||||||
|  |      * @param key Key to add. It will be generated if none. | ||||||
|  |      * @return Key name. | ||||||
|  |      */ | ||||||
|  |     block(key = ''): string { | ||||||
|  |         // Add a special DELAY entry whenever another entry is added.
 | ||||||
|  |         if (this.pendingList.length === 0) { | ||||||
|  |             this.pendingList.push('DELAY'); | ||||||
|  |         } | ||||||
|  |         if (!key) { | ||||||
|  |             key = 'generated-' + this.keyIndex; | ||||||
|  |             this.keyIndex++; | ||||||
|  |         } | ||||||
|  |         this.pendingList.push(key); | ||||||
|  | 
 | ||||||
|  |         TestsBehatRuntime.log('PENDING+: ' + this.pendingList); | ||||||
|  | 
 | ||||||
|  |         return key; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Removes a pending key from the array. If this would clear the array, the actual clear only | ||||||
|  |      * takes effect after the queued events are finished. | ||||||
|  |      * | ||||||
|  |      * @param key Key to remove | ||||||
|  |      */ | ||||||
|  |     async unblock(key: string): Promise<void> { | ||||||
|  |         // Remove the key immediately.
 | ||||||
|  |         this.pendingList = this.pendingList.filter((x) => x !== key); | ||||||
|  | 
 | ||||||
|  |         TestsBehatRuntime.log('PENDING-: ' + this.pendingList); | ||||||
|  | 
 | ||||||
|  |         // If the only thing left is DELAY, then remove that as well, later...
 | ||||||
|  |         if (this.pendingList.length === 1) { | ||||||
|  |             if (!document.hidden) { | ||||||
|  |                 // When tab is not active, ticks should be slower and may do Behat to fail.
 | ||||||
|  |                 // From Timers API:
 | ||||||
|  |                 // https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers
 | ||||||
|  |                 // "This API does not guarantee that timers will run exactly on schedule.
 | ||||||
|  |                 // Delays due to CPU load, other tasks, etc, are to be expected."
 | ||||||
|  |                 await CoreUtils.nextTicks(10); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Check there isn't a spinner...
 | ||||||
|  |             await this.checkUIBlocked(); | ||||||
|  | 
 | ||||||
|  |             // 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); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Adds a pending key to the array, but removes it after some ticks. | ||||||
|  |      */ | ||||||
|  |     async delay(): Promise<void> { | ||||||
|  |         const key = this.block('forced-delay'); | ||||||
|  |         this.unblock(key); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * It would be really beautiful if you could detect CSS transitions and animations, that would | ||||||
|  |      * cover almost everything, but sadly there is no way to do this because the transitionstart | ||||||
|  |      * and animationcancel events are not implemented in Chrome, so we cannot detect either of | ||||||
|  |      * these reliably. Instead, we have to look for any DOM changes and do horrible polling. Most | ||||||
|  |      * of the animations are set to 500ms so we allow it to continue from 500ms after any DOM | ||||||
|  |      * change. | ||||||
|  |      */ | ||||||
|  |     protected listenToMutations(): void { | ||||||
|  |         // Set listener using the mutation callback.
 | ||||||
|  |         const observer = new MutationObserver(() => { | ||||||
|  |             this.lastMutation = Date.now(); | ||||||
|  | 
 | ||||||
|  |             if (!this.recentMutation) { | ||||||
|  |                 this.recentMutation = true; | ||||||
|  |                 this.block('dom-mutation'); | ||||||
|  | 
 | ||||||
|  |                 setTimeout(() => { | ||||||
|  |                     this.pollRecentMutation(); | ||||||
|  |                 }, 500); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Also update the spinner presence if needed.
 | ||||||
|  |             this.checkUIBlocked(); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         observer.observe(document, { attributes: true, childList: true, subtree: true }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called from the mutation callback to remove the pending tag after 500ms if nothing else | ||||||
|  |      * gets mutated. | ||||||
|  |      * | ||||||
|  |      * This will be called after 500ms, then every 100ms until there have been no mutation events | ||||||
|  |      * for 500ms. | ||||||
|  |      */ | ||||||
|  |     protected pollRecentMutation(): void { | ||||||
|  |         if (Date.now() - this.lastMutation > 500) { | ||||||
|  |             this.recentMutation = false; | ||||||
|  |             this.unblock('dom-mutation'); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         setTimeout(() => { | ||||||
|  |             this.pollRecentMutation(); | ||||||
|  |         }, 100); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Checks if a loading spinner is present and visible; if so, adds it to the pending array | ||||||
|  |      * (and if not, removes it). | ||||||
|  |      */ | ||||||
|  |     protected async checkUIBlocked(): Promise<void> { | ||||||
|  |         await CoreUtils.nextTick(); | ||||||
|  |         const blocked = document.querySelector<HTMLElement>('div.core-loading-container, ion-loading, .click-block-active'); | ||||||
|  | 
 | ||||||
|  |         if (blocked?.offsetParent) { | ||||||
|  |             if (!this.waitingBlocked) { | ||||||
|  |                 this.block('blocked'); | ||||||
|  |                 this.waitingBlocked = true; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             if (this.waitingBlocked) { | ||||||
|  |                 this.unblock('blocked'); | ||||||
|  |                 this.waitingBlocked = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Override XMLHttpRequest to mark things pending while there is a request waiting. | ||||||
|  |      */ | ||||||
|  |     protected xmlRequestOverride(): void { | ||||||
|  |         const realOpen = XMLHttpRequest.prototype.open; | ||||||
|  |         let requestIndex = 0; | ||||||
|  | 
 | ||||||
|  |         XMLHttpRequest.prototype.open = function(...args) { | ||||||
|  |             NgZone.run(() => { | ||||||
|  |                 const index = requestIndex++; | ||||||
|  |                 const key = 'httprequest-' + index; | ||||||
|  | 
 | ||||||
|  |                 try { | ||||||
|  |                 // Add to the list of pending requests.
 | ||||||
|  |                     TestsBehatBlocking.block(key); | ||||||
|  | 
 | ||||||
|  |                     // Detect when it finishes and remove it from the list.
 | ||||||
|  |                     this.addEventListener('loadend', () => { | ||||||
|  |                         TestsBehatBlocking.unblock(key); | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |                     return realOpen.apply(this, args); | ||||||
|  |                 } catch (error) { | ||||||
|  |                     TestsBehatBlocking.unblock(key); | ||||||
|  |                     throw error; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const TestsBehatBlocking = makeSingleton(TestsBehatBlockingService); | ||||||
							
								
								
									
										540
									
								
								src/testing/services/behat-dom.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										540
									
								
								src/testing/services/behat-dom.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,540 @@ | |||||||
|  | // (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 { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { NgZone } from '@singletons'; | ||||||
|  | import { TestsBehatBlocking } from './behat-blocking'; | ||||||
|  | import { TestBehatElementLocator } from './behat-runtime'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Behat Dom Utils helper functions. | ||||||
|  |  */ | ||||||
|  | export class TestsBehatDomUtils { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if an element is visible. | ||||||
|  |      * | ||||||
|  |      * @param element Element. | ||||||
|  |      * @param container Container. | ||||||
|  |      * @return Whether the element is visible or not. | ||||||
|  |      */ | ||||||
|  |     static isElementVisible(element: HTMLElement, container: HTMLElement): boolean { | ||||||
|  |         if (element.getAttribute('aria-hidden') === 'true' || getComputedStyle(element).display === 'none') { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const parentElement = this.getParentElement(element); | ||||||
|  |         if (parentElement === container) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!parentElement) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return this.isElementVisible(parentElement, container); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if an element is selected. | ||||||
|  |      * | ||||||
|  |      * @param element Element. | ||||||
|  |      * @param container Container. | ||||||
|  |      * @return Whether the element is selected or not. | ||||||
|  |      */ | ||||||
|  |     static isElementSelected(element: HTMLElement, container: HTMLElement): boolean { | ||||||
|  |         const ariaCurrent = element.getAttribute('aria-current'); | ||||||
|  |         if ( | ||||||
|  |             (ariaCurrent && ariaCurrent !== 'false') || | ||||||
|  |             (element.getAttribute('aria-selected') === 'true') || | ||||||
|  |             (element.getAttribute('aria-checked') === 'true') | ||||||
|  |         ) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const parentElement = this.getParentElement(element); | ||||||
|  |         if (!parentElement || parentElement === container) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return this.isElementSelected(parentElement, container); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Finds elements within a given container with exact info. | ||||||
|  |      * | ||||||
|  |      * @param container Parent element to search the element within | ||||||
|  |      * @param text Text to look for | ||||||
|  |      * @return Elements containing the given text with exact boolean. | ||||||
|  |      */ | ||||||
|  |     protected static findElementsBasedOnTextWithinWithExact(container: HTMLElement, text: string): ElementsWithExact[] { | ||||||
|  |         const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"]`; | ||||||
|  | 
 | ||||||
|  |         const elements = Array.from(container.querySelectorAll<HTMLElement>(attributesSelector)) | ||||||
|  |             .filter((element => this.isElementVisible(element, container))) | ||||||
|  |             .map((element) => { | ||||||
|  |                 const exact = this.checkElementLabel(element, text); | ||||||
|  | 
 | ||||||
|  |                 return { element, exact }; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         const treeWalker = document.createTreeWalker( | ||||||
|  |             container, | ||||||
|  |             NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_DOCUMENT_FRAGMENT | NodeFilter.SHOW_TEXT,  // eslint-disable-line no-bitwise
 | ||||||
|  |             { | ||||||
|  |                 acceptNode: node => { | ||||||
|  |                     if (node instanceof HTMLStyleElement || | ||||||
|  |                         node instanceof HTMLLinkElement || | ||||||
|  |                         node instanceof HTMLScriptElement) { | ||||||
|  |                         return NodeFilter.FILTER_REJECT; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     if (node instanceof HTMLElement && | ||||||
|  |                         (node.getAttribute('aria-hidden') === 'true' || getComputedStyle(node).display === 'none')) { | ||||||
|  |                         return NodeFilter.FILTER_REJECT; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     return NodeFilter.FILTER_ACCEPT; | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         let currentNode: Node | null = null; | ||||||
|  |         // eslint-disable-next-line no-cond-assign
 | ||||||
|  |         while (currentNode = treeWalker.nextNode()) { | ||||||
|  |             if (currentNode instanceof Text) { | ||||||
|  |                 if (currentNode.textContent?.includes(text) && currentNode.parentElement) { | ||||||
|  |                     elements.push({ | ||||||
|  |                         element: currentNode.parentElement, | ||||||
|  |                         exact: currentNode.textContent.trim() === text, | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (currentNode instanceof HTMLElement) { | ||||||
|  |                 const labelledBy = currentNode.getAttribute('aria-labelledby'); | ||||||
|  |                 const labelElement = labelledBy && container.querySelector<HTMLElement>(`#${labelledBy}`); | ||||||
|  |                 if (labelElement && labelElement.innerText && labelElement.innerText.includes(text)) { | ||||||
|  |                     elements.push({ | ||||||
|  |                         element: currentNode, | ||||||
|  |                         exact: labelElement.innerText.trim() == text, | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (currentNode instanceof Element && currentNode.shadowRoot) { | ||||||
|  |                 for (const childNode of Array.from(currentNode.shadowRoot.childNodes)) { | ||||||
|  |                     if (!(childNode instanceof HTMLElement) || ( | ||||||
|  |                         childNode instanceof HTMLStyleElement || | ||||||
|  |                         childNode instanceof HTMLLinkElement || | ||||||
|  |                         childNode instanceof HTMLScriptElement)) { | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     if (childNode.matches(attributesSelector)) { | ||||||
|  |                         elements.push({ | ||||||
|  |                             element: childNode, | ||||||
|  |                             exact: this.checkElementLabel(childNode, text), | ||||||
|  |                         }); | ||||||
|  | 
 | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     elements.push(...this.findElementsBasedOnTextWithinWithExact(childNode, text)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return elements; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Checks an element has exactly the same label (title, alt or aria-label). | ||||||
|  |      * | ||||||
|  |      * @param element Element to check. | ||||||
|  |      * @param text Text to check. | ||||||
|  |      * @return If text matches any of the label attributes. | ||||||
|  |      */ | ||||||
|  |     protected static checkElementLabel(element: HTMLElement, text: string): boolean { | ||||||
|  |         return element.title === text || | ||||||
|  |             element.getAttribute('alt') === text || | ||||||
|  |             element.getAttribute('aria-label') === text; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Finds elements within a given container. | ||||||
|  |      * | ||||||
|  |      * @param container Parent element to search the element within. | ||||||
|  |      * @param text Text to look for. | ||||||
|  |      * @return Elements containing the given text. | ||||||
|  |      */ | ||||||
|  |     protected static findElementsBasedOnTextWithin(container: HTMLElement, text: string): HTMLElement[] { | ||||||
|  |         const elements = this.findElementsBasedOnTextWithinWithExact(container, text); | ||||||
|  | 
 | ||||||
|  |         // Give more relevance to exact matches.
 | ||||||
|  |         elements.sort((a, b) => Number(b.exact) - Number(a.exact)); | ||||||
|  | 
 | ||||||
|  |         return elements.map(element => element.element); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Given a list of elements, get the top ancestors among all of them. | ||||||
|  |      * | ||||||
|  |      * This will remote duplicates and drop any elements nested within each other. | ||||||
|  |      * | ||||||
|  |      * @param elements Elements list. | ||||||
|  |      * @return Top ancestors. | ||||||
|  |      */ | ||||||
|  |     protected static getTopAncestors(elements: HTMLElement[]): HTMLElement[] { | ||||||
|  |         const uniqueElements = new Set(elements); | ||||||
|  | 
 | ||||||
|  |         for (const element of uniqueElements) { | ||||||
|  |             for (const otherElement of uniqueElements) { | ||||||
|  |                 if (otherElement === element) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (element.contains(otherElement)) { | ||||||
|  |                     uniqueElements.delete(otherElement); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return Array.from(uniqueElements); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get parent element, including Shadow DOM parents. | ||||||
|  |      * | ||||||
|  |      * @param element Element. | ||||||
|  |      * @return Parent element. | ||||||
|  |      */ | ||||||
|  |     protected static getParentElement(element: HTMLElement): HTMLElement | null { | ||||||
|  |         return element.parentElement || | ||||||
|  |             (element.getRootNode() && (element.getRootNode() as ShadowRoot).host as HTMLElement) || | ||||||
|  |             null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get closest element matching a selector, without traversing up a given container. | ||||||
|  |      * | ||||||
|  |      * @param element Element. | ||||||
|  |      * @param selector Selector. | ||||||
|  |      * @param container Topmost container to search within. | ||||||
|  |      * @return Closest matching element. | ||||||
|  |      */ | ||||||
|  |     protected static getClosestMatching(element: HTMLElement, selector: string, container: HTMLElement | null): HTMLElement | null { | ||||||
|  |         if (element.matches(selector)) { | ||||||
|  |             return element; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (element === container || !element.parentElement) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return this.getClosestMatching(element.parentElement, selector, container); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Function to find top container element. | ||||||
|  |      * | ||||||
|  |      * @param containerName Whether to search inside the a container name. | ||||||
|  |      * @return Found top container element. | ||||||
|  |      */ | ||||||
|  |     protected static getCurrentTopContainerElement(containerName: string): HTMLElement | null { | ||||||
|  |         let topContainer: HTMLElement | null = null; | ||||||
|  |         let containers: HTMLElement[] = []; | ||||||
|  |         const nonImplementedSelectors = | ||||||
|  |             'ion-alert, ion-popover, ion-action-sheet, ion-modal, core-user-tours-user-tour.is-active, page-core-mainmenu, ion-app'; | ||||||
|  | 
 | ||||||
|  |         switch (containerName) { | ||||||
|  |             case 'html': | ||||||
|  |                 containers = Array.from(document.querySelectorAll<HTMLElement>('html')); | ||||||
|  |                 break; | ||||||
|  |             case 'toast': | ||||||
|  |                 containers = Array.from(document.querySelectorAll('ion-app ion-toast.hydrated')); | ||||||
|  |                 containers = containers.map(container => container?.shadowRoot?.querySelector('.toast-container') || container); | ||||||
|  |                 break; | ||||||
|  |             case 'alert': | ||||||
|  |                 containers = Array.from(document.querySelectorAll('ion-app ion-alert.hydrated')); | ||||||
|  |                 break; | ||||||
|  |             case 'action-sheet': | ||||||
|  |                 containers = Array.from(document.querySelectorAll('ion-app ion-action-sheet.hydrated')); | ||||||
|  |                 break; | ||||||
|  |             case 'modal': | ||||||
|  |                 containers = Array.from(document.querySelectorAll('ion-app ion-modal.hydrated')); | ||||||
|  |                 break; | ||||||
|  |             case 'popover': | ||||||
|  |                 containers = Array.from(document.querySelectorAll('ion-app ion-popover.hydrated')); | ||||||
|  |                 break; | ||||||
|  |             case 'user-tour': | ||||||
|  |                 containers = Array.from(document.querySelectorAll('core-user-tours-user-tour.is-active')); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 // Other containerName or not implemented.
 | ||||||
|  |                 containers = Array.from(document.querySelectorAll<HTMLElement>(nonImplementedSelectors)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (containers.length > 0) { | ||||||
|  |             // Get the one with more zIndex.
 | ||||||
|  |             topContainer = | ||||||
|  |                 containers.reduce((a, b) => getComputedStyle(a).zIndex > getComputedStyle(b).zIndex ? a : b, containers[0]); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!topContainer) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (containerName == 'page' || containerName == 'split-view content') { | ||||||
|  |             // Find non hidden pages inside the container.
 | ||||||
|  |             let pageContainers = Array.from(topContainer.querySelectorAll<HTMLElement>('.ion-page:not(.ion-page-hidden)')); | ||||||
|  |             pageContainers = pageContainers.filter((page) => !page.closest('.ion-page.ion-page-hidden')); | ||||||
|  | 
 | ||||||
|  |             if (pageContainers.length > 0) { | ||||||
|  |                 // Get the more general one to avoid failing.
 | ||||||
|  |                 topContainer = pageContainers[0]; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (containerName == 'split-view content') { | ||||||
|  |                 topContainer = topContainer.querySelector<HTMLElement>('core-split-view ion-router-outlet'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return topContainer; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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. | ||||||
|  |      * @return First found element. | ||||||
|  |      */ | ||||||
|  |     static findElementBasedOnText(locator: TestBehatElementLocator, containerName = ''): HTMLElement { | ||||||
|  |         return this.findElementsBasedOnText(locator, containerName)[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. | ||||||
|  |      * @return Found elements | ||||||
|  |      */ | ||||||
|  |     protected static findElementsBasedOnText(locator: TestBehatElementLocator, containerName = ''): HTMLElement[] { | ||||||
|  |         let topContainer = this.getCurrentTopContainerElement(containerName); | ||||||
|  | 
 | ||||||
|  |         let container = topContainer; | ||||||
|  | 
 | ||||||
|  |         if (locator.within) { | ||||||
|  |             const withinElements = this.findElementsBasedOnText(locator.within); | ||||||
|  | 
 | ||||||
|  |             if (withinElements.length === 0) { | ||||||
|  |                 throw new Error('There was no match for within text'); | ||||||
|  |             } else if (withinElements.length > 1) { | ||||||
|  |                 const withinElementsAncestors = this.getTopAncestors(withinElements); | ||||||
|  | 
 | ||||||
|  |                 if (withinElementsAncestors.length > 1) { | ||||||
|  |                     throw new Error('Too many matches for within text'); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 topContainer = container = withinElementsAncestors[0]; | ||||||
|  |             } else { | ||||||
|  |                 topContainer = container = withinElements[0]; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (topContainer && locator.near) { | ||||||
|  |             const nearElements = this.findElementsBasedOnText(locator.near); | ||||||
|  | 
 | ||||||
|  |             if (nearElements.length === 0) { | ||||||
|  |                 throw new Error('There was no match for near text'); | ||||||
|  |             } else if (nearElements.length > 1) { | ||||||
|  |                 const nearElementsAncestors = this.getTopAncestors(nearElements); | ||||||
|  | 
 | ||||||
|  |                 if (nearElementsAncestors.length > 1) { | ||||||
|  |                     throw new Error('Too many matches for near text'); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 container = this.getParentElement(nearElementsAncestors[0]); | ||||||
|  |             } else { | ||||||
|  |                 container = this.getParentElement(nearElements[0]); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         do { | ||||||
|  |             if (!container) { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const elements = this.findElementsBasedOnTextWithin(container, locator.text); | ||||||
|  | 
 | ||||||
|  |             let filteredElements: HTMLElement[] = elements; | ||||||
|  | 
 | ||||||
|  |             if (locator.selector) { | ||||||
|  |                 filteredElements = []; | ||||||
|  |                 const selector = locator.selector; | ||||||
|  | 
 | ||||||
|  |                 elements.forEach((element) => { | ||||||
|  |                     const closest = this.getClosestMatching(element, selector, container); | ||||||
|  |                     if (closest) { | ||||||
|  |                         filteredElements.push(closest); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (filteredElements.length > 0) { | ||||||
|  |                 return filteredElements; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } while (container !== topContainer && (container = this.getParentElement(container)) && container !== topContainer); | ||||||
|  | 
 | ||||||
|  |         return []; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Make sure that an element is visible and wait to trigger the callback. | ||||||
|  |      * | ||||||
|  |      * @param element Element. | ||||||
|  |      */ | ||||||
|  |     protected static async ensureElementVisible(element: HTMLElement): Promise<DOMRect> { | ||||||
|  |         const initialRect = element.getBoundingClientRect(); | ||||||
|  | 
 | ||||||
|  |         element.scrollIntoView(false); | ||||||
|  | 
 | ||||||
|  |         return new Promise<DOMRect>((resolve): void => { | ||||||
|  |             requestAnimationFrame(() => { | ||||||
|  |                 const rect = element.getBoundingClientRect(); | ||||||
|  | 
 | ||||||
|  |                 if (initialRect.y !== rect.y) { | ||||||
|  |                     setTimeout(() => { | ||||||
|  |                         resolve(rect); | ||||||
|  |                     }, 300); | ||||||
|  | 
 | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 resolve(rect); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Press an element. | ||||||
|  |      * | ||||||
|  |      * @param element Element to press. | ||||||
|  |      */ | ||||||
|  |     static async pressElement(element: HTMLElement): Promise<void> { | ||||||
|  |         NgZone.run(async () => { | ||||||
|  |             const blockKey = TestsBehatBlocking.block(); | ||||||
|  | 
 | ||||||
|  |             // Events don't bubble up across Shadow DOM boundaries, and some buttons
 | ||||||
|  |             // may not work without doing this.
 | ||||||
|  |             const parentElement = this.getParentElement(element); | ||||||
|  | 
 | ||||||
|  |             if (parentElement && parentElement.matches('ion-button, ion-back-button')) { | ||||||
|  |                 element = parentElement; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const rect = await this.ensureElementVisible(element); | ||||||
|  | 
 | ||||||
|  |             // Simulate a mouse click on the button.
 | ||||||
|  |             const eventOptions: MouseEventInit = { | ||||||
|  |                 clientX: rect.left + rect.width / 2, | ||||||
|  |                 clientY: rect.top + rect.height / 2, | ||||||
|  |                 bubbles: true, | ||||||
|  |                 view: window, | ||||||
|  |                 cancelable: true, | ||||||
|  |             }; | ||||||
|  | 
 | ||||||
|  |             // There are some buttons in the app that don't respond to click events, for example
 | ||||||
|  |             // buttons using the core-supress-events directive. That's why we need to send both
 | ||||||
|  |             // click and mouse events.
 | ||||||
|  |             element.dispatchEvent(new MouseEvent('mousedown', eventOptions)); | ||||||
|  | 
 | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 element.dispatchEvent(new MouseEvent('mouseup', eventOptions)); | ||||||
|  |                 element.click(); | ||||||
|  | 
 | ||||||
|  |                 TestsBehatBlocking.unblock(blockKey); | ||||||
|  |             }, 300); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Set an element value. | ||||||
|  |      * | ||||||
|  |      * @param element HTML to set. | ||||||
|  |      * @param value Value to be set. | ||||||
|  |      */ | ||||||
|  |     static async setElementValue(element: HTMLElement, value: string): Promise<void> { | ||||||
|  |         NgZone.run(async () => { | ||||||
|  |             const blockKey = TestsBehatBlocking.block(); | ||||||
|  | 
 | ||||||
|  |             // Functions to get/set value depending on field type.
 | ||||||
|  |             let setValue = (text: string) => { | ||||||
|  |                 element.innerHTML = text; | ||||||
|  |             }; | ||||||
|  |             let getValue = () => element.innerHTML; | ||||||
|  | 
 | ||||||
|  |             if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { | ||||||
|  |                 setValue = (text: string) => { | ||||||
|  |                     element.value = text; | ||||||
|  |                 }; | ||||||
|  |                 getValue = () => element.value; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Pretend we have cut and pasted the new text.
 | ||||||
|  |             let event: InputEvent; | ||||||
|  |             if (getValue() !== '') { | ||||||
|  |                 event = new InputEvent('input', { | ||||||
|  |                     bubbles: true, | ||||||
|  |                     view: window, | ||||||
|  |                     cancelable: true, | ||||||
|  |                     inputType: 'deleteByCut', | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 await CoreUtils.nextTick(); | ||||||
|  |                 setValue(''); | ||||||
|  |                 element.dispatchEvent(event); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (value !== '') { | ||||||
|  |                 event = new InputEvent('input', { | ||||||
|  |                     bubbles: true, | ||||||
|  |                     view: window, | ||||||
|  |                     cancelable: true, | ||||||
|  |                     inputType: 'insertFromPaste', | ||||||
|  |                     data: value, | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 await CoreUtils.nextTick(); | ||||||
|  |                 setValue(value); | ||||||
|  |                 element.dispatchEvent(event); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             TestsBehatBlocking.unblock(blockKey); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type ElementsWithExact = { | ||||||
|  |     element: HTMLElement; | ||||||
|  |     exact: boolean; | ||||||
|  | }; | ||||||
							
								
								
									
										398
									
								
								src/testing/services/behat-runtime.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								src/testing/services/behat-runtime.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,398 @@ | |||||||
|  | // (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 { TestsBehatDomUtils } from './behat-dom'; | ||||||
|  | import { TestsBehatBlocking } from './behat-blocking'; | ||||||
|  | import { CoreCustomURLSchemes } from '@services/urlschemes'; | ||||||
|  | import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; | ||||||
|  | import { CoreConfig } from '@services/config'; | ||||||
|  | import { EnvironmentConfig } from '@/types/config'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Behat runtime servive with public API. | ||||||
|  |  */ | ||||||
|  | export class TestsBehatRuntime { | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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, | ||||||
|  |             scrollTo: TestsBehatRuntime.scrollTo, | ||||||
|  |             setField: TestsBehatRuntime.setField, | ||||||
|  |             handleCustomURL: TestsBehatRuntime.handleCustomURL, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         if (!options) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (options.skipOnBoarding === true) { | ||||||
|  |             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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles a custom URL. | ||||||
|  |      * | ||||||
|  |      * @param url Url to open. | ||||||
|  |      * @return OK if successful, or ERROR: followed by message. | ||||||
|  |      */ | ||||||
|  |     static async handleCustomURL(url: string): Promise<string> { | ||||||
|  |         const blockKey = TestsBehatBlocking.block(); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             await CoreCustomURLSchemes.handleCustomURL(url); | ||||||
|  | 
 | ||||||
|  |             return 'OK'; | ||||||
|  |         } catch (error) { | ||||||
|  |             return 'ERROR: ' + error.message; | ||||||
|  |         } finally { | ||||||
|  |             TestsBehatBlocking.unblock(blockKey); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Function to find and click an app standard button. | ||||||
|  |      * | ||||||
|  |      * @param button Type of button to press. | ||||||
|  |      * @return OK if successful, or ERROR: followed by message. | ||||||
|  |      */ | ||||||
|  |     static pressStandard(button: string): string { | ||||||
|  |         this.log('Action - Click standard button: ' + button); | ||||||
|  | 
 | ||||||
|  |         // Find button
 | ||||||
|  |         let foundButton: HTMLElement | undefined; | ||||||
|  | 
 | ||||||
|  |         switch (button) { | ||||||
|  |             case 'back': | ||||||
|  |                 foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'Back' }); | ||||||
|  |                 break; | ||||||
|  |             case 'main menu': // Deprecated name.
 | ||||||
|  |             case 'more menu': | ||||||
|  |                 foundButton = TestsBehatDomUtils.findElementBasedOnText({ | ||||||
|  |                     text: 'More', | ||||||
|  |                     selector: 'ion-tab-button', | ||||||
|  |                 }); | ||||||
|  |                 break; | ||||||
|  |             case 'user menu' : | ||||||
|  |                 foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'User account' }); | ||||||
|  |                 break; | ||||||
|  |             case 'page menu': | ||||||
|  |                 foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'Display options' }); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 return 'ERROR: Unsupported standard button type'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!foundButton) { | ||||||
|  |             return `ERROR: Button '${button}' not found`; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Click button
 | ||||||
|  |         TestsBehatDomUtils.pressElement(foundButton); | ||||||
|  | 
 | ||||||
|  |         return 'OK'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * When there is a popup, clicks on the backdrop. | ||||||
|  |      * | ||||||
|  |      * @return OK if successful, or ERROR: followed by message | ||||||
|  |      */ | ||||||
|  |     static closePopup(): string { | ||||||
|  |         this.log('Action - Close popup'); | ||||||
|  | 
 | ||||||
|  |         let backdrops = Array.from(document.querySelectorAll('ion-backdrop')); | ||||||
|  |         backdrops = backdrops.filter((backdrop) => !!backdrop.offsetParent); | ||||||
|  | 
 | ||||||
|  |         if (!backdrops.length) { | ||||||
|  |             return 'ERROR: Could not find backdrop'; | ||||||
|  |         } | ||||||
|  |         if (backdrops.length > 1) { | ||||||
|  |             return 'ERROR: Found too many backdrops'; | ||||||
|  |         } | ||||||
|  |         const backdrop = backdrops[0]; | ||||||
|  |         backdrop.click(); | ||||||
|  | 
 | ||||||
|  |         // Mark busy until the click finishes processing.
 | ||||||
|  |         TestsBehatBlocking.delay(); | ||||||
|  | 
 | ||||||
|  |         return 'OK'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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. | ||||||
|  |      * @return OK if successful, or ERROR: followed by message | ||||||
|  |      */ | ||||||
|  |     static find(locator: TestBehatElementLocator, containerName: string): string { | ||||||
|  |         this.log('Action - Find', { locator, containerName }); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const element = TestsBehatDomUtils.findElementBasedOnText(locator, containerName); | ||||||
|  | 
 | ||||||
|  |             if (!element) { | ||||||
|  |                 return 'ERROR: No element matches locator to find.'; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             this.log('Action - Found', { locator, containerName, element }); | ||||||
|  | 
 | ||||||
|  |             return 'OK'; | ||||||
|  |         } catch (error) { | ||||||
|  |             return 'ERROR: ' + error.message; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Scroll an element into view. | ||||||
|  |      * | ||||||
|  |      * @param locator Element locator. | ||||||
|  |      * @return OK if successful, or ERROR: followed by message | ||||||
|  |      */ | ||||||
|  |     static scrollTo(locator: TestBehatElementLocator): string { | ||||||
|  |         this.log('Action - scrollTo', { locator }); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             let element = TestsBehatDomUtils.findElementBasedOnText(locator); | ||||||
|  | 
 | ||||||
|  |             if (!element) { | ||||||
|  |                 return 'ERROR: No element matches element to scroll to.'; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             element = element.closest('ion-item') ?? element.closest('button') ?? element; | ||||||
|  | 
 | ||||||
|  |             element.scrollIntoView(); | ||||||
|  | 
 | ||||||
|  |             this.log('Action - Scrolled to', { locator, element }); | ||||||
|  | 
 | ||||||
|  |             return 'OK'; | ||||||
|  |         } catch (error) { | ||||||
|  |             return 'ERROR: ' + error.message; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load more items form an active list with infinite loader. | ||||||
|  |      * | ||||||
|  |      * @return OK if successful, or ERROR: followed by message | ||||||
|  |      */ | ||||||
|  |     static async loadMoreItems(): Promise<string> { | ||||||
|  |         this.log('Action - loadMoreItems'); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const infiniteLoading = Array | ||||||
|  |                 .from(document.querySelectorAll<HTMLElement>('core-infinite-loading')) | ||||||
|  |                 .find(element => !element.closest('.ion-page-hidden')); | ||||||
|  | 
 | ||||||
|  |             if (!infiniteLoading) { | ||||||
|  |                 return 'ERROR: There isn\'t an infinite loader in the current page.'; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const initialOffset = infiniteLoading.offsetTop; | ||||||
|  |             const isLoading = () => !!infiniteLoading.querySelector('ion-spinner[aria-label]'); | ||||||
|  |             const isCompleted = () => !isLoading() && !infiniteLoading.querySelector('ion-button'); | ||||||
|  |             const hasMoved = () => infiniteLoading.offsetTop !== initialOffset; | ||||||
|  | 
 | ||||||
|  |             if (isCompleted()) { | ||||||
|  |                 return 'ERROR: All items are already loaded.'; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             infiniteLoading.scrollIntoView({ behavior: 'smooth' }); | ||||||
|  | 
 | ||||||
|  |             // Wait 100ms
 | ||||||
|  |             await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  | 
 | ||||||
|  |             if (isLoading() || isCompleted() || hasMoved()) { | ||||||
|  |                 return 'OK'; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             infiniteLoading.querySelector<HTMLElement>('ion-button')?.click(); | ||||||
|  | 
 | ||||||
|  |             // Wait 100ms
 | ||||||
|  |             await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  | 
 | ||||||
|  |             return (isLoading() || isCompleted() || hasMoved()) ? 'OK' : 'ERROR: Couldn\'t load more items.'; | ||||||
|  |         } catch (error) { | ||||||
|  |             return 'ERROR: ' + error.message; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check whether an item is selected or not. | ||||||
|  |      * | ||||||
|  |      * @param locator Element locator. | ||||||
|  |      * @return YES or NO if successful, or ERROR: followed by message | ||||||
|  |      */ | ||||||
|  |     static isSelected(locator: TestBehatElementLocator): string { | ||||||
|  |         this.log('Action - Is Selected', locator); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const element = TestsBehatDomUtils.findElementBasedOnText(locator); | ||||||
|  | 
 | ||||||
|  |             return TestsBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO'; | ||||||
|  |         } catch (error) { | ||||||
|  |             return 'ERROR: ' + error.message; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Function to press arbitrary item based on its text or Aria label. | ||||||
|  |      * | ||||||
|  |      * @param locator Element locator. | ||||||
|  |      * @return OK if successful, or ERROR: followed by message | ||||||
|  |      */ | ||||||
|  |     static press(locator: TestBehatElementLocator): string { | ||||||
|  |         this.log('Action - Press', locator); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const found = TestsBehatDomUtils.findElementBasedOnText(locator); | ||||||
|  | 
 | ||||||
|  |             if (!found) { | ||||||
|  |                 return 'ERROR: No element matches locator to press.'; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             TestsBehatDomUtils.pressElement(found); | ||||||
|  | 
 | ||||||
|  |             return 'OK'; | ||||||
|  |         } catch (error) { | ||||||
|  |             return 'ERROR: ' + error.message; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the currently displayed page header. | ||||||
|  |      * | ||||||
|  |      * @return OK: followed by header text if successful, or ERROR: followed by message. | ||||||
|  |      */ | ||||||
|  |     static 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)); | ||||||
|  | 
 | ||||||
|  |         if (titles.length > 1) { | ||||||
|  |             return 'ERROR: Too many possible titles.'; | ||||||
|  |         } else if (!titles.length) { | ||||||
|  |             return 'ERROR: No title found.'; | ||||||
|  |         } else { | ||||||
|  |             const title = titles[0].innerText.trim(); | ||||||
|  | 
 | ||||||
|  |             return 'OK:' + title; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sets the text of a field to the specified value. | ||||||
|  |      * | ||||||
|  |      * This currently matches fields only based on the placeholder attribute. | ||||||
|  |      * | ||||||
|  |      * @param field Field name | ||||||
|  |      * @param value New value | ||||||
|  |      * @return OK or ERROR: followed by message | ||||||
|  |      */ | ||||||
|  |     static setField(field: string, value: string): string { | ||||||
|  |         this.log('Action - Set field ' + field + ' to: ' + value); | ||||||
|  | 
 | ||||||
|  |         const found: HTMLElement | HTMLInputElement | HTMLTextAreaElement =TestsBehatDomUtils.findElementBasedOnText( | ||||||
|  |             { text: field, selector: 'input, textarea, [contenteditable="true"]' }, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         if (!found) { | ||||||
|  |             return 'ERROR: No element matches field to set.'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         TestsBehatDomUtils.setElementValue(found, value); | ||||||
|  | 
 | ||||||
|  |         return 'OK'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get an Angular component instance. | ||||||
|  |      * | ||||||
|  |      * @param selector Element selector | ||||||
|  |      * @param className Constructor class name | ||||||
|  |      * @return Component instance | ||||||
|  |      */ | ||||||
|  |     static getAngularInstance(selector: string, className: string): unknown { | ||||||
|  |         this.log('Action - Get Angular instance ' + selector + ', ' + className); | ||||||
|  | 
 | ||||||
|  |         // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  |         const activeElement = Array.from(document.querySelectorAll<any>(`.ion-page:not(.ion-page-hidden) ${selector}`)).pop(); | ||||||
|  | 
 | ||||||
|  |         if (!activeElement || !activeElement.__ngContext__) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return activeElement.__ngContext__.find(node => node?.constructor?.name === className); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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 { | ||||||
|  |         const now = new Date(); | ||||||
|  |         const nowFormatted = String(now.getHours()).padStart(2, '0') + ':' + | ||||||
|  |                 String(now.getMinutes()).padStart(2, '0') + ':' + | ||||||
|  |                 String(now.getSeconds()).padStart(2, '0') + '.' + | ||||||
|  |                 String(now.getMilliseconds()).padStart(2, '0'); | ||||||
|  | 
 | ||||||
|  |         console.log('BEHAT: ' + nowFormatted, ...args); // eslint-disable-line no-console
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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 = { | ||||||
|  |     text: string; | ||||||
|  |     within?: TestBehatElementLocator; | ||||||
|  |     near?: TestBehatElementLocator; | ||||||
|  |     selector?: string; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export type TestsBehatInitOptions = { | ||||||
|  |     skipOnBoarding?: boolean; | ||||||
|  |     configOverrides?: Partial<EnvironmentConfig>; | ||||||
|  | }; | ||||||
							
								
								
									
										30
									
								
								src/tests/behat/navigation_activities.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/tests/behat/navigation_activities.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | @app @javascript | ||||||
|  | Feature: It navigates properly within activities. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student  | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user    | course | role    | | ||||||
|  |       | student | C1     | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | idnumber | course | name  | intro             | content | | ||||||
|  |       | label      | label    | C1     | Label | Label description | -       | | ||||||
|  |       | page       | page     | C1     | Page  | -                 | <a href="/mod/label/view.php?id=${label:cmid}">Go to label</a> | | ||||||
|  |     And I replace the arguments in "page" "content" | ||||||
|  | 
 | ||||||
|  |   Scenario: Navigates using deep links | ||||||
|  |     Given I entered the course "Course 1" as "student" in the app | ||||||
|  |     When I press "Page" in the app | ||||||
|  |     And I press "Go to label" in the app | ||||||
|  |     Then I should find "Label description" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     Then I should find "Go to label" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     Then I should find "Label description" in the app | ||||||
							
								
								
									
										84
									
								
								src/tests/behat/navigation_deeplinks.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/tests/behat/navigation_deeplinks.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | |||||||
|  | @app @javascript | ||||||
|  | Feature: It navigates properly using deep links. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  |       | student2 | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |       | student2 | C1     | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name       | intro       | course | idnumber | | ||||||
|  |       | forum      | Test forum | Test forum  | C1     | forum    | | ||||||
|  |     And the following forum discussions exist in course "Course 1": | ||||||
|  |       | forum      | user     | name        | message       | | ||||||
|  |       | Test forum | student1 | Forum topic | Forum message | | ||||||
|  |     And the following config values are set as admin: | ||||||
|  |       | forcelogout     | 1 | tool_mobile | | ||||||
|  |       | defaulthomepage | 0 |             | | ||||||
|  | 
 | ||||||
|  |   Scenario: Receive a push notification | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I press the user menu button in the app | ||||||
|  |     And I press "Log out" in the app | ||||||
|  |     And I wait the app to restart | ||||||
|  |     And I press "Add" in the app | ||||||
|  |     And I set the field "Your site" to "$WWWROOT" in the app | ||||||
|  |     And I press "Connect to your site" in the app | ||||||
|  |     And I log in as "student1" | ||||||
|  |     And I receive a push notification in the app for: | ||||||
|  |       | username | module | discussion  | | ||||||
|  |       | student2 | forum  | Forum topic | | ||||||
|  |     And I wait the app to restart | ||||||
|  |     Then I should find "Reconnect" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "Password" to "student2" in the app | ||||||
|  |     And I press "Log in" in the app | ||||||
|  |     Then I should find "Forum topic" in the app | ||||||
|  |     And I should find "Forum message" in the app | ||||||
|  |     But I should not find "Site home" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     Then I should find "Site home" in the app | ||||||
|  |     But I should not find "Forum topic" in the app | ||||||
|  |     And I should not find "Forum message" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Open a link with a custom URL | ||||||
|  |     When I launch the app | ||||||
|  |     And I open a custom link in the app for: | ||||||
|  |       | discussion  | | ||||||
|  |       | Forum topic | | ||||||
|  |     And I log in as "student1" | ||||||
|  |     And I wait loading to finish in the app | ||||||
|  |     Then I should find "Forum topic" in the app | ||||||
|  |     And I should find "Forum message" in the app | ||||||
|  |     But I should not find "Site home" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     Then I should find "Site home" in the app | ||||||
|  |     But I should not find "Forum topic" in the app | ||||||
|  |     And I should not find "Forum message" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Open a link with a custom URL that calls WebServices for a logged out site | ||||||
|  |     Given I entered the app as "student2" | ||||||
|  |     When I press the user menu button in the app | ||||||
|  |     And I press "Log out" in the app | ||||||
|  |     And I wait the app to restart | ||||||
|  |     And I open a custom link in the app for: | ||||||
|  |       | forum      | | ||||||
|  |       | Test forum | | ||||||
|  |     Then I should find "Reconnect" in the app | ||||||
|  | 
 | ||||||
|  |     When I set the field "Password" to "student2" in the app | ||||||
|  |     And I press "Log in" in the app | ||||||
|  |     Then I should find "Test forum" in the app | ||||||
|  | 
 | ||||||
|  |     When I press the back button in the app | ||||||
|  |     Then I should find "Site home" in the app | ||||||
|  |     But I should not find "Test forum" in the app | ||||||
							
								
								
									
										46
									
								
								src/tests/behat/navigation_externallinks.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/tests/behat/navigation_externallinks.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | @app @javascript | ||||||
|  | Feature: It opens external links properly. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |     And the following "activities" exist: | ||||||
|  |       | activity   | name       | intro       | course | idnumber | | ||||||
|  |       | forum      | Test forum | Test forum  | C1     | forum    | | ||||||
|  |     And the following forum discussions exist in course "Course 1": | ||||||
|  |       | forum      | user     | name        | message       | | ||||||
|  |       | Test forum | student1 | Forum topic | See <a href="https://moodle.org/">moodle.org external link</a> | | ||||||
|  | 
 | ||||||
|  |   Scenario: Click an external link | ||||||
|  |     Given I entered the forum activity "Test forum" on course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Forum topic" in the app | ||||||
|  |     And I press "moodle.org external link" in the app | ||||||
|  |     Then I should find "You are about to leave the app" in the app | ||||||
|  | 
 | ||||||
|  |     When I press "Cancel" in the app | ||||||
|  |     And I press "moodle.org external link" in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     Then the app should have opened a browser tab with url "moodle.org" | ||||||
|  | 
 | ||||||
|  |     When I close the browser tab opened by the app | ||||||
|  |     And I press the back button in the app | ||||||
|  |     And I press "Information" in the app | ||||||
|  |     And I press "Open in browser" in the app | ||||||
|  |     Then the app should have opened a browser tab | ||||||
|  | 
 | ||||||
|  |     When I close the browser tab opened by the app | ||||||
|  |     When I close the popup in the app | ||||||
|  |     And I press "Forum topic" in the app | ||||||
|  |     And I press "moodle.org external link" in the app | ||||||
|  |     And I select "Don't show again." in the app | ||||||
|  |     And I press "OK" in the app | ||||||
|  |     And I close the browser tab opened by the app | ||||||
|  |     And I press "moodle.org external link" in the app | ||||||
|  |     Then the app should have opened a browser tab with url "moodle.org" | ||||||
							
								
								
									
										59
									
								
								src/tests/behat/navigation_gestures.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/tests/behat/navigation_gestures.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | @app @javascript | ||||||
|  | Feature: It navigates using gestures. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | firstname | lastname | | ||||||
|  |       | student1 | Student   | First    | | ||||||
|  |       | teacher1 | Teacher   | First    | | ||||||
|  |       | student2 | Student   | Second   | | ||||||
|  |       | teacher2 | Teacher   | Second   | | ||||||
|  |       | student3 | Student   | Third    | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |       | teacher1 | C1     | teacher | | ||||||
|  |       | student2 | C1     | student | | ||||||
|  |       | teacher2 | C1     | teacher | | ||||||
|  |       | student3 | C1     | student | | ||||||
|  | 
 | ||||||
|  |   Scenario: Swipe between participants | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Student First" in the app | ||||||
|  |     And I swipe to the left in the app | ||||||
|  |     Then I should find "Teacher First" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Student Second" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Teacher First" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Student First" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Student First" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Swipe between filtered participants | ||||||
|  |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|  |     When I press "Participants" in the app | ||||||
|  |     And I press "Search" in the app | ||||||
|  |     And I set the field "Search" to "student" in the app | ||||||
|  |     And I press "Search" "button" near "Clear search" in the app | ||||||
|  |     And I press "Student First" in the app | ||||||
|  |     And I swipe to the left in the app | ||||||
|  |     Then I should find "Student Second" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the left in the app | ||||||
|  |     Then I should find "Student Third" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Student Second" in the app | ||||||
|  | 
 | ||||||
|  |     When I swipe to the right in the app | ||||||
|  |     Then I should find "Student First" in the app | ||||||
							
								
								
									
										109
									
								
								src/tests/behat/navigation_splitview.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/tests/behat/navigation_splitview.feature
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,109 @@ | |||||||
|  | @app @javascript | ||||||
|  | Feature: It navigates properly in pages with a split-view component. | ||||||
|  | 
 | ||||||
|  |   Background: | ||||||
|  |     Given the following "users" exist: | ||||||
|  |       | username | | ||||||
|  |       | student1 | | ||||||
|  |     And the following "courses" exist: | ||||||
|  |       | fullname | shortname | | ||||||
|  |       | Course 2 | C2        | | ||||||
|  |       | Course 1 | C1        | | ||||||
|  |     And the following "course enrolments" exist: | ||||||
|  |       | user     | course | role    | | ||||||
|  |       | student1 | C1     | student | | ||||||
|  |       | student1 | C2     | student | | ||||||
|  |     And the following "grade categories" exist: | ||||||
|  |       | fullname          | course | | ||||||
|  |       | Grade category C1 | C1     | | ||||||
|  |       | Grade category C2 | C2     | | ||||||
|  |     And the following "grade items" exist: | ||||||
|  |       | gradecategory     | itemname      | grademin | grademax | course | | ||||||
|  |       | Grade category C1 | Grade item C1 | 20       | 40       | C1     | | ||||||
|  |       | Grade category C2 | Grade item C2 | 60       | 80       | C2     | | ||||||
|  | 
 | ||||||
|  |   Scenario: Navigate in grades tab on mobile | ||||||
|  | 
 | ||||||
|  |     # Open user menu | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     And I press the user menu button in the app | ||||||
|  | 
 | ||||||
|  |     # Open grades page | ||||||
|  |     When I press "Grades" in the app | ||||||
|  |     Then the header should be "Grades" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  | 
 | ||||||
|  |     # Open C1 course grades | ||||||
|  |     When I press "Course 1" in the app | ||||||
|  |     Then the header should be "Course 1" in the app | ||||||
|  |     And I should find "Grade category C1" in the app | ||||||
|  | 
 | ||||||
|  |     # Open C1 grade item | ||||||
|  |     When I press "Grade item C1" in the app | ||||||
|  |     Then I should find "20" near "Range" in the app | ||||||
|  |     And I should find "40" near "Range" in the app | ||||||
|  | 
 | ||||||
|  |     # Go back to grades page | ||||||
|  |     When I press the back button in the app | ||||||
|  |     Then the header should be "Grades" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  | 
 | ||||||
|  |     # Open C2 course grades | ||||||
|  |     When I press "Course 2" in the app | ||||||
|  |     Then the header should be "Course 2" in the app | ||||||
|  |     And I should find "Grade category C2" in the app | ||||||
|  | 
 | ||||||
|  |     # Open C2 grade item | ||||||
|  |     When I press "Grade item C2" in the app | ||||||
|  |     Then I should find "60" near "Range" in the app | ||||||
|  |     And I should find "80" near "Range" in the app | ||||||
|  | 
 | ||||||
|  |     # Go back to grades page | ||||||
|  |     When I press the back button in the app | ||||||
|  |     Then the header should be "Grades" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  | 
 | ||||||
|  |     # Go back to main page | ||||||
|  |     When I press the back button in the app | ||||||
|  |     Then I should find "Acceptance test site" in the app | ||||||
|  |     And I should find "User account" in the app | ||||||
|  |     But I should not find "Back" in the app | ||||||
|  | 
 | ||||||
|  |   Scenario: Navigate in grades tab on tablet | ||||||
|  | 
 | ||||||
|  |     # Open user menu | ||||||
|  |     Given I entered the app as "student1" | ||||||
|  |     And I change viewport size to "1200x640" | ||||||
|  |     And I press the user menu button in the app | ||||||
|  | 
 | ||||||
|  |     # Open grades page | ||||||
|  |     When I press "Grades" in the app | ||||||
|  |     Then the header should be "Grades" in the app | ||||||
|  |     And I should find "Course 1" in the app | ||||||
|  |     And I should find "Course 2" in the app | ||||||
|  |     And I should find "Grade category C1" in the app | ||||||
|  | 
 | ||||||
|  |     # Open C1 course grades | ||||||
|  |     When I press "Grade item C1" in the app | ||||||
|  |     Then I should find "Grade category C1" in the app | ||||||
|  |     And I should find "20" near "Range" in the app | ||||||
|  |     And I should find "40" near "Range" in the app | ||||||
|  | 
 | ||||||
|  |     # Select C2 course | ||||||
|  |     When I press "Course 2" in the app | ||||||
|  |     Then "Course 2" should be selected in the app | ||||||
|  |     And I should find "Grade category C2" in the app | ||||||
|  | 
 | ||||||
|  |     # Open C2 course grades | ||||||
|  |     When I press "Grade item C2" in the app | ||||||
|  |     Then I should find "60" near "Range" in the app | ||||||
|  |     And I should find "80" near "Range" in the app | ||||||
|  | 
 | ||||||
|  |     # Go back to main page | ||||||
|  |     When I press the back button in the app | ||||||
|  |     Then I should find "Acceptance test site" in the app | ||||||
|  |     And I should find "User account" in the app | ||||||
|  |     But I should not find "Back" in the app | ||||||
| @ -81,13 +81,7 @@ Feature: Measure performance. | |||||||
|     Then "Login" should have taken less than 10 seconds |     Then "Login" should have taken less than 10 seconds | ||||||
| 
 | 
 | ||||||
|   Scenario: Open Activity |   Scenario: Open Activity | ||||||
|     When I launch the app |     Given I entered the app as "student1" | ||||||
|     Then I should see "Connect to Moodle" |  | ||||||
|     But I should not see "Welcome to the Moodle App!" |  | ||||||
| 
 |  | ||||||
|     And I set the field "Your site" to "$WWWROOT" in the app |  | ||||||
|     And I press "Connect to your site" in the app |  | ||||||
|     And I log in as "student1" |  | ||||||
|     Then I press "My courses" in the app |     Then I press "My courses" in the app | ||||||
|     And I should find "Course 1" in the app |     And I should find "Course 1" in the app | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user