Merge pull request #3278 from crazyserver/MOBILE-4061

Mobile 4061: Include behat tests on app code
main
Noel De Martin 2022-05-12 17:36:42 +02:00 committed by GitHub
commit a9dddde79a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 7111 additions and 29 deletions

View 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"

View File

@ -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: |

View File

@ -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'));
});

View 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');
}
}

View 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'],
],
],
];

View File

@ -0,0 +1,5 @@
this.CoreSitesProvider.getSite().then(site => {
const username = site.infos.username;
document.getElementById('username').innerText = `, ${username}`;
});

View 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);");
}
}

View 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);
}
}

View File

@ -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 {
" ],",
" ],",
" ],",
");",
"];",
"",
])
);

View File

@ -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);
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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"

View 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

View File

@ -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 },

View File

@ -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;

View 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

View 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

View File

@ -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

View File

@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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"

View 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"

View File

@ -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

View 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

View 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

View File

@ -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);

View 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 {}

View 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 {}

View 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);

View 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;
};

View 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>;
};

View 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

View 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

View 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"

View 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

View 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

View File

@ -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