Merge remote-tracking branch 'origin/4.1' into integration
commit
d897ccffbd
|
@ -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"
|
|
@ -1,6 +1,6 @@
|
||||||
name: Performance
|
name: Performance
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [ pull_request, workflow_dispatch ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
performance:
|
performance:
|
||||||
|
@ -19,7 +19,6 @@ jobs:
|
||||||
- name: Additional checkouts
|
- name: Additional checkouts
|
||||||
run: |
|
run: |
|
||||||
git clone --branch master --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle
|
git clone --branch master --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle
|
||||||
git clone --branch integration --depth 1 https://github.com/moodlehq/moodle-local_moodlemobileapp $GITHUB_WORKSPACE/moodle/local/moodlemobileapp
|
|
||||||
git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
|
git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
|
||||||
- name: Install npm packages
|
- name: Install npm packages
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<widget android-versionCode="40100" id="com.moodle.moodlemobile" ios-CFBundleVersion="4.0.1.0" version="4.0.1" versionCode="40100" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
<widget android-versionCode="41000" id="com.moodle.moodlemobile" ios-CFBundleVersion="4.1.0.0" version="4.1.0-dev" versionCode="41000" xmlns="http://www.w3.org/ns/widgets" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||||
<name>Moodle</name>
|
<name>Moodle</name>
|
||||||
<description>Moodle official app</description>
|
<description>Moodle official app</description>
|
||||||
<author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author>
|
<author email="mobile@moodle.com" href="http://moodle.com">Moodle Mobile team</author>
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<preference name="UIWebViewBounce" value="false" />
|
<preference name="UIWebViewBounce" value="false" />
|
||||||
<preference name="DisallowOverscroll" value="true" />
|
<preference name="DisallowOverscroll" value="true" />
|
||||||
<preference name="prerendered-icon" value="true" />
|
<preference name="prerendered-icon" value="true" />
|
||||||
<preference name="AppendUserAgent" value="MoodleMobile 4.0.1 (40100)" />
|
<preference name="AppendUserAgent" value="MoodleMobile 4.1.0-dev (41000)" />
|
||||||
<preference name="BackupWebStorage" value="none" />
|
<preference name="BackupWebStorage" value="none" />
|
||||||
<preference name="ScrollEnabled" value="false" />
|
<preference name="ScrollEnabled" value="false" />
|
||||||
<preference name="KeyboardDisplayRequiresUserAction" value="false" />
|
<preference name="KeyboardDisplayRequiresUserAction" value="false" />
|
||||||
|
@ -251,7 +251,7 @@
|
||||||
<true />
|
<true />
|
||||||
</edit-config>
|
</edit-config>
|
||||||
<edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString">
|
<edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString">
|
||||||
<string>4.0.1</string>
|
<string>4.1.0</string>
|
||||||
</edit-config>
|
</edit-config>
|
||||||
<edit-config file="*-Info.plist" mode="overwrite" target="CFBundleLocalizations">
|
<edit-config file="*-Info.plist" mode="overwrite" target="CFBundleLocalizations">
|
||||||
<array>
|
<array>
|
||||||
|
|
|
@ -71,5 +71,5 @@ gulp.task('watch', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('watch-behat', () => {
|
gulp.task('watch-behat', () => {
|
||||||
gulp.watch(['./tests/behat'], { interval: 500 }, gulp.parallel('behat'));
|
gulp.watch(['./src/**/*.feature', './local-moodleappbehat'], { interval: 500 }, gulp.parallel('behat'));
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
|
@ -0,0 +1,5 @@
|
||||||
|
this.CoreSitesProvider.getSite().then(site => {
|
||||||
|
const username = site.infos.username;
|
||||||
|
|
||||||
|
document.getElementById('username').innerText = `, ${username}`;
|
||||||
|
});
|
|
@ -0,0 +1,807 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->enter_site();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enter site.
|
||||||
|
*/
|
||||||
|
protected function enter_site() {
|
||||||
|
if (!$this->is_in_login_page()) {
|
||||||
|
// Already in the site.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $CFG;
|
||||||
|
|
||||||
|
$this->i_set_the_field_in_the_app('Your site', $CFG->wwwroot);
|
||||||
|
$this->i_press_in_the_app('"Connect to your site"');
|
||||||
|
$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);");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,698 @@
|
||||||
|
<?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;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $lmsversion = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->skip_restricted_tags_scenarios();
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workaround while MDL-74621 is not integrated in all supported versions.
|
||||||
|
* This function will skip scenarios based on @lms_from and @lms_upto tags.
|
||||||
|
*/
|
||||||
|
public function skip_restricted_tags_scenarios() {
|
||||||
|
if (is_null($this->lmsversion)) {
|
||||||
|
global $CFG;
|
||||||
|
|
||||||
|
$version = trim($CFG->release);
|
||||||
|
$versionarr = explode(" ", $version);
|
||||||
|
if (!empty($versionarr)) {
|
||||||
|
$version = $versionarr[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace everything but numbers and dots by dots.
|
||||||
|
$version = preg_replace('/[^\.\d]/', '.', $version);
|
||||||
|
// Combine multiple dots in one.
|
||||||
|
$version = preg_replace('/(\.{2,})/', '.', $version);
|
||||||
|
// Trim possible leading and trailing dots.
|
||||||
|
$this->lmsversion = trim($version, '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->has_version_restrictions()) {
|
||||||
|
// Skip this test.
|
||||||
|
throw new DriverException('Incompatible tags.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets if version is incompatible with the @lms_from and @lms_upto tags.
|
||||||
|
*
|
||||||
|
* @return bool If scenario has any version incompatible tag.
|
||||||
|
*/
|
||||||
|
protected function has_version_restrictions() : bool {
|
||||||
|
$usedtags = behat_hooks::get_tags_for_scenario();
|
||||||
|
|
||||||
|
$detectedversioncount = substr_count($this->lmsversion, '.');
|
||||||
|
|
||||||
|
// Set up relevant tags for each version.
|
||||||
|
$usedtags = array_keys($usedtags);
|
||||||
|
foreach ($usedtags as $usedtag) {
|
||||||
|
if (!preg_match('~^lms_(from|upto)([0-9]+(?:\.[0-9]+)*)$~', $usedtag, $matches)) {
|
||||||
|
// No match, ignore.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$direction = $matches[1];
|
||||||
|
$version = $matches[2];
|
||||||
|
|
||||||
|
$versioncount = substr_count($version, '.');
|
||||||
|
|
||||||
|
// Compare versions on same length.
|
||||||
|
$detected = $this->lmsversion;
|
||||||
|
if ($versioncount < $detectedversioncount) {
|
||||||
|
$detected_parts = explode('.', $this->lmsversion);
|
||||||
|
array_splice($detected_parts, $versioncount - $detectedversioncount);
|
||||||
|
$detected = implode('.', $detected_parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
$compare = version_compare($detected, $version);
|
||||||
|
// Installed version OLDER than the one being considered, so do not
|
||||||
|
// include any scenarios that only run from the considered version up.
|
||||||
|
if ($compare === -1 && $direction === 'from') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Installed version NEWER than the one being considered, so do not
|
||||||
|
// include any scenarios that only run up to that version.
|
||||||
|
if ($compare === 1 && $direction === 'upto') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,8 @@ use Behat\Mink\Exception\DriverException;
|
||||||
use Facebook\WebDriver\Exception\InvalidArgumentException;
|
use Facebook\WebDriver\Exception\InvalidArgumentException;
|
||||||
use Moodle\BehatExtension\Driver\WebDriver;
|
use Moodle\BehatExtension\Driver\WebDriver;
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/../behat_app.php');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performance measures for one particular metric.
|
* Performance measures for one particular metric.
|
||||||
*/
|
*/
|
||||||
|
@ -334,7 +336,7 @@ class performance_measure implements behat_app_listener {
|
||||||
" ],",
|
" ],",
|
||||||
" ],",
|
" ],",
|
||||||
" ],",
|
" ],",
|
||||||
");",
|
"];",
|
||||||
"",
|
"",
|
||||||
])
|
])
|
||||||
);
|
);
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"app_id": "com.moodle.moodlemobile",
|
"app_id": "com.moodle.moodlemobile",
|
||||||
"appname": "Moodle Mobile",
|
"appname": "Moodle Mobile",
|
||||||
"versioncode": 40100,
|
"versioncode": 41000,
|
||||||
"versionname": "4.0.1",
|
"versionname": "4.1.0-dev",
|
||||||
"cache_update_frequency_usually": 420000,
|
"cache_update_frequency_usually": 420000,
|
||||||
"cache_update_frequency_often": 1200000,
|
"cache_update_frequency_often": 1200000,
|
||||||
"cache_update_frequency_sometimes": 3600000,
|
"cache_update_frequency_sometimes": 3600000,
|
||||||
|
@ -76,9 +76,10 @@
|
||||||
"password": "moodle"
|
"password": "moodle"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"defaultZoomLevel": "none",
|
||||||
"zoomlevels": {
|
"zoomlevels": {
|
||||||
"normal": 100,
|
"none": 100,
|
||||||
"low": 110,
|
"medium": 110,
|
||||||
"high": 120
|
"high": 120
|
||||||
},
|
},
|
||||||
"customurlscheme": "moodlemobile",
|
"customurlscheme": "moodlemobile",
|
||||||
|
|
|
@ -1,48 +1,48 @@
|
||||||
{
|
{
|
||||||
"name": "moodlemobile",
|
"name": "moodlemobile",
|
||||||
"version": "4.0.1",
|
"version": "4.1.0-dev",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "moodlemobile",
|
"name": "moodlemobile",
|
||||||
"version": "4.0.1",
|
"version": "4.1.0-dev",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "10.0.14",
|
"@angular/animations": "~10.0.14",
|
||||||
"@angular/common": "10.0.14",
|
"@angular/common": "~10.0.14",
|
||||||
"@angular/core": "10.0.14",
|
"@angular/core": "~10.0.14",
|
||||||
"@angular/forms": "10.0.14",
|
"@angular/forms": "~10.0.14",
|
||||||
"@angular/platform-browser": "10.0.14",
|
"@angular/platform-browser": "~10.0.14",
|
||||||
"@angular/platform-browser-dynamic": "10.0.14",
|
"@angular/platform-browser-dynamic": "~10.0.14",
|
||||||
"@angular/router": "10.0.14",
|
"@angular/router": "~10.0.14",
|
||||||
"@ionic-native/badge": "5.33.0",
|
"@ionic-native/badge": "^5.33.0",
|
||||||
"@ionic-native/camera": "5.33.0",
|
"@ionic-native/camera": "^5.33.0",
|
||||||
"@ionic-native/chooser": "5.33.0",
|
"@ionic-native/chooser": "^5.33.0",
|
||||||
"@ionic-native/clipboard": "5.33.0",
|
"@ionic-native/clipboard": "^5.33.0",
|
||||||
"@ionic-native/core": "5.33.0",
|
"@ionic-native/core": "^5.33.0",
|
||||||
"@ionic-native/device": "5.33.0",
|
"@ionic-native/device": "^5.33.0",
|
||||||
"@ionic-native/diagnostic": "5.33.0",
|
"@ionic-native/diagnostic": "^5.33.0",
|
||||||
"@ionic-native/file": "5.33.0",
|
"@ionic-native/file": "^5.33.0",
|
||||||
"@ionic-native/file-opener": "5.33.0",
|
"@ionic-native/file-opener": "^5.33.0",
|
||||||
"@ionic-native/file-transfer": "5.33.0",
|
"@ionic-native/file-transfer": "^5.33.0",
|
||||||
"@ionic-native/geolocation": "5.33.0",
|
"@ionic-native/geolocation": "^5.33.0",
|
||||||
"@ionic-native/http": "5.33.0",
|
"@ionic-native/http": "^5.33.0",
|
||||||
"@ionic-native/in-app-browser": "5.33.0",
|
"@ionic-native/in-app-browser": "^5.33.0",
|
||||||
"@ionic-native/ionic-webview": "5.33.0",
|
"@ionic-native/ionic-webview": "^5.33.0",
|
||||||
"@ionic-native/keyboard": "5.33.0",
|
"@ionic-native/keyboard": "^5.33.0",
|
||||||
"@ionic-native/local-notifications": "5.33.0",
|
"@ionic-native/local-notifications": "^5.33.0",
|
||||||
"@ionic-native/media": "5.33.0",
|
"@ionic-native/media": "^5.33.0",
|
||||||
"@ionic-native/media-capture": "5.33.0",
|
"@ionic-native/media-capture": "^5.33.0",
|
||||||
"@ionic-native/network": "5.33.0",
|
"@ionic-native/network": "^5.33.0",
|
||||||
"@ionic-native/push": "5.33.0",
|
"@ionic-native/push": "^5.33.0",
|
||||||
"@ionic-native/qr-scanner": "5.33.0",
|
"@ionic-native/qr-scanner": "^5.33.0",
|
||||||
"@ionic-native/splash-screen": "5.33.0",
|
"@ionic-native/splash-screen": "^5.33.0",
|
||||||
"@ionic-native/sqlite": "5.33.0",
|
"@ionic-native/sqlite": "^5.33.0",
|
||||||
"@ionic-native/status-bar": "5.33.0",
|
"@ionic-native/status-bar": "^5.33.0",
|
||||||
"@ionic-native/web-intent": "5.33.0",
|
"@ionic-native/web-intent": "^5.33.0",
|
||||||
"@ionic-native/zip": "5.33.0",
|
"@ionic-native/zip": "^5.33.0",
|
||||||
"@ionic/angular": "5.9.2",
|
"@ionic/angular": "^5.9.2",
|
||||||
"@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5",
|
"@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5",
|
||||||
"@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3",
|
"@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3",
|
||||||
"@moodlehq/cordova-plugin-ionic-webview": "5.0.0-moodle.1",
|
"@moodlehq/cordova-plugin-ionic-webview": "5.0.0-moodle.1",
|
||||||
|
@ -50,106 +50,106 @@
|
||||||
"@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.2",
|
"@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.2",
|
||||||
"@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1",
|
"@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1",
|
||||||
"@moodlehq/phonegap-plugin-push": "2.0.0-moodle.4",
|
"@moodlehq/phonegap-plugin-push": "2.0.0-moodle.4",
|
||||||
"@ngx-translate/core": "13.0.0",
|
"@ngx-translate/core": "^13.0.0",
|
||||||
"@ngx-translate/http-loader": "6.0.0",
|
"@ngx-translate/http-loader": "^6.0.0",
|
||||||
"@types/chart.js": "2.9.31",
|
"@types/chart.js": "^2.9.31",
|
||||||
"@types/cordova": "0.0.34",
|
"@types/cordova": "0.0.34",
|
||||||
"@types/dom-mediacapture-record": "1.0.7",
|
"@types/dom-mediacapture-record": "^1.0.7",
|
||||||
"chart.js": "2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"com-darryncampbell-cordova-plugin-intent": "2.2.0",
|
"com-darryncampbell-cordova-plugin-intent": "^2.2.0",
|
||||||
"cordova": "11.0.0",
|
"cordova": "^11.0.0",
|
||||||
"cordova-android": "10.1.1",
|
"cordova-android": "^10.1.1",
|
||||||
"cordova-clipboard": "1.3.0",
|
"cordova-clipboard": "^1.3.0",
|
||||||
"cordova-ios": "6.2.0",
|
"cordova-ios": "^6.2.0",
|
||||||
"cordova-plugin-add-swift-support": "2.0.2",
|
"cordova-plugin-add-swift-support": "^2.0.2",
|
||||||
"cordova-plugin-advanced-http": "3.2.2",
|
"cordova-plugin-advanced-http": "^3.2.2",
|
||||||
"cordova-plugin-badge": "0.8.8",
|
"cordova-plugin-badge": "^0.8.8",
|
||||||
"cordova-plugin-camera": "6.0.0",
|
"cordova-plugin-camera": "^6.0.0",
|
||||||
"cordova-plugin-chooser": "1.3.2",
|
"cordova-plugin-chooser": "^1.3.2",
|
||||||
"cordova-plugin-customurlscheme": "5.0.2",
|
"cordova-plugin-customurlscheme": "^5.0.2",
|
||||||
"cordova-plugin-device": "2.0.3",
|
"cordova-plugin-device": "^2.0.3",
|
||||||
"cordova-plugin-file": "6.0.2",
|
"cordova-plugin-file": "^6.0.2",
|
||||||
"cordova-plugin-file-opener2": "3.0.5",
|
"cordova-plugin-file-opener2": "^3.0.5",
|
||||||
"cordova-plugin-geolocation": "4.1.0",
|
"cordova-plugin-geolocation": "^4.1.0",
|
||||||
"cordova-plugin-ionic-keyboard": "2.2.0",
|
"cordova-plugin-ionic-keyboard": "^2.2.0",
|
||||||
"cordova-plugin-media": "5.0.4",
|
"cordova-plugin-media": "^5.0.4",
|
||||||
"cordova-plugin-media-capture": "3.0.3",
|
"cordova-plugin-media-capture": "^3.0.3",
|
||||||
"cordova-plugin-network-information": "3.0.0",
|
"cordova-plugin-network-information": "^3.0.0",
|
||||||
"cordova-plugin-prevent-override": "1.0.1",
|
"cordova-plugin-prevent-override": "^1.0.1",
|
||||||
"cordova-plugin-splashscreen": "6.0.0",
|
"cordova-plugin-splashscreen": "^6.0.0",
|
||||||
"cordova-plugin-statusbar": "3.0.0",
|
"cordova-plugin-statusbar": "^3.0.0",
|
||||||
"cordova-plugin-wkuserscript": "1.0.1",
|
"cordova-plugin-wkuserscript": "^1.0.1",
|
||||||
"cordova-plugin-wkwebview-cookies": "1.0.1",
|
"cordova-plugin-wkwebview-cookies": "^1.0.1",
|
||||||
"cordova-sqlite-storage": "6.0.0",
|
"cordova-sqlite-storage": "^6.0.0",
|
||||||
"cordova.plugins.diagnostic": "6.1.1",
|
"cordova.plugins.diagnostic": "^6.1.1",
|
||||||
"core-js": "3.9.1",
|
"core-js": "^3.9.1",
|
||||||
"es6-promise-plugin": "4.2.2",
|
"es6-promise-plugin": "^4.2.2",
|
||||||
"hammerjs": "2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"jszip": "3.7.1",
|
"jszip": "^3.7.1",
|
||||||
"mathjax": "2.7.7",
|
"mathjax": "2.7.7",
|
||||||
"moment": "2.29.2",
|
"moment": "^2.29.2",
|
||||||
"nl.kingsquare.cordova.background-audio": "1.0.1",
|
"nl.kingsquare.cordova.background-audio": "^1.0.1",
|
||||||
"rxjs": "6.5.5",
|
"rxjs": "~6.5.5",
|
||||||
"ts-md5": "1.2.7",
|
"ts-md5": "^1.2.7",
|
||||||
"tslib": "2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"zone.js": "0.10.3"
|
"zone.js": "~0.10.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "10.0.1",
|
"@angular-builders/custom-webpack": "^10.0.1",
|
||||||
"@angular-devkit/architect": "0.1202.7",
|
"@angular-devkit/architect": "^0.1202.7",
|
||||||
"@angular-devkit/build-angular": "0.1000.8",
|
"@angular-devkit/build-angular": "~0.1000.8",
|
||||||
"@angular-eslint/builder": "4.2.0",
|
"@angular-eslint/builder": "^4.2.0",
|
||||||
"@angular-eslint/eslint-plugin": "4.2.0",
|
"@angular-eslint/eslint-plugin": "^4.2.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "4.2.0",
|
"@angular-eslint/eslint-plugin-template": "^4.2.0",
|
||||||
"@angular-eslint/schematics": "4.2.0",
|
"@angular-eslint/schematics": "^4.2.0",
|
||||||
"@angular-eslint/template-parser": "4.2.0",
|
"@angular-eslint/template-parser": "^4.2.0",
|
||||||
"@angular/cli": "10.0.8",
|
"@angular/cli": "~10.0.8",
|
||||||
"@angular/compiler": "10.0.14",
|
"@angular/compiler": "~10.0.14",
|
||||||
"@angular/compiler-cli": "10.0.14",
|
"@angular/compiler-cli": "~10.0.14",
|
||||||
"@angular/language-service": "10.0.14",
|
"@angular/language-service": "~10.0.14",
|
||||||
"@ionic/angular-toolkit": "2.3.3",
|
"@ionic/angular-toolkit": "^2.3.3",
|
||||||
"@ionic/cli": "6.19.0",
|
"@ionic/cli": "^6.19.0",
|
||||||
"@types/faker": "5.1.3",
|
"@types/faker": "^5.1.3",
|
||||||
"@types/node": "12.12.64",
|
"@types/node": "^12.12.64",
|
||||||
"@types/resize-observer-browser": "0.1.5",
|
"@types/resize-observer-browser": "^0.1.5",
|
||||||
"@types/webpack-env": "1.16.0",
|
"@types/webpack-env": "^1.16.0",
|
||||||
"@typescript-eslint/eslint-plugin": "4.22.0",
|
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||||
"@typescript-eslint/parser": "4.22.0",
|
"@typescript-eslint/parser": "^4.22.0",
|
||||||
"check-es-compat": "1.1.1",
|
"check-es-compat": "^1.1.1",
|
||||||
"cordova-plugin-androidx-adapter": "1.1.3",
|
"cordova-plugin-androidx-adapter": "^1.1.3",
|
||||||
"cordova-plugin-screen-orientation": "^3.0.2",
|
"cordova-plugin-screen-orientation": "^3.0.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "7.25.0",
|
"eslint": "^7.25.0",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-header": "3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
"eslint-plugin-import": "2.22.1",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
"eslint-plugin-jest": "24.3.6",
|
"eslint-plugin-jest": "^24.3.6",
|
||||||
"eslint-plugin-jsdoc": "32.3.3",
|
"eslint-plugin-jsdoc": "^32.3.3",
|
||||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||||
"eslint-plugin-promise": "5.1.0",
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
"faker": "5.1.0",
|
"faker": "^5.1.0",
|
||||||
"fs-extra": "9.1.0",
|
"fs-extra": "^9.1.0",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"gulp-clip-empty-files": "0.1.2",
|
"gulp-clip-empty-files": "^0.1.2",
|
||||||
"gulp-concat": "2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-flatten": "0.4.0",
|
"gulp-flatten": "^0.4.0",
|
||||||
"gulp-htmlmin": "5.0.1",
|
"gulp-htmlmin": "^5.0.1",
|
||||||
"gulp-rename": "2.0.0",
|
"gulp-rename": "^2.0.0",
|
||||||
"gulp-slash": "1.1.3",
|
"gulp-slash": "^1.1.3",
|
||||||
"jest": "26.5.2",
|
"jest": "^26.5.2",
|
||||||
"jest-preset-angular": "8.3.1",
|
"jest-preset-angular": "^8.3.1",
|
||||||
"jsonc-parser": "2.3.1",
|
"jsonc-parser": "^2.3.1",
|
||||||
"native-run": "1.4.0",
|
"native-run": "^1.4.0",
|
||||||
"terser-webpack-plugin": "4.2.3",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"ts-jest": "26.4.1",
|
"ts-jest": "^26.4.1",
|
||||||
"ts-node": "8.3.0",
|
"ts-node": "~8.3.0",
|
||||||
"typescript": "3.9.9"
|
"typescript": "^3.9.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.15.0 <15"
|
"node": ">=14.15.0 <15"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"keytar": "7.2.0"
|
"keytar": "^7.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular-builders/custom-webpack": {
|
"node_modules/@angular-builders/custom-webpack": {
|
||||||
|
|
248
package.json
248
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "moodlemobile",
|
"name": "moodlemobile",
|
||||||
"version": "4.0.1",
|
"version": "4.1.0-dev",
|
||||||
"description": "The official app for Moodle.",
|
"description": "The official app for Moodle.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Moodle Pty Ltd.",
|
"name": "Moodle Pty Ltd.",
|
||||||
|
@ -38,40 +38,40 @@
|
||||||
"ionic:build:before": "gulp"
|
"ionic:build:before": "gulp"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "10.0.14",
|
"@angular/animations": "~10.0.14",
|
||||||
"@angular/common": "10.0.14",
|
"@angular/common": "~10.0.14",
|
||||||
"@angular/core": "10.0.14",
|
"@angular/core": "~10.0.14",
|
||||||
"@angular/forms": "10.0.14",
|
"@angular/forms": "~10.0.14",
|
||||||
"@angular/platform-browser": "10.0.14",
|
"@angular/platform-browser": "~10.0.14",
|
||||||
"@angular/platform-browser-dynamic": "10.0.14",
|
"@angular/platform-browser-dynamic": "~10.0.14",
|
||||||
"@angular/router": "10.0.14",
|
"@angular/router": "~10.0.14",
|
||||||
"@ionic-native/badge": "5.33.0",
|
"@ionic-native/badge": "^5.33.0",
|
||||||
"@ionic-native/camera": "5.33.0",
|
"@ionic-native/camera": "^5.33.0",
|
||||||
"@ionic-native/chooser": "5.33.0",
|
"@ionic-native/chooser": "^5.33.0",
|
||||||
"@ionic-native/clipboard": "5.33.0",
|
"@ionic-native/clipboard": "^5.33.0",
|
||||||
"@ionic-native/core": "5.33.0",
|
"@ionic-native/core": "^5.33.0",
|
||||||
"@ionic-native/device": "5.33.0",
|
"@ionic-native/device": "^5.33.0",
|
||||||
"@ionic-native/diagnostic": "5.33.0",
|
"@ionic-native/diagnostic": "^5.33.0",
|
||||||
"@ionic-native/file": "5.33.0",
|
"@ionic-native/file": "^5.33.0",
|
||||||
"@ionic-native/file-opener": "5.33.0",
|
"@ionic-native/file-opener": "^5.33.0",
|
||||||
"@ionic-native/file-transfer": "5.33.0",
|
"@ionic-native/file-transfer": "^5.33.0",
|
||||||
"@ionic-native/geolocation": "5.33.0",
|
"@ionic-native/geolocation": "^5.33.0",
|
||||||
"@ionic-native/http": "5.33.0",
|
"@ionic-native/http": "^5.33.0",
|
||||||
"@ionic-native/in-app-browser": "5.33.0",
|
"@ionic-native/in-app-browser": "^5.33.0",
|
||||||
"@ionic-native/ionic-webview": "5.33.0",
|
"@ionic-native/ionic-webview": "^5.33.0",
|
||||||
"@ionic-native/keyboard": "5.33.0",
|
"@ionic-native/keyboard": "^5.33.0",
|
||||||
"@ionic-native/local-notifications": "5.33.0",
|
"@ionic-native/local-notifications": "^5.33.0",
|
||||||
"@ionic-native/media": "5.33.0",
|
"@ionic-native/media": "^5.33.0",
|
||||||
"@ionic-native/media-capture": "5.33.0",
|
"@ionic-native/media-capture": "^5.33.0",
|
||||||
"@ionic-native/network": "5.33.0",
|
"@ionic-native/network": "^5.33.0",
|
||||||
"@ionic-native/push": "5.33.0",
|
"@ionic-native/push": "^5.33.0",
|
||||||
"@ionic-native/qr-scanner": "5.33.0",
|
"@ionic-native/qr-scanner": "^5.33.0",
|
||||||
"@ionic-native/splash-screen": "5.33.0",
|
"@ionic-native/splash-screen": "^5.33.0",
|
||||||
"@ionic-native/sqlite": "5.33.0",
|
"@ionic-native/sqlite": "^5.33.0",
|
||||||
"@ionic-native/status-bar": "5.33.0",
|
"@ionic-native/status-bar": "^5.33.0",
|
||||||
"@ionic-native/web-intent": "5.33.0",
|
"@ionic-native/web-intent": "^5.33.0",
|
||||||
"@ionic-native/zip": "5.33.0",
|
"@ionic-native/zip": "^5.33.0",
|
||||||
"@ionic/angular": "5.9.2",
|
"@ionic/angular": "^5.9.2",
|
||||||
"@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5",
|
"@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5",
|
||||||
"@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3",
|
"@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3",
|
||||||
"@moodlehq/cordova-plugin-ionic-webview": "5.0.0-moodle.1",
|
"@moodlehq/cordova-plugin-ionic-webview": "5.0.0-moodle.1",
|
||||||
|
@ -79,100 +79,100 @@
|
||||||
"@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.2",
|
"@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.2",
|
||||||
"@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1",
|
"@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1",
|
||||||
"@moodlehq/phonegap-plugin-push": "2.0.0-moodle.4",
|
"@moodlehq/phonegap-plugin-push": "2.0.0-moodle.4",
|
||||||
"@ngx-translate/core": "13.0.0",
|
"@ngx-translate/core": "^13.0.0",
|
||||||
"@ngx-translate/http-loader": "6.0.0",
|
"@ngx-translate/http-loader": "^6.0.0",
|
||||||
"@types/chart.js": "2.9.31",
|
"@types/chart.js": "^2.9.31",
|
||||||
"@types/cordova": "0.0.34",
|
"@types/cordova": "0.0.34",
|
||||||
"@types/dom-mediacapture-record": "1.0.7",
|
"@types/dom-mediacapture-record": "^1.0.7",
|
||||||
"chart.js": "2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"com-darryncampbell-cordova-plugin-intent": "2.2.0",
|
"com-darryncampbell-cordova-plugin-intent": "^2.2.0",
|
||||||
"cordova": "11.0.0",
|
"cordova": "^11.0.0",
|
||||||
"cordova-android": "10.1.1",
|
"cordova-android": "^10.1.1",
|
||||||
"cordova-clipboard": "1.3.0",
|
"cordova-clipboard": "^1.3.0",
|
||||||
"cordova-ios": "6.2.0",
|
"cordova-ios": "^6.2.0",
|
||||||
"cordova-plugin-add-swift-support": "2.0.2",
|
"cordova-plugin-add-swift-support": "^2.0.2",
|
||||||
"cordova-plugin-advanced-http": "3.2.2",
|
"cordova-plugin-advanced-http": "^3.2.2",
|
||||||
"cordova-plugin-badge": "0.8.8",
|
"cordova-plugin-badge": "^0.8.8",
|
||||||
"cordova-plugin-camera": "6.0.0",
|
"cordova-plugin-camera": "^6.0.0",
|
||||||
"cordova-plugin-chooser": "1.3.2",
|
"cordova-plugin-chooser": "^1.3.2",
|
||||||
"cordova-plugin-customurlscheme": "5.0.2",
|
"cordova-plugin-customurlscheme": "^5.0.2",
|
||||||
"cordova-plugin-device": "2.0.3",
|
"cordova-plugin-device": "^2.0.3",
|
||||||
"cordova-plugin-file": "6.0.2",
|
"cordova-plugin-file": "^6.0.2",
|
||||||
"cordova-plugin-file-opener2": "3.0.5",
|
"cordova-plugin-file-opener2": "^3.0.5",
|
||||||
"cordova-plugin-geolocation": "4.1.0",
|
"cordova-plugin-geolocation": "^4.1.0",
|
||||||
"cordova-plugin-ionic-keyboard": "2.2.0",
|
"cordova-plugin-ionic-keyboard": "^2.2.0",
|
||||||
"cordova-plugin-media": "5.0.4",
|
"cordova-plugin-media": "^5.0.4",
|
||||||
"cordova-plugin-media-capture": "3.0.3",
|
"cordova-plugin-media-capture": "^3.0.3",
|
||||||
"cordova-plugin-network-information": "3.0.0",
|
"cordova-plugin-network-information": "^3.0.0",
|
||||||
"cordova-plugin-prevent-override": "1.0.1",
|
"cordova-plugin-prevent-override": "^1.0.1",
|
||||||
"cordova-plugin-splashscreen": "6.0.0",
|
"cordova-plugin-splashscreen": "^6.0.0",
|
||||||
"cordova-plugin-statusbar": "3.0.0",
|
"cordova-plugin-statusbar": "^3.0.0",
|
||||||
"cordova-plugin-wkuserscript": "1.0.1",
|
"cordova-plugin-wkuserscript": "^1.0.1",
|
||||||
"cordova-plugin-wkwebview-cookies": "1.0.1",
|
"cordova-plugin-wkwebview-cookies": "^1.0.1",
|
||||||
"cordova-sqlite-storage": "6.0.0",
|
"cordova-sqlite-storage": "^6.0.0",
|
||||||
"cordova.plugins.diagnostic": "6.1.1",
|
"cordova.plugins.diagnostic": "^6.1.1",
|
||||||
"core-js": "3.9.1",
|
"core-js": "^3.9.1",
|
||||||
"es6-promise-plugin": "4.2.2",
|
"es6-promise-plugin": "^4.2.2",
|
||||||
"hammerjs": "2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"jszip": "3.7.1",
|
"jszip": "^3.7.1",
|
||||||
"mathjax": "2.7.7",
|
"mathjax": "2.7.7",
|
||||||
"moment": "2.29.2",
|
"moment": "^2.29.2",
|
||||||
"nl.kingsquare.cordova.background-audio": "1.0.1",
|
"nl.kingsquare.cordova.background-audio": "^1.0.1",
|
||||||
"rxjs": "6.5.5",
|
"rxjs": "~6.5.5",
|
||||||
"ts-md5": "1.2.7",
|
"ts-md5": "^1.2.7",
|
||||||
"tslib": "2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"zone.js": "0.10.3"
|
"zone.js": "~0.10.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "10.0.1",
|
"@angular-builders/custom-webpack": "^10.0.1",
|
||||||
"@angular-devkit/architect": "0.1202.7",
|
"@angular-devkit/architect": "^0.1202.7",
|
||||||
"@angular-devkit/build-angular": "0.1000.8",
|
"@angular-devkit/build-angular": "~0.1000.8",
|
||||||
"@angular-eslint/builder": "4.2.0",
|
"@angular-eslint/builder": "^4.2.0",
|
||||||
"@angular-eslint/eslint-plugin": "4.2.0",
|
"@angular-eslint/eslint-plugin": "^4.2.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "4.2.0",
|
"@angular-eslint/eslint-plugin-template": "^4.2.0",
|
||||||
"@angular-eslint/schematics": "4.2.0",
|
"@angular-eslint/schematics": "^4.2.0",
|
||||||
"@angular-eslint/template-parser": "4.2.0",
|
"@angular-eslint/template-parser": "^4.2.0",
|
||||||
"@angular/cli": "10.0.8",
|
"@angular/cli": "~10.0.8",
|
||||||
"@angular/compiler": "10.0.14",
|
"@angular/compiler": "~10.0.14",
|
||||||
"@angular/compiler-cli": "10.0.14",
|
"@angular/compiler-cli": "~10.0.14",
|
||||||
"@angular/language-service": "10.0.14",
|
"@angular/language-service": "~10.0.14",
|
||||||
"@ionic/angular-toolkit": "2.3.3",
|
"@ionic/angular-toolkit": "^2.3.3",
|
||||||
"@ionic/cli": "6.19.0",
|
"@ionic/cli": "^6.19.0",
|
||||||
"@types/faker": "5.1.3",
|
"@types/faker": "^5.1.3",
|
||||||
"@types/node": "12.12.64",
|
"@types/node": "^12.12.64",
|
||||||
"@types/resize-observer-browser": "0.1.5",
|
"@types/resize-observer-browser": "^0.1.5",
|
||||||
"@types/webpack-env": "1.16.0",
|
"@types/webpack-env": "^1.16.0",
|
||||||
"@typescript-eslint/eslint-plugin": "4.22.0",
|
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||||
"@typescript-eslint/parser": "4.22.0",
|
"@typescript-eslint/parser": "^4.22.0",
|
||||||
"check-es-compat": "1.1.1",
|
"check-es-compat": "^1.1.1",
|
||||||
"cordova-plugin-androidx-adapter": "1.1.3",
|
"cordova-plugin-androidx-adapter": "^1.1.3",
|
||||||
"cordova-plugin-screen-orientation": "^3.0.2",
|
"cordova-plugin-screen-orientation": "^3.0.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "7.25.0",
|
"eslint": "^7.25.0",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-header": "3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
"eslint-plugin-import": "2.22.1",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
"eslint-plugin-jest": "24.3.6",
|
"eslint-plugin-jest": "^24.3.6",
|
||||||
"eslint-plugin-jsdoc": "32.3.3",
|
"eslint-plugin-jsdoc": "^32.3.3",
|
||||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||||
"eslint-plugin-promise": "5.1.0",
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
"faker": "5.1.0",
|
"faker": "^5.1.0",
|
||||||
"fs-extra": "9.1.0",
|
"fs-extra": "^9.1.0",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"gulp-clip-empty-files": "0.1.2",
|
"gulp-clip-empty-files": "^0.1.2",
|
||||||
"gulp-concat": "2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"gulp-flatten": "0.4.0",
|
"gulp-flatten": "^0.4.0",
|
||||||
"gulp-htmlmin": "5.0.1",
|
"gulp-htmlmin": "^5.0.1",
|
||||||
"gulp-rename": "2.0.0",
|
"gulp-rename": "^2.0.0",
|
||||||
"gulp-slash": "1.1.3",
|
"gulp-slash": "^1.1.3",
|
||||||
"jest": "26.5.2",
|
"jest": "^26.5.2",
|
||||||
"jest-preset-angular": "8.3.1",
|
"jest-preset-angular": "^8.3.1",
|
||||||
"jsonc-parser": "2.3.1",
|
"jsonc-parser": "^2.3.1",
|
||||||
"native-run": "1.4.0",
|
"native-run": "^1.4.0",
|
||||||
"terser-webpack-plugin": "4.2.3",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"ts-jest": "26.4.1",
|
"ts-jest": "^26.4.1",
|
||||||
"ts-node": "8.3.0",
|
"ts-node": "~8.3.0",
|
||||||
"typescript": "3.9.9"
|
"typescript": "^3.9.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.15.0 <15"
|
"node": ">=14.15.0 <15"
|
||||||
|
@ -243,6 +243,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"keytar": "7.2.0"
|
"keytar": "^7.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,37 +14,82 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
const { existsSync, readFileSync, writeFileSync } = require('fs');
|
const { existsSync, readFileSync, writeFileSync, statSync, renameSync, rmSync } = require('fs');
|
||||||
const { readdir } = require('fs').promises;
|
const { readdir } = require('fs').promises;
|
||||||
const { mkdirSync, copySync } = require('fs-extra');
|
const { mkdirSync, copySync } = require('fs-extra');
|
||||||
const { resolve } = require('path');
|
const { resolve, extname, dirname, basename, relative } = require('path');
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const pluginPath = process.argv[2] || guessPluginPath() || fail('Folder argument missing!');
|
const pluginPath = process.argv[2] || guessPluginPath() || fail('Folder argument missing!');
|
||||||
|
|
||||||
if (!existsSync(pluginPath)) {
|
if (!existsSync(pluginPath)) {
|
||||||
mkdirSync(pluginPath);
|
mkdirSync(pluginPath);
|
||||||
|
} else {
|
||||||
|
// Empty directory, except the excluding list.
|
||||||
|
const excludeFromErase = [
|
||||||
|
'.git',
|
||||||
|
'.gitignore',
|
||||||
|
'README.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
const files = await readdir(pluginPath, { withFileTypes: true });
|
||||||
|
for (const file of files) {
|
||||||
|
if (excludeFromErase.indexOf(file.name) >= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = resolve(pluginPath, file.name);
|
||||||
|
rmSync(`${path}`, {recursive: true});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Copy plugin template.
|
// Copy plugin template.
|
||||||
const { version: appVersion } = require(projectPath('package.json'));
|
const { version: appVersion } = require(projectPath('package.json'));
|
||||||
const templatePath = projectPath('scripts/templates/behat-plugin');
|
const templatePath = projectPath('local-moodleappbehat');
|
||||||
|
|
||||||
|
|
||||||
|
copySync(templatePath, pluginPath);
|
||||||
|
|
||||||
|
// Update version.php
|
||||||
|
const pluginFilePath = pluginPath + '/version.php';
|
||||||
|
const fileContents = readFileSync(pluginFilePath).toString();
|
||||||
|
|
||||||
const replacements = {
|
const replacements = {
|
||||||
appVersion,
|
appVersion,
|
||||||
pluginVersion: getMoodlePluginVersion(),
|
pluginVersion: getMoodlePluginVersion(),
|
||||||
};
|
};
|
||||||
|
writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements));
|
||||||
|
|
||||||
copySync(templatePath, pluginPath);
|
// Copy feature files.
|
||||||
|
const behatTempFeaturesPath = `${pluginPath}/behat-tmp`;
|
||||||
|
copySync(projectPath('src'), behatTempFeaturesPath, { filter: isFeatureFileOrDirectory });
|
||||||
|
|
||||||
for await (const templateFilePath of getDirectoryFiles(templatePath)) {
|
const behatFeaturesPath = `${pluginPath}/tests/behat`;
|
||||||
const pluginFilePath = pluginPath + templateFilePath.substr(templatePath.length);
|
if (!existsSync(behatFeaturesPath)) {
|
||||||
const fileContents = readFileSync(pluginFilePath).toString();
|
mkdirSync(behatFeaturesPath, {recursive: true});
|
||||||
|
|
||||||
writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy features.
|
for await (const featureFile of getDirectoryFiles(behatTempFeaturesPath)) {
|
||||||
copySync(projectPath('tests/behat'), `${pluginPath}/tests/behat`);
|
const featurePath = dirname(featureFile);
|
||||||
|
if (!featurePath.endsWith('/tests/behat')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPath = featurePath.substring(0, featurePath.length - ('/tests/behat'.length));
|
||||||
|
const searchRegExp = new RegExp('/', 'g');
|
||||||
|
const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core';
|
||||||
|
const featureFilename = prefix + '-' + basename(featureFile);
|
||||||
|
renameSync(featureFile, behatFeaturesPath + '/' + featureFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
rmSync(behatTempFeaturesPath, {recursive: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFeatureFileOrDirectory(src) {
|
||||||
|
const stats = statSync(src);
|
||||||
|
|
||||||
|
return stats.isDirectory() || extname(src) === '.feature';
|
||||||
}
|
}
|
||||||
|
|
||||||
function fail(message) {
|
function fail(message) {
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -0,0 +1,71 @@
|
||||||
|
@mod @mod_quiz @app @javascript
|
||||||
|
Feature: Attempt a quiz in app
|
||||||
|
As a student
|
||||||
|
In order to demonstrate what I know
|
||||||
|
I need to be able to attempt quizzes
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "courses" exist:
|
||||||
|
| fullname | shortname |
|
||||||
|
| Course 1 | C1 |
|
||||||
|
And the following "users" exist:
|
||||||
|
| username |
|
||||||
|
| student1 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber |
|
||||||
|
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
|
||||||
|
And the following "question categories" exist:
|
||||||
|
| contextlevel | reference | name |
|
||||||
|
| Course | C1 | Test questions |
|
||||||
|
And the following "questions" exist:
|
||||||
|
| questioncategory | qtype | name | questiontext |
|
||||||
|
| Test questions | truefalse | TF1 | Text of the first question |
|
||||||
|
| Test questions | truefalse | TF2 | Text of the second question |
|
||||||
|
And quiz "Quiz 1" contains the following questions:
|
||||||
|
| question | page |
|
||||||
|
| TF1 | 1 |
|
||||||
|
| TF2 | 2 |
|
||||||
|
|
||||||
|
Scenario: Next and previous navigation
|
||||||
|
Given I entered the quiz activity "Quiz 1" on course "Course 1" as "student1" in the app
|
||||||
|
And I press "Attempt quiz now" in the app
|
||||||
|
Then I should find "Text of the first question" in the app
|
||||||
|
But I should not find "Text of the second question" in the app
|
||||||
|
|
||||||
|
When I press "Next" near "Question 1" in the app
|
||||||
|
Then I should find "Text of the second question" in the app
|
||||||
|
But I should not find "Text of the first question" in the app
|
||||||
|
|
||||||
|
When I press "Previous" near "Question 2" in the app
|
||||||
|
Then I should find "Text of the first question" in the app
|
||||||
|
But I should not find "Text of the second question" in the app
|
||||||
|
|
||||||
|
When I press "Next" near "Quiz 1" in the app
|
||||||
|
Then I should find "Text of the second question" in the app
|
||||||
|
But I should not find "Text of the first question" in the app
|
||||||
|
|
||||||
|
When I press "Previous" near "Quiz 1" in the app
|
||||||
|
Then I should find "Text of the first question" in the app
|
||||||
|
But I should not find "Text of the second question" in the app
|
||||||
|
|
||||||
|
When I press "Next" near "Question 1" in the app
|
||||||
|
And I press "Submit" in the app
|
||||||
|
Then I should find "Summary of attempt" in the app
|
||||||
|
|
||||||
|
When I press "Not yet answered" within "2" "ion-item" in the app
|
||||||
|
Then I should find "Text of the second question" in the app
|
||||||
|
But I should not find "Text of the first question" in the app
|
||||||
|
|
||||||
|
When I press "Submit" in the app
|
||||||
|
And I press "Submit all and finish" in the app
|
||||||
|
Then I should find "Once you submit" in the app
|
||||||
|
|
||||||
|
When I press "Cancel" near "Once you submit" in the app
|
||||||
|
Then I should find "Summary of attempt" in the app
|
||||||
|
|
||||||
|
When I press "Submit all and finish" in the app
|
||||||
|
And I press "OK" near "Once you submit" in the app
|
||||||
|
Then I should find "Review" in the app
|
|
@ -32,6 +32,7 @@ import { JitCompilerFactory } from '@angular/platform-browser-dynamic';
|
||||||
import { CoreCronDelegate } from '@services/cron';
|
import { CoreCronDelegate } from '@services/cron';
|
||||||
import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron';
|
import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron';
|
||||||
import { moodleTransitionAnimation } from '@classes/page-transition';
|
import { moodleTransitionAnimation } from '@classes/page-transition';
|
||||||
|
import { BehatTestingModule } from '@/testing/behat-testing.module';
|
||||||
|
|
||||||
// For translate loader. AoT requires an exported function for factories.
|
// For translate loader. AoT requires an exported function for factories.
|
||||||
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
||||||
|
@ -59,6 +60,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
CoreModule,
|
CoreModule,
|
||||||
AddonsModule,
|
AddonsModule,
|
||||||
|
BehatTestingModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||||
|
|
|
@ -208,10 +208,17 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||||
await content.componentOnReady();
|
await content.componentOnReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
parentPage = parentPage.parentElement.closest('.ion-page');
|
parentPage = parentPage.parentElement.closest('.ion-page, .ion-page-hidden, .ion-page-invisible');
|
||||||
|
|
||||||
// Check if the page has a header. If it doesn't, search the next parent page.
|
// Check if the page has a header. If it doesn't, search the next parent page.
|
||||||
const header = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
let header = parentPage?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
||||||
|
|
||||||
|
if (header && getComputedStyle(header).display !== 'none') {
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find using content if any.
|
||||||
|
header = content?.parentElement?.querySelector<HTMLIonHeaderElement>(':scope > ion-header');
|
||||||
|
|
||||||
if (header && getComputedStyle(header).display !== 'none') {
|
if (header && getComputedStyle(header).display !== 'none') {
|
||||||
return header;
|
return header;
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,496 @@
|
||||||
|
@core @core_course @app @javascript
|
||||||
|
Feature: Test basic usage of one course in app
|
||||||
|
In order to participate in one course while using the mobile app
|
||||||
|
As a student
|
||||||
|
I need basic course functionality to work
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname | email |
|
||||||
|
| teacher1 | Teacher | teacher | teacher1@example.com |
|
||||||
|
| student1 | Student | student | student1@example.com |
|
||||||
|
| student2 | Student2 | student2 | student2@example.com |
|
||||||
|
And the following "courses" exist:
|
||||||
|
| fullname | shortname | category |
|
||||||
|
| Course 1 | C1 | 0 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| teacher1 | C1 | editingteacher |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | option | section |
|
||||||
|
| choice | Choice course 1 | Test choice description | C1 | choice1 | Option 1, Option 2, Option 3 | 1 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | course | idnumber | name | intro | assignsubmission_onlinetext_enabled | section |
|
||||||
|
| assign | C1 | assign1 | assignment | Test assignment description | 1 | 1 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | groupmode | assessed | scale[modgrade_type] |
|
||||||
|
| forum | Test forum name | Test forum | C1 | forum | 0 | 5 | Point |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | groupmode | section |
|
||||||
|
| chat | Test chat name | Test chat | C1 | chat | 0 | 2 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | section |
|
||||||
|
| data | Web links | Useful links | C1 | data1 | 4 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | groupmode | section |
|
||||||
|
| lti | Test external name | Test external | C1 | external | 0 | 1 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | groupmode | section |
|
||||||
|
| feedback | Test feedback name | Test feedback | C1 | feedback | 0 | 3 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | section |
|
||||||
|
| glossary | Test glossary | glossary description | C1 | gloss1 | 5 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | section |
|
||||||
|
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 2 |
|
||||||
|
And the following "question categories" exist:
|
||||||
|
| contextlevel | reference | name |
|
||||||
|
| Course | C1 | Test questions |
|
||||||
|
And the following "questions" exist:
|
||||||
|
| questioncategory | qtype | name | questiontext |
|
||||||
|
| Test questions | truefalse | TF1 | Text of the first question |
|
||||||
|
| Test questions | truefalse | TF2 | Text of the second question |
|
||||||
|
And quiz "Quiz 1" contains the following questions:
|
||||||
|
| question | page |
|
||||||
|
| TF1 | 1 |
|
||||||
|
| TF2 | 2 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | groupmode | section |
|
||||||
|
| survey | Test survey name | Test survey | C1 | survey | 0 | 1 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | groupmode |
|
||||||
|
| wiki | Test wiki name | Test wiki | C1 | wiki | 0 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | groupmode | section |
|
||||||
|
| lesson | Test lesson name | Test lesson | C1 | lesson | 0 | 3 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | groupmode | section |
|
||||||
|
| scorm | Test scorm name | Test scorm | C1 | scorm | 0 | 2 |
|
||||||
|
And the following "activities" exist:
|
||||||
|
| activity | name | intro | course | idnumber | groupmode | section |
|
||||||
|
| workshop | Test workshop name | Test workshop | C1 | workshop | 0 | 3 |
|
||||||
|
|
||||||
|
Scenario: View course contents
|
||||||
|
When I entered the course "Course 1" as "student1" in the app
|
||||||
|
Then the header should be "Course 1" in the app
|
||||||
|
And I should find "Choice course 1" in the app
|
||||||
|
And I should find "assignment" in the app
|
||||||
|
And I should find "Test forum name" in the app
|
||||||
|
And I should find "Test chat name" in the app
|
||||||
|
And I should find "Web links" in the app
|
||||||
|
And I should find "Test external name" in the app
|
||||||
|
And I should find "Test feedback name" in the app
|
||||||
|
And I should find "Test glossary" in the app
|
||||||
|
And I should find "Quiz 1" in the app
|
||||||
|
And I should find "Test survey name" in the app
|
||||||
|
And I should find "Test wiki name" in the app
|
||||||
|
And I should find "Test lesson name" in the app
|
||||||
|
And I should find "Test scorm name" in the app
|
||||||
|
And I should find "Test workshop name" in the app
|
||||||
|
|
||||||
|
When I press "Choice course 1" in the app
|
||||||
|
Then the header should be "Choice course 1" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "assignment" in the app
|
||||||
|
Then the header should be "assignment" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test forum name" in the app
|
||||||
|
Then the header should be "Test forum name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test chat name" in the app
|
||||||
|
Then the header should be "Test chat name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Web links" in the app
|
||||||
|
Then the header should be "Web links" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test external name" in the app
|
||||||
|
Then the header should be "Test external name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test feedback name" in the app
|
||||||
|
And I press "OK" in the app
|
||||||
|
Then the header should be "Test feedback name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test glossary" in the app
|
||||||
|
Then the header should be "Test glossary" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Quiz 1" in the app
|
||||||
|
Then the header should be "Quiz 1" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test survey name" in the app
|
||||||
|
Then the header should be "Test survey name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test wiki name" in the app
|
||||||
|
And I press "OK" in the app
|
||||||
|
Then the header should be "Test wiki name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test lesson name" in the app
|
||||||
|
Then the header should be "Test lesson name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test scorm name" in the app
|
||||||
|
Then the header should be "Test scorm name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test workshop name" in the app
|
||||||
|
Then the header should be "Test workshop name" in the app
|
||||||
|
|
||||||
|
Scenario: View section contents
|
||||||
|
When I entered the course "Course 1" as "student1" in the app
|
||||||
|
Then the header should be "Course 1" in the app
|
||||||
|
And I should find "Choice course 1" in the app
|
||||||
|
And I should find "assignment" in the app
|
||||||
|
And I should find "Test forum name" in the app
|
||||||
|
And I should find "Test chat name" in the app
|
||||||
|
And I should find "Web links" in the app
|
||||||
|
And I should find "Test external name" in the app
|
||||||
|
And I should find "Test feedback name" in the app
|
||||||
|
And I should find "Test glossary" in the app
|
||||||
|
And I should find "Quiz 1" in the app
|
||||||
|
And I should find "Test survey name" in the app
|
||||||
|
And I should find "Test wiki name" in the app
|
||||||
|
And I should find "Test lesson name" in the app
|
||||||
|
And I should find "Test scorm name" in the app
|
||||||
|
And I should find "Test workshop name" in the app
|
||||||
|
|
||||||
|
When I press "Course index" in the app
|
||||||
|
And I press "General" in the app
|
||||||
|
Then I should find "Test forum name" in the app
|
||||||
|
And I should find "Test wiki name" in the app
|
||||||
|
But I should not find "Choice course 1" in the app
|
||||||
|
And I should not find "assignment" in the app
|
||||||
|
And I should not find "Test chat name" in the app
|
||||||
|
And I should not find "Web links" in the app
|
||||||
|
And I should not find "Test external name" in the app
|
||||||
|
And I should not find "Test feedback name" in the app
|
||||||
|
And I should not find "Test glossary" in the app
|
||||||
|
And I should not find "Quiz 1" in the app
|
||||||
|
And I should not find "Test survey name" in the app
|
||||||
|
And I should not find "Test lesson name" in the app
|
||||||
|
And I should not find "Test scorm name" in the app
|
||||||
|
And I should not find "Test workshop name" in the app
|
||||||
|
|
||||||
|
When I press "Test forum name" in the app
|
||||||
|
Then the header should be "Test forum name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test wiki name" in the app
|
||||||
|
And I press "OK" in the app
|
||||||
|
Then the header should be "Test wiki name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Course index" in the app
|
||||||
|
And I press "Topic 1" in the app
|
||||||
|
Then I should find "Choice course 1" in the app
|
||||||
|
And I should find "assignment" in the app
|
||||||
|
And I should find "Test external name" in the app
|
||||||
|
And I should find "Test survey name" in the app
|
||||||
|
But I should not find "Test forum name" in the app
|
||||||
|
And I should not find "Test chat name" in the app
|
||||||
|
And I should not find "Web links" in the app
|
||||||
|
And I should not find "Test feedback name" in the app
|
||||||
|
And I should not find "Test glossary" in the app
|
||||||
|
And I should not find "Quiz 1" in the app
|
||||||
|
And I should not find "Test wiki name" in the app
|
||||||
|
And I should not find "Test lesson name" in the app
|
||||||
|
And I should not find "Test scorm name" in the app
|
||||||
|
And I should not find "Test workshop name" in the app
|
||||||
|
|
||||||
|
When I press "Choice course 1" in the app
|
||||||
|
Then the header should be "Choice course 1" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "assignment" in the app
|
||||||
|
Then the header should be "assignment" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test external name" in the app
|
||||||
|
Then the header should be "Test external name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test survey name" in the app
|
||||||
|
Then the header should be "Test survey name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Course index" in the app
|
||||||
|
And I press "Topic 2" in the app
|
||||||
|
Then I should find "Quiz 1" in the app
|
||||||
|
And I should find "Test chat name" in the app
|
||||||
|
And I should find "Test scorm name" in the app
|
||||||
|
But I should not find "Choice course 1" in the app
|
||||||
|
And I should not find "assignment" in the app
|
||||||
|
And I should not find "Test forum name" in the app
|
||||||
|
And I should not find "Web links" in the app
|
||||||
|
And I should not find "Test external name" in the app
|
||||||
|
And I should not find "Test feedback name" in the app
|
||||||
|
And I should not find "Test glossary" in the app
|
||||||
|
And I should not find "Test survey name" in the app
|
||||||
|
And I should not find "Test wiki name" in the app
|
||||||
|
And I should not find "Test lesson name" in the app
|
||||||
|
And I should not find "Test workshop name" in the app
|
||||||
|
|
||||||
|
When I press "Test chat name" in the app
|
||||||
|
Then the header should be "Test chat name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Quiz 1" in the app
|
||||||
|
Then the header should be "Quiz 1" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test scorm name" in the app
|
||||||
|
Then the header should be "Test scorm name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Course index" in the app
|
||||||
|
And I press "Topic 3" in the app
|
||||||
|
Then I should find "Test feedback name" in the app
|
||||||
|
And I should find "Test lesson name" in the app
|
||||||
|
And I should find "Test workshop name" in the app
|
||||||
|
But I should not find "Choice course 1" in the app
|
||||||
|
And I should not find "assignment" in the app
|
||||||
|
And I should not find "Test forum name" in the app
|
||||||
|
And I should not find "Test chat name" in the app
|
||||||
|
And I should not find "Web links" in the app
|
||||||
|
And I should not find "Test external name" in the app
|
||||||
|
And I should not find "Test glossary" in the app
|
||||||
|
And I should not find "Quiz 1" in the app
|
||||||
|
And I should not find "Test survey name" in the app
|
||||||
|
And I should not find "Test wiki name" in the app
|
||||||
|
And I should not find "Test scorm name" in the app
|
||||||
|
|
||||||
|
When I press "Test feedback name" in the app
|
||||||
|
And I press "OK" in the app
|
||||||
|
Then the header should be "Test feedback name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test lesson name" in the app
|
||||||
|
Then the header should be "Test lesson name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Test workshop name" in the app
|
||||||
|
Then the header should be "Test workshop name" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Course index" in the app
|
||||||
|
And I press "Topic 4" in the app
|
||||||
|
Then I should find "Web links" in the app
|
||||||
|
But I should not find "Choice course 1" in the app
|
||||||
|
And I should not find "assignment" in the app
|
||||||
|
And I should not find "Test forum name" in the app
|
||||||
|
And I should not find "Test chat name" in the app
|
||||||
|
And I should not find "Test external name" in the app
|
||||||
|
And I should not find "Test feedback name" in the app
|
||||||
|
And I should not find "Test glossary" in the app
|
||||||
|
And I should not find "Quiz 1" in the app
|
||||||
|
And I should not find "Test survey name" in the app
|
||||||
|
And I should not find "Test wiki name" in the app
|
||||||
|
And I should not find "Test lesson name" in the app
|
||||||
|
And I should not find "Test scorm name" in the app
|
||||||
|
And I should not find "Test workshop name" in the app
|
||||||
|
|
||||||
|
When I press "Web links" in the app
|
||||||
|
Then the header should be "Web links" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
And I press "Course index" in the app
|
||||||
|
And I press "Topic 5" in the app
|
||||||
|
Then I should find "Test glossary" in the app
|
||||||
|
But I should not find "Choice course 1" in the app
|
||||||
|
And I should not find "assignment" in the app
|
||||||
|
And I should not find "Test forum name" in the app
|
||||||
|
And I should not find "Test chat name" in the app
|
||||||
|
And I should not find "Web links" in the app
|
||||||
|
And I should not find "Test external name" in the app
|
||||||
|
And I should not find "Test feedback name" in the app
|
||||||
|
And I should not find "Quiz 1" in the app
|
||||||
|
And I should not find "Test survey name" in the app
|
||||||
|
And I should not find "Test wiki name" in the app
|
||||||
|
And I should not find "Test lesson name" in the app
|
||||||
|
And I should not find "Test scorm name" in the app
|
||||||
|
And I should not find "Test workshop name" in the app
|
||||||
|
|
||||||
|
When I press "Test glossary" in the app
|
||||||
|
Then the header should be "Test glossary" in the app
|
||||||
|
|
||||||
|
Scenario: Navigation between sections using the bottom arrows
|
||||||
|
When I entered the course "Course 1" as "student1" in the app
|
||||||
|
Then the header should be "Course 1" in the app
|
||||||
|
And I should find "Choice course 1" in the app
|
||||||
|
And I should find "assignment" in the app
|
||||||
|
And I should find "Test forum name" in the app
|
||||||
|
And I should find "Test chat name" in the app
|
||||||
|
And I should find "Web links" in the app
|
||||||
|
And I should find "Test external name" in the app
|
||||||
|
And I should find "Test feedback name" in the app
|
||||||
|
And I should find "Test glossary" in the app
|
||||||
|
And I should find "Quiz 1" in the app
|
||||||
|
And I should find "Test survey name" in the app
|
||||||
|
And I should find "Test wiki name" in the app
|
||||||
|
And I should find "Test lesson name" in the app
|
||||||
|
And I should find "Test scorm name" in the app
|
||||||
|
And I should find "Test workshop name" in the app
|
||||||
|
|
||||||
|
When I press "Course index" in the app
|
||||||
|
And I press "General" in the app
|
||||||
|
Then I should find "General" in the app
|
||||||
|
And I should find "Next: Topic 1" in the app
|
||||||
|
But I should not find "Topic 2" in the app
|
||||||
|
And I should not find "Topic 3" in the app
|
||||||
|
And I should not find "Topic 4" in the app
|
||||||
|
And I should not find "Topic 5" in the app
|
||||||
|
And I should not find "Previous:" in the app
|
||||||
|
|
||||||
|
When I press "Next:" in the app
|
||||||
|
Then I should find "Topic 1" in the app
|
||||||
|
And I should find "Previous: General" in the app
|
||||||
|
And I should find "Next: Topic 2" in the app
|
||||||
|
But I should not find "Topic 3" in the app
|
||||||
|
And I should not find "Topic 4" in the app
|
||||||
|
And I should not find "Topic 5" in the app
|
||||||
|
|
||||||
|
When I press "Next:" in the app
|
||||||
|
Then I should find "Topic 2" in the app
|
||||||
|
And I should find "Previous: Topic 1" in the app
|
||||||
|
And I should find "Next: Topic 3" in the app
|
||||||
|
But I should not find "General" in the app
|
||||||
|
And I should not find "Topic 4" in the app
|
||||||
|
And I should not find "Topic 5" in the app
|
||||||
|
|
||||||
|
When I press "Next:" in the app
|
||||||
|
Then I should find "Topic 3" in the app
|
||||||
|
And I should find "Previous: Topic 2" in the app
|
||||||
|
And I should find "Next: Topic 4" in the app
|
||||||
|
But I should not find "General" in the app
|
||||||
|
And I should not find "Topic 1" in the app
|
||||||
|
And I should not find "Topic 5" in the app
|
||||||
|
|
||||||
|
When I press "Next:" in the app
|
||||||
|
Then I should find "Topic 4" in the app
|
||||||
|
And I should find "Previous: Topic 3" in the app
|
||||||
|
And I should find "Next: Topic 5" in the app
|
||||||
|
But I should not find "General" in the app
|
||||||
|
And I should not find "Topic 1" in the app
|
||||||
|
And I should not find "Topic 2" in the app
|
||||||
|
|
||||||
|
When I press "Next:" in the app
|
||||||
|
Then I should find "Topic 5" in the app
|
||||||
|
And I should find "Previous: Topic 4" in the app
|
||||||
|
But I should not find "General" in the app
|
||||||
|
And I should not find "Topic 1" in the app
|
||||||
|
And I should not find "Topic 2" in the app
|
||||||
|
And I should not find "Topic 3" in the app
|
||||||
|
And I should not find "Next:" in the app
|
||||||
|
|
||||||
|
When I press "Previous:" in the app
|
||||||
|
Then I should find "Topic 4" in the app
|
||||||
|
And I should find "Previous: Topic 3" in the app
|
||||||
|
And I should find "Next: Topic 5" in the app
|
||||||
|
But I should not find "General" in the app
|
||||||
|
And I should not find "Topic 1" in the app
|
||||||
|
And I should not find "Topic 2" in the app
|
||||||
|
|
||||||
|
@lms_from4.0
|
||||||
|
Scenario: Self enrol
|
||||||
|
Given I entered the course "Course 1" as "teacher1" in the app
|
||||||
|
And I press "Course summary" in the app
|
||||||
|
And I press "Open in browser" in the app
|
||||||
|
And I switch to the browser tab opened by the app
|
||||||
|
And I log in as "teacher1"
|
||||||
|
And I click on "Participants" "link"
|
||||||
|
And I select "Enrolment methods" from the "jump" singleselect
|
||||||
|
And I click on "Enable" "icon" in the "Self enrolment (Student)" "table_row"
|
||||||
|
And I close the browser tab opened by the app
|
||||||
|
Given I entered the app as "student2"
|
||||||
|
When I press "Site home" in the app
|
||||||
|
And I press "Available courses" in the app
|
||||||
|
And I press "Course 1" in the app
|
||||||
|
And I press "Enrol me" in the app
|
||||||
|
And I press "OK" in the app
|
||||||
|
And I wait loading to finish in the app
|
||||||
|
Then the header should be "Course 1" in the app
|
||||||
|
And I should find "Choice course 1" in the app
|
||||||
|
And I should find "assignment" in the app
|
||||||
|
And I should find "Test forum name" in the app
|
||||||
|
And I should find "Test chat name" in the app
|
||||||
|
And I should find "Web links" in the app
|
||||||
|
And I should find "Test external name" in the app
|
||||||
|
And I should find "Test feedback name" in the app
|
||||||
|
And I should find "Test glossary" in the app
|
||||||
|
And I should find "Quiz 1" in the app
|
||||||
|
And I should find "Test survey name" in the app
|
||||||
|
And I should find "Test wiki name" in the app
|
||||||
|
And I should find "Test lesson name" in the app
|
||||||
|
And I should find "Test scorm name" in the app
|
||||||
|
And I should find "Test workshop name" in the app
|
||||||
|
|
||||||
|
@lms_from4.0
|
||||||
|
Scenario: Guest access
|
||||||
|
Given I entered the course "Course 1" as "teacher1" in the app
|
||||||
|
And I press "Course summary" in the app
|
||||||
|
And I press "Open in browser" in the app
|
||||||
|
And I switch to the browser tab opened by the app
|
||||||
|
And I log in as "teacher1"
|
||||||
|
And I click on "Participants" "link"
|
||||||
|
And I select "Enrolment methods" from the "jump" singleselect
|
||||||
|
And I click on "Enable" "icon" in the "Guest access" "table_row"
|
||||||
|
And I close the browser tab opened by the app
|
||||||
|
Given I entered the app as "student2"
|
||||||
|
When I press "Site home" in the app
|
||||||
|
And I press "Available courses" in the app
|
||||||
|
And I press "Course 1" in the app
|
||||||
|
|
||||||
|
Then I should find "Course summary" in the app
|
||||||
|
And I should find "Course" in the app
|
||||||
|
|
||||||
|
When I press "View course" "ion-button" in the app
|
||||||
|
Then the header should be "Course 1" in the app
|
||||||
|
And I should find "Choice course 1" in the app
|
||||||
|
And I should find "assignment" in the app
|
||||||
|
And I should find "Test forum name" in the app
|
||||||
|
And I should find "Test chat name" in the app
|
||||||
|
And I should find "Web links" in the app
|
||||||
|
And I should find "Test feedback name" in the app
|
||||||
|
And I should find "Test glossary" in the app
|
||||||
|
And I should find "Quiz 1" in the app
|
||||||
|
And I should find "Test survey name" in the app
|
||||||
|
And I should find "Test wiki name" in the app
|
||||||
|
And I should find "Test lesson name" in the app
|
||||||
|
And I should find "Test scorm name" in the app
|
||||||
|
And I should find "Test workshop name" in the app
|
||||||
|
|
||||||
|
Scenario: View blocks on drawer
|
||||||
|
Given the following "blocks" exist:
|
||||||
|
| blockname | contextlevel | reference | pagetypepattern | defaultregion | configdata |
|
||||||
|
| html | Course | C1 | course-view-* | site-pre | Tzo4OiJzdGRDbGFzcyI6Mjp7czo1OiJ0aXRsZSI7czoxNToiSFRNTCB0aXRsZSB0ZXN0IjtzOjQ6InRleHQiO3M6OToiYm9keSB0ZXN0Ijt9 |
|
||||||
|
| activity_modules | Course | C1 | course-view-* | site-pre | |
|
||||||
|
And I entered the course "Course 1" as "student1" in the app
|
||||||
|
Then the header should be "Course 1" in the app
|
||||||
|
And I should find "Choice course 1" in the app
|
||||||
|
And I should find "assignment" in the app
|
||||||
|
And I should find "Test forum name" in the app
|
||||||
|
And I should find "Test chat name" in the app
|
||||||
|
And I should find "Web links" in the app
|
||||||
|
And I should find "Test external name" in the app
|
||||||
|
And I should find "Test feedback name" in the app
|
||||||
|
And I should find "Test glossary" in the app
|
||||||
|
And I should find "Quiz 1" in the app
|
||||||
|
And I should find "Test survey name" in the app
|
||||||
|
And I should find "Test wiki name" in the app
|
||||||
|
And I should find "Test lesson name" in the app
|
||||||
|
And I should find "Test scorm name" in the app
|
||||||
|
And I should find "Test workshop name" in the app
|
||||||
|
Then I press "Open block drawer" in the app
|
||||||
|
And I should find "HTML title test" in the app
|
||||||
|
And I should find "body test" in the app
|
||||||
|
And I should find "Activities" in the app
|
|
@ -0,0 +1,34 @@
|
||||||
|
@core @core_course @app @javascript @lms_upto3.10
|
||||||
|
Feature: Check course completion feature.
|
||||||
|
In order to track the progress of the course on mobile device
|
||||||
|
As a student
|
||||||
|
I need to be able to update the activity completion status.
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname | email |
|
||||||
|
| student1 | Student | 1 | student1@example.com |
|
||||||
|
And the following "courses" exist:
|
||||||
|
| fullname | shortname | category | enablecompletion |
|
||||||
|
| Course 1 | C1 | 0 | 1 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
|
||||||
|
Scenario: Activity completion, marking the checkbox manually
|
||||||
|
Given the following "activities" exist:
|
||||||
|
| activity | name | course | idnumber | completion | completionview |
|
||||||
|
| forum | First forum | C1 | forum1 | 1 | 0 |
|
||||||
|
| forum | Second forum | C1 | forum2 | 1 | 0 |
|
||||||
|
And I entered the course "Course 1" as "student1" in the app
|
||||||
|
# Set activities as completed.
|
||||||
|
Then I should find "0%" in the app
|
||||||
|
And I click on "ion-button[title=\"Not completed: First forum. Select to mark as complete.\"]" "css"
|
||||||
|
And I should find "50%" in the app
|
||||||
|
And I click on "ion-button[title=\"Not completed: Second forum. Select to mark as complete.\"]" "css"
|
||||||
|
And I should find "100%" in the app
|
||||||
|
# Set activities as not completed.
|
||||||
|
And I click on "ion-button[title=\"Completed: First forum. Select to mark as not complete.\"]" "css"
|
||||||
|
And I should find "50%" in the app
|
||||||
|
And I click on "ion-button[title=\"Completed: Second forum. Select to mark as not complete.\"]" "css"
|
||||||
|
And I should find "0%" in the app
|
|
@ -0,0 +1,35 @@
|
||||||
|
@core @core_course @app @javascript
|
||||||
|
Feature: Check course completion feature.
|
||||||
|
In order to track the progress of the course on mobile device
|
||||||
|
As a student
|
||||||
|
I need to be able to update the activity completion status.
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname | email |
|
||||||
|
| student1 | Student | 1 | student1@example.com |
|
||||||
|
And the following "courses" exist:
|
||||||
|
| fullname | shortname | category | enablecompletion |
|
||||||
|
| Course 1 | C1 | 0 | 1 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
|
||||||
|
@lms_from3.11
|
||||||
|
Scenario: Activity completion, marking the checkbox manually
|
||||||
|
Given the following "activities" exist:
|
||||||
|
| activity | name | course | idnumber | completion | completionview |
|
||||||
|
| forum | First forum | C1 | forum1 | 1 | 0 |
|
||||||
|
| forum | Second forum | C1 | forum2 | 1 | 0 |
|
||||||
|
And I entered the course "Course 1" as "student1" in the app
|
||||||
|
# Set activities as completed.
|
||||||
|
Then I should find "0%" in the app
|
||||||
|
And I press "Mark First forum as done" in the app
|
||||||
|
And I should find "50%" in the app
|
||||||
|
And I press "Mark Second forum as done" in the app
|
||||||
|
And I should find "100%" in the app
|
||||||
|
# Set activities as not completed.
|
||||||
|
And I press "First forum is marked as done. Press to undo." in the app
|
||||||
|
And I should find "50%" in the app
|
||||||
|
And I press "Second forum is marked as done. Press to undo." in the app
|
||||||
|
And I should find "0%" in the app
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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"
|
|
@ -41,7 +41,7 @@ export class CoreSettingsGeneralPage {
|
||||||
languages: { code: string; name: string }[] = [];
|
languages: { code: string; name: string }[] = [];
|
||||||
selectedLanguage = '';
|
selectedLanguage = '';
|
||||||
zoomLevels: { value: CoreZoomLevel; style: number; selected: boolean }[] = [];
|
zoomLevels: { value: CoreZoomLevel; style: number; selected: boolean }[] = [];
|
||||||
selectedZoomLevel = CoreZoomLevel.NORMAL;
|
selectedZoomLevel = CoreZoomLevel.NONE;
|
||||||
richTextEditor = true;
|
richTextEditor = true;
|
||||||
debugDisplay = false;
|
debugDisplay = false;
|
||||||
analyticsSupported = false;
|
analyticsSupported = false;
|
||||||
|
|
|
@ -50,8 +50,8 @@ export const enum CoreColorScheme {
|
||||||
* Constants to define zoom levels.
|
* Constants to define zoom levels.
|
||||||
*/
|
*/
|
||||||
export const enum CoreZoomLevel {
|
export const enum CoreZoomLevel {
|
||||||
NORMAL = 'normal',
|
NONE = 'none',
|
||||||
LOW = 'low',
|
MEDIUM = 'medium',
|
||||||
HIGH = 'high',
|
HIGH = 'high',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,13 +303,13 @@ export class CoreSettingsHelperProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the value to solve edge cases.
|
// Reset the value to solve edge cases.
|
||||||
CoreConfig.set(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.NORMAL);
|
CoreConfig.set(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.NONE);
|
||||||
|
|
||||||
if (fontSize < 100) {
|
if (fontSize < 100) {
|
||||||
if (fontSize > 90) {
|
if (fontSize > 90) {
|
||||||
CoreConfig.set(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.HIGH);
|
CoreConfig.set(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.HIGH);
|
||||||
} else if (fontSize > 70) {
|
} else if (fontSize > 70) {
|
||||||
CoreConfig.set(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.LOW);
|
CoreConfig.set(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.MEDIUM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,7 +326,7 @@ export class CoreSettingsHelperProvider {
|
||||||
* @return The saved zoom Level option.
|
* @return The saved zoom Level option.
|
||||||
*/
|
*/
|
||||||
async getZoomLevel(): Promise<CoreZoomLevel> {
|
async getZoomLevel(): Promise<CoreZoomLevel> {
|
||||||
return CoreConfig.get(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.NORMAL);
|
return CoreConfig.get(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreConstants.CONFIG.defaultZoomLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -23,6 +23,7 @@ import { CoreLoginHelper } from '@features/login/services/login-helper';
|
||||||
import { CoreSites } from './sites';
|
import { CoreSites } from './sites';
|
||||||
import { CoreUtils, PromiseDefer } from './utils/utils';
|
import { CoreUtils, PromiseDefer } from './utils/utils';
|
||||||
import { CoreApp } from './app';
|
import { CoreApp } from './app';
|
||||||
|
import { CoreZoomLevel } from '@features/settings/services/settings-helper';
|
||||||
|
|
||||||
const VERSION_APPLIED = 'version_applied';
|
const VERSION_APPLIED = 'version_applied';
|
||||||
|
|
||||||
|
@ -71,6 +72,10 @@ export class CoreUpdateManagerProvider {
|
||||||
promises.push(CoreH5P.h5pPlayer.deleteAllContentIndexes());
|
promises.push(CoreH5P.h5pPlayer.deleteAllContentIndexes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (versionCode >= 41000 && versionApplied < 41000 && versionApplied > 0) {
|
||||||
|
promises.push(this.upgradeFontSizeNames());
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
@ -121,6 +126,19 @@ export class CoreUpdateManagerProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async upgradeFontSizeNames(): Promise<void> {
|
||||||
|
const storedFontSizeName = await CoreConfig.get(CoreConstants.SETTINGS_ZOOM_LEVEL);
|
||||||
|
switch (storedFontSizeName) {
|
||||||
|
case 'low':
|
||||||
|
await CoreConfig.set(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.NONE);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'normal':
|
||||||
|
await CoreConfig.set(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.MEDIUM);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CoreUpdateManager = makeSingleton(CoreUpdateManagerProvider);
|
export const CoreUpdateManager = makeSingleton(CoreUpdateManagerProvider);
|
||||||
|
|
|
@ -108,7 +108,7 @@ export class CoreCustomURLSchemesProvider {
|
||||||
|
|
||||||
// Some platforms like Windows add a slash at the end. Remove it.
|
// Some platforms like Windows add a slash at the end. Remove it.
|
||||||
// Some sites add a # at the end of the URL. If it's there, remove it.
|
// Some sites add a # at the end of the URL. If it's there, remove it.
|
||||||
url = url.replace(/\/?#?\/?$/, '');
|
url = url.replace(/\/?(#.*)?\/?$/, '');
|
||||||
|
|
||||||
const modal = await CoreDomUtils.showModalLoading();
|
const modal = await CoreDomUtils.showModalLoading();
|
||||||
let data: CoreCustomURLSchemesParams;
|
let data: CoreCustomURLSchemesParams;
|
||||||
|
|
|
@ -1792,12 +1792,12 @@ export class CoreDomUtilsProvider {
|
||||||
|
|
||||||
const { waitForDismissCompleted, ...popoverOptions } = options;
|
const { waitForDismissCompleted, ...popoverOptions } = options;
|
||||||
const popover = await PopoverController.create(popoverOptions);
|
const popover = await PopoverController.create(popoverOptions);
|
||||||
const zoomLevel = await CoreConfig.get(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreZoomLevel.NORMAL);
|
const zoomLevel = await CoreConfig.get(CoreConstants.SETTINGS_ZOOM_LEVEL, CoreConstants.CONFIG.defaultZoomLevel);
|
||||||
|
|
||||||
await popover.present();
|
await popover.present();
|
||||||
|
|
||||||
// Fix popover position if zoom is applied.
|
// Fix popover position if zoom is applied.
|
||||||
if (zoomLevel !== CoreZoomLevel.NORMAL) {
|
if (zoomLevel !== CoreZoomLevel.NONE) {
|
||||||
switch (getMode()) {
|
switch (getMode()) {
|
||||||
case 'ios':
|
case 'ios':
|
||||||
fixIOSPopoverPosition(popover, options.event);
|
fixIOSPopoverPosition(popover, options.event);
|
||||||
|
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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);
|
|
@ -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;
|
||||||
|
};
|
|
@ -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>;
|
||||||
|
};
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -0,0 +1,109 @@
|
||||||
|
@app @javascript
|
||||||
|
Feature: It navigates properly in pages with a split-view component.
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username |
|
||||||
|
| student1 |
|
||||||
|
And the following "courses" exist:
|
||||||
|
| fullname | shortname |
|
||||||
|
| Course 2 | C2 |
|
||||||
|
| Course 1 | C1 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
| student1 | C2 | student |
|
||||||
|
And the following "grade categories" exist:
|
||||||
|
| fullname | course |
|
||||||
|
| Grade category C1 | C1 |
|
||||||
|
| Grade category C2 | C2 |
|
||||||
|
And the following "grade items" exist:
|
||||||
|
| gradecategory | itemname | grademin | grademax | course |
|
||||||
|
| Grade category C1 | Grade item C1 | 20 | 40 | C1 |
|
||||||
|
| Grade category C2 | Grade item C2 | 60 | 80 | C2 |
|
||||||
|
|
||||||
|
Scenario: Navigate in grades tab on mobile
|
||||||
|
|
||||||
|
# Open user menu
|
||||||
|
Given I entered the app as "student1"
|
||||||
|
And I press the user menu button in the app
|
||||||
|
|
||||||
|
# Open grades page
|
||||||
|
When I press "Grades" in the app
|
||||||
|
Then the header should be "Grades" in the app
|
||||||
|
And I should find "Course 1" in the app
|
||||||
|
And I should find "Course 2" in the app
|
||||||
|
|
||||||
|
# Open C1 course grades
|
||||||
|
When I press "Course 1" in the app
|
||||||
|
Then the header should be "Course 1" in the app
|
||||||
|
And I should find "Grade category C1" in the app
|
||||||
|
|
||||||
|
# Open C1 grade item
|
||||||
|
When I press "Grade item C1" in the app
|
||||||
|
Then I should find "20" near "Range" in the app
|
||||||
|
And I should find "40" near "Range" in the app
|
||||||
|
|
||||||
|
# Go back to grades page
|
||||||
|
When I press the back button in the app
|
||||||
|
Then the header should be "Grades" in the app
|
||||||
|
And I should find "Course 1" in the app
|
||||||
|
And I should find "Course 2" in the app
|
||||||
|
|
||||||
|
# Open C2 course grades
|
||||||
|
When I press "Course 2" in the app
|
||||||
|
Then the header should be "Course 2" in the app
|
||||||
|
And I should find "Grade category C2" in the app
|
||||||
|
|
||||||
|
# Open C2 grade item
|
||||||
|
When I press "Grade item C2" in the app
|
||||||
|
Then I should find "60" near "Range" in the app
|
||||||
|
And I should find "80" near "Range" in the app
|
||||||
|
|
||||||
|
# Go back to grades page
|
||||||
|
When I press the back button in the app
|
||||||
|
Then the header should be "Grades" in the app
|
||||||
|
And I should find "Course 1" in the app
|
||||||
|
And I should find "Course 2" in the app
|
||||||
|
|
||||||
|
# Go back to main page
|
||||||
|
When I press the back button in the app
|
||||||
|
Then I should find "Acceptance test site" in the app
|
||||||
|
And I should find "User account" in the app
|
||||||
|
But I should not find "Back" in the app
|
||||||
|
|
||||||
|
Scenario: Navigate in grades tab on tablet
|
||||||
|
|
||||||
|
# Open user menu
|
||||||
|
Given I entered the app as "student1"
|
||||||
|
And I change viewport size to "1200x640"
|
||||||
|
And I press the user menu button in the app
|
||||||
|
|
||||||
|
# Open grades page
|
||||||
|
When I press "Grades" in the app
|
||||||
|
Then the header should be "Grades" in the app
|
||||||
|
And I should find "Course 1" in the app
|
||||||
|
And I should find "Course 2" in the app
|
||||||
|
And I should find "Grade category C1" in the app
|
||||||
|
|
||||||
|
# Open C1 course grades
|
||||||
|
When I press "Grade item C1" in the app
|
||||||
|
Then I should find "Grade category C1" in the app
|
||||||
|
And I should find "20" near "Range" in the app
|
||||||
|
And I should find "40" near "Range" in the app
|
||||||
|
|
||||||
|
# Select C2 course
|
||||||
|
When I press "Course 2" in the app
|
||||||
|
Then "Course 2" should be selected in the app
|
||||||
|
And I should find "Grade category C2" in the app
|
||||||
|
|
||||||
|
# Open C2 course grades
|
||||||
|
When I press "Grade item C2" in the app
|
||||||
|
Then I should find "60" near "Range" in the app
|
||||||
|
And I should find "80" near "Range" in the app
|
||||||
|
|
||||||
|
# Go back to main page
|
||||||
|
When I press the back button in the app
|
||||||
|
Then I should find "Acceptance test site" in the app
|
||||||
|
And I should find "User account" in the app
|
||||||
|
But I should not find "Back" in the app
|
|
@ -81,13 +81,7 @@ Feature: Measure performance.
|
||||||
Then "Login" should have taken less than 10 seconds
|
Then "Login" should have taken less than 10 seconds
|
||||||
|
|
||||||
Scenario: Open Activity
|
Scenario: Open Activity
|
||||||
When I launch the app
|
Given I entered the app as "student1"
|
||||||
Then I should see "Connect to Moodle"
|
|
||||||
But I should not see "Welcome to the Moodle App!"
|
|
||||||
|
|
||||||
And I set the field "Your site" to "$WWWROOT" in the app
|
|
||||||
And I press "Connect to your site" in the app
|
|
||||||
And I log in as "student1"
|
|
||||||
Then I press "My courses" in the app
|
Then I press "My courses" in the app
|
||||||
And I should find "Course 1" in the app
|
And I should find "Course 1" in the app
|
||||||
|
|
|
@ -39,6 +39,7 @@ export interface EnvironmentConfig {
|
||||||
wsservice: string;
|
wsservice: string;
|
||||||
demo_sites: Record<string, CoreSitesDemoSiteData>;
|
demo_sites: Record<string, CoreSitesDemoSiteData>;
|
||||||
zoomlevels: Record<CoreZoomLevel, number>;
|
zoomlevels: Record<CoreZoomLevel, number>;
|
||||||
|
defaultZoomLevel?: CoreZoomLevel; // Set the default zoom level of the app.
|
||||||
customurlscheme: string;
|
customurlscheme: string;
|
||||||
siteurl: string;
|
siteurl: string;
|
||||||
sitename: string;
|
sitename: string;
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
This files describes API changes in the Moodle Mobile app,
|
This files describes API changes in the Moodle Mobile app,
|
||||||
information provided here is intended especially for developers.
|
information provided here is intended especially for developers.
|
||||||
|
|
||||||
|
=== 4.0.1 ===
|
||||||
|
|
||||||
|
- Zoom levels changed from "normal / low / high" to " none / medium / high".
|
||||||
|
|
||||||
=== 4.0.0 ===
|
=== 4.0.0 ===
|
||||||
|
|
||||||
- The versioncode in moodle.config.json has changed from 4 digits to 5 digits to match the actual value for the stores: the 4.0.0 version's versioncode is now 40000 instead of 4000. If you maintain a Moodle plugin with mobile support and you use the versioncode that is sent in every request, you might need to check if this change will affect your code.
|
- The versioncode in moodle.config.json has changed from 4 digits to 5 digits to match the actual value for the stores: the 4.0.0 version's versioncode is now 40000 instead of 4000. If you maintain a Moodle plugin with mobile support and you use the versioncode that is sent in every request, you might need to check if this change will affect your code.
|
||||||
|
|
Loading…
Reference in New Issue