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 | ||||
| 
 | ||||
| on: [push, pull_request] | ||||
| on: [ pull_request, workflow_dispatch ] | ||||
| 
 | ||||
| jobs: | ||||
|   performance: | ||||
| @ -19,7 +19,6 @@ jobs: | ||||
|     - name: Additional checkouts | ||||
|       run: | | ||||
|         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 | ||||
|     - name: Install npm packages | ||||
|       run: | | ||||
|  | ||||
| @ -71,5 +71,5 @@ gulp.task('watch', () => { | ||||
| }); | ||||
| 
 | ||||
| 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 Moodle\BehatExtension\Driver\WebDriver; | ||||
| 
 | ||||
| require_once(__DIR__ . '/../behat_app.php'); | ||||
| 
 | ||||
| /** | ||||
|  * 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.
 | ||||
|     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 = { | ||||
|         appVersion, | ||||
|         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)); | ||||
|     } | ||||
| 
 | ||||
|     // Copy plugin files.
 | ||||
|     copySync(projectPath('tests/behat'), `${pluginPath}/tests/behat`); | ||||
|     writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements)); | ||||
| 
 | ||||
|     // Copy feature files.
 | ||||
|     const behatTempFeaturesPath = `${pluginPath}/behat-tmp`; | ||||
| @ -80,7 +77,8 @@ async function main() { | ||||
|         } | ||||
| 
 | ||||
|         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); | ||||
|         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 { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron'; | ||||
| import { moodleTransitionAnimation } from '@classes/page-transition'; | ||||
| import { BehatTestingModule } from '@/testing/behat-testing.module'; | ||||
| 
 | ||||
| // For translate loader. AoT requires an exported function for factories.
 | ||||
| export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { | ||||
| @ -59,6 +60,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { | ||||
|         AppRoutingModule, | ||||
|         CoreModule, | ||||
|         AddonsModule, | ||||
|         BehatTestingModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, | ||||
|  | ||||
| @ -208,10 +208,17 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy { | ||||
|                 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.
 | ||||
|             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') { | ||||
|                 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.
 | ||||
|             this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId); | ||||
|             await this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId); | ||||
|             this.sites[siteId] = candidateSite; | ||||
| 
 | ||||
|             if (login) { | ||||
|                 // Turn candidate site into current site.
 | ||||
|                 this.currentSite = candidateSite; | ||||
|                 // Store session.
 | ||||
|                 this.login(siteId); | ||||
|                 await this.login(siteId); | ||||
|             } else if (this.currentSite && this.currentSite.getId() == siteId) { | ||||
|                 // Current site has just been updated, trigger the event.
 | ||||
|                 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 | ||||
| 
 | ||||
|   Scenario: Open Activity | ||||
|     When I launch the app | ||||
|     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" | ||||
|     Given I entered the app as "student1" | ||||
|     Then I press "My courses" in the app | ||||
|     And I should find "Course 1" in the app | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user