Merge pull request #3914 from NoelDeMartin/MOBILE-4496

MOBILE-4496: Improve Behat CI
main
Dani Palou 2024-01-31 15:05:53 +01:00 committed by GitHub
commit c60f792927
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 482 additions and 951 deletions

View File

@ -5,8 +5,6 @@ on:
inputs: inputs:
behat_tags: behat_tags:
description: 'Behat tags to execute' description: 'Behat tags to execute'
required: true
default: '~@performance'
moodle_branch: moodle_branch:
description: 'Moodle branch' description: 'Moodle branch'
required: true required: true
@ -14,76 +12,217 @@ on:
moodle_repository: moodle_repository:
description: 'Moodle repository' description: 'Moodle repository'
required: true required: true
default: 'https://github.com/moodle/moodle' default: 'https://github.com/moodle/moodle.git'
pull_request: pull_request:
branches: [ main, v*.x ] branches: [ main, v*.x ]
jobs: jobs:
behat:
build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: outputs:
MOODLE_DOCKER_DB: pgsql tags: ${{ steps.set-tags.outputs.tags }}
MOODLE_DOCKER_BROWSER: chrome
MOODLE_DOCKER_PHP_VERSION: '8.1'
MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'main' }}
MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }}
BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
path: app
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: 'app/.nvmrc'
- name: Additional checkouts
run: | - name: Install npm dependencies
git clone --branch $MOODLE_BRANCH --depth 1 $MOODLE_REPOSITORY $GITHUB_WORKSPACE/moodle working-directory: app
git clone --branch main --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
- name: Install npm packages
run: npm ci --no-audit run: npm ci --no-audit
- name: Create Behat faildumps folder
- name: Build app
working-directory: app
run: npm run build:test
- name: Build Behat plugin
working-directory: app
run: ./scripts/build-behat-plugin.js ../plugin
- name: Prepare Behat tags
id: set-tags
run: | run: |
mkdir moodle/behatfaildumps if [ -z $BEHAT_TAGS ]; then
chmod 777 moodle/behatfaildumps tags=(
"@addon_block_timeline"
"@addon_calendar"
"@addon_competency"
"@addon_messages"
"@addon_mod_assign"
"@addon_mod_bigbluebuttonbn"
"@addon_mod_book"
"@addon_mod_chat"
"@addon_mod_choice"
"@addon_mod_data"
"@addon_mod_feedback"
"@addon_mod_forum"
"@addon_mod_glossary"
"@addon_mod_lesson"
"@addon_mod_quiz"
"@addon_mod_scorm"
"@addon_mod_survey"
"@addon_mod_workshop"
"@addon_notifications"
"@core"
"@core_comments"
"@core_course"
"@core_courses"
"@core_grades"
"@core_login"
"@core_mainmenu"
"@core_reminders"
"@core_reportbuilder"
"@core_search"
"@core_settings"
"@core_siteplugins"
"@core_user"
"@core_usertour"
)
tags_json="["
for tag in "${tags[@]}"; do
tags_json+="\"$tag\","
done
tags_json="${tags_json%?}"
tags_json+="]"
echo "tags=$tags_json" >> $GITHUB_OUTPUT;
else
echo "tags=[\"$BEHAT_TAGS\"]" >> $GITHUB_OUTPUT;
fi
env:
BEHAT_TAGS: ${{ github.event.inputs.behat_tags }}
# We need to upload an artifact so that the download-artifact action
# in the "complete" job does not fail if no other artifacts were uploaded.
- name: Create build logs
run: touch logs.txt
- name: Upload build logs
uses: actions/upload-artifact@v4
with:
name: build
path: logs.txt
- uses: actions/cache/save@v4
with:
key: build-${{ github.sha }}
path: |
app/node_modules/**/*
app/www/**/*
plugin/**/*
behat:
runs-on: ubuntu-latest
needs: build
continue-on-error: true
strategy:
matrix:
tags: ${{ fromJSON(needs.build.outputs.tags) }}
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: 'postgres'
POSTGRES_HOST_AUTH_METHOD: 'trust'
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3
steps:
- uses: actions/checkout@v4
with:
path: app
- uses: actions/setup-node@v4
with:
node-version-file: 'app/.nvmrc'
- uses: shivammathur/setup-php@v2
with:
php-version: 8.1
ini-values: max_input_vars=5000
coverage: none
- uses: actions/cache/restore@v4
with:
key: build-${{ github.sha }}
path: |
app/node_modules/**/*
app/www/**/*
plugin/**/*
- name: Launch Docker images
working-directory: app
run: |
docker run -d --rm -p 8001:80 --name moodleapp -v ./www:/usr/share/nginx/html -v ./nginx.conf:/etc/nginx/conf.d/default.conf nginx:alpine
docker run -d --rm -p 8002:80 --name bigbluebutton moodlehq/bigbluebutton_mock:latest
- name: Initialise moodle-plugin-ci
run: |
composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4.3
echo $(cd ci/bin; pwd) >> $GITHUB_PATH
echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH
sudo locale-gen en_AU.UTF-8
echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV
- name: Install Behat Snapshots plugin - name: Install Behat Snapshots plugin
run: git clone --branch main --depth 1 https://github.com/NoelDeMartin/moodle-local_behatsnapshots $GITHUB_WORKSPACE/moodle/local/behatsnapshots run: moodle-plugin-ci add-plugin NoelDeMartin/moodle-local_behatsnapshots
- name: Generate Behat tests plugin
run: | - name: Install moodle-plugin-ci
export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle run: moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1
npx gulp behat env:
- name: Configure & launch Moodle with Docker DB: pgsql
run: | MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'main' }}
export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle MOODLE_REPO: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle.git' }}
cp $GITHUB_WORKSPACE/moodle-docker/config.docker-template.php $GITHUB_WORKSPACE/moodle/config.php MOODLE_BEHAT_IONIC_WWWROOT: http://localhost:8001
sed -i "69c\$CFG->behat_faildump_path = '/var/www/html/behatfaildumps';" $GITHUB_WORKSPACE/moodle/config.php MOODLE_BEHAT_DEFAULT_BROWSER: chrome
sed -i "69i\$CFG->behat_increasetimeout = 2;" $GITHUB_WORKSPACE/moodle/config.php
sed -i "69i\$CFG->behat_ionic_wwwroot = 'http://moodleapp';" $GITHUB_WORKSPACE/moodle/config.php - name: Update config
echo "define('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER', 'http://bbbmockserver/hash' . sha1(\$CFG->behat_wwwroot));" >> $GITHUB_WORKSPACE/moodle/config.php run: moodle-plugin-ci add-config 'define("TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER", "http://localhost:8002/hash" . sha1($CFG->wwwroot));'
$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 app with Docker
run: |
docker build --build-arg build_command="npm run build:test" -t moodlehq/moodleapp:behat .
docker run -d --rm --name moodleapp moodlehq/moodleapp:behat
docker network connect moodle-docker_default moodleapp --alias moodleapp
docker run --detach --name bbbmockserver --network moodle-docker_default moodlehq/bigbluebutton_mock:latest
- 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 --parallel=8 --optimize-runs='@app&&~@local&&$BEHAT_TAGS'"
- name: Run Behat tests - name: Run Behat tests
run: | run: moodle-plugin-ci behat --auto-rerun 3 --profile chrome --tags="@app&&~@local&&$BEHAT_TAGS"
export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle env:
$GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --verbose --tags='@app&&~@local&&$BEHAT_TAGS' --auto-rerun=3" BEHAT_TAGS: ${{ matrix.tags }}
- name: Upload Snapshot failures - name: Upload Snapshot failures
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: ${{ failure() }} if: ${{ failure() }}
with: with:
name: snapshot_failures name: snapshot_failures-${{ matrix.tags }}
path: moodle/local/moodleappbehat/tests/behat/snapshots/failures/* path: moodle/local/moodleappbehat/tests/behat/snapshots/failures/*
- name: Upload Behat failures - name: Upload Behat failures
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: ${{ failure() }} if: ${{ failure() }}
with: with:
name: behat_failures name: behat_failures-${{ matrix.tags }}
path: moodle/behatfaildumps path: moodledata/behat_dump/*
complete:
runs-on: ubuntu-latest
needs: [behat]
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Check failure artifacts
run: |
rm ./artifacts/build -rf
if [ -n "$(ls -A ./artifacts)" ]; then
echo "There were some failures in the previous jobs"
exit 1
fi

View File

@ -1,58 +0,0 @@
name: Performance
on: [ workflow_dispatch ]
jobs:
performance:
runs-on: ubuntu-latest
env:
MOODLE_DOCKER_DB: pgsql
MOODLE_DOCKER_BROWSER: chrome
MOODLE_DOCKER_PHP_VERSION: '8.0'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Additional checkouts
run: |
git clone --branch main --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle
git clone --branch main --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
- name: Install npm packages
run: 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 "67i\ 'capabilities' => [" $GITHUB_WORKSPACE/moodle/config.php
sed -i "68i\ 'extra_capabilities' => [" $GITHUB_WORKSPACE/moodle/config.php
sed -i "69i\ 'goog:loggingPrefs' => ['performance' => 'ALL']," $GITHUB_WORKSPACE/moodle/config.php
sed -i "70i\ 'chromeOptions' => ['perfLoggingPrefs' => ['traceCategories' => 'devtools.timeline']]," $GITHUB_WORKSPACE/moodle/config.php
sed -i "71i\ ]," $GITHUB_WORKSPACE/moodle/config.php
sed -i "72i\ ]," $GITHUB_WORKSPACE/moodle/config.php
sed -i "84i\$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 app with Docker
run: |
docker build --build-arg build_command="npm run build:test" -t moodlehq/moodleapp:performance .
docker run -d --rm --name moodleapp moodlehq/moodleapp:performance
docker network connect moodle-docker_default moodleapp --alias moodleapp
- name: Init Behat
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 performance tests
run: |
export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
for i in {0..2}
do
$GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags="@performance" --auto-rerun"
done
- name: Show performance results
run: node ./scripts/print-performance-measures.js $GITHUB_WORKSPACE/moodle/behatperformancemeasures/

View File

@ -57,7 +57,6 @@ class behat_app extends behat_app_helper {
} }
$this->featurepath = dirname($feature->getFile()); $this->featurepath = dirname($feature->getFile());
$this->configure_performance_logs();
} }
/** /**
@ -82,23 +81,6 @@ class behat_app extends behat_app_helper {
$this->enter_site(); $this->enter_site();
} }
/**
* Configure performance logs.
*/
protected function configure_performance_logs() {
global $CFG;
$performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null;
if ($performanceLogs !== 'ALL') {
return;
}
// Enable DB Logging only for app tests with performance logs activated.
$this->getSession()->visit($this->get_app_url() . '/assets/env.json');
$this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';");
}
/** /**
* Check whether the current page is the login form. * Check whether the current page is the login form.
*/ */
@ -1073,14 +1055,24 @@ class behat_app extends behat_app_helper {
} }
/**
* Send pending notifications.
*
* @Then /^I flush pending notifications in the app$/
*/
public function i_flush_notifications() {
$this->runtime_js("flushNotifications()");
}
/** /**
* Check if a notification has been triggered and is present. * Check if a notification has been triggered and is present.
* *
* @Then /^a notification with title (".+") is( not)? present in the app$/ * @Then /^a notification with title (".+") should( not)? be present in the app$/
* @param string $title Notification title * @param string $title Notification title
* @param bool $not Whether assert that the notification was not found * @param bool $not Whether assert that the notification was not found
*/ */
public function notification_present_in_the_app(string $title, bool $not = false) { public function notification_present_in_the_app(string $title, bool $not = false) {
$this->spin(function() use ($not, $title) {
$result = $this->runtime_js("notificationIsPresentWithText($title)"); $result = $this->runtime_js("notificationIsPresentWithText($title)");
if ($not && $result === 'YES') { if ($not && $result === 'YES') {
@ -1096,6 +1088,50 @@ class behat_app extends behat_app_helper {
} }
return true; return true;
});
}
/**
* Check if a notification has been scheduled.
*
* @Then /^a notification with title (".+") should( not)? be scheduled(?: (\d+) minutes before the "(.+)" assignment due date)? in the app$/
* @param string $title Notification title
* @param bool $not Whether assert that the notification was not scheduled
* @param int $minutes Minutes before the assignment at which the notification was scheduled
* @param string $assignment Assignment for which the notification was scheduled
*/
public function notification_scheduled_in_the_app(string $title, bool $not = false, ?int $minutes = null, ?string $assignment = null) {
if (!is_null($minutes)) {
global $DB;
$assign = $DB->get_record('assign', ['name' => $assignment]);
if (!$assign) {
throw new ExpectationException("Couldn't find '$assignment' assignment", $this->getSession()->getDriver());
}
$date = ($assign->duedate - $minutes * 60) * 1000;
} else {
$date = 'undefined';
}
$this->spin(function() use ($not, $title, $date) {
$result = $this->runtime_js("notificationIsScheduledWithText($title, $date)");
if ($not && $result === 'YES') {
throw new ExpectationException("Notification is scheduled", $this->getSession()->getDriver());
}
if (!$not && $result === 'NO') {
throw new ExpectationException("Notification is not scheduled", $this->getSession()->getDriver());
}
if ($result !== 'YES' && $result !== 'NO') {
throw new DriverException('Error checking scheduled notification - ' . $result);
}
return true;
});
} }
/** /**

View File

@ -292,6 +292,8 @@ class behat_app_helper extends behat_base {
/** /**
* Replaces $WWWROOT for the url of the Moodle site. * Replaces $WWWROOT for the url of the Moodle site.
* *
* Using $WWWROOTPATTERN will replace it for a regex pattern.
*
* @Transform /^(.*\$WWWROOT.*)$/ * @Transform /^(.*\$WWWROOT.*)$/
* @param string $text Text. * @param string $text Text.
* @return string * @return string
@ -299,7 +301,10 @@ class behat_app_helper extends behat_base {
public function replace_wwwroot($text) { public function replace_wwwroot($text) {
global $CFG; global $CFG;
return str_replace('$WWWROOT', $CFG->behat_wwwroot, $text); $text = str_replace('$WWWROOTPATTERN', preg_quote($CFG->behat_wwwroot, '/'), $text);
$text = str_replace('$WWWROOT', $CFG->behat_wwwroot, $text);
return $text;
} }
/** /**

View File

@ -1,128 +0,0 @@
<?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/>.
use Behat\Mink\Exception\DriverException;
use Behat\Mink\Exception\ExpectationException;
require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
require_once(__DIR__ . '/classes/performance_measure.php');
/**
* Behat step definitions to measure performance.
*/
class behat_performance extends behat_base {
/**
* @var array
*/
private $measures = [];
/**
* Start measuring performance.
*
* @When /^I start measuring "([^"]+)"$/
*/
public function i_start_measuring(string $name) {
$this->measures[$name] = new performance_measure($name, $this->getSession()->getDriver());
$this->measures[$name]->start();
}
/**
* Stop measuring performance.
*
* @When /^I stop measuring "([^"]+)"$/
*/
public function i_stop_measuring(string $name) {
$this->get_performance_measure($name)->end();
}
/**
* Assert how long a performance measure took.
*
* @Then /^"([^"]+)" should have taken (less than|more than|exactly) (\d+(?:\.\d+)? (?:seconds|milliseconds))$/
*/
public function timing_should_have_taken(string $measure, string $comparison, string $expectedtime) {
$measuretiming = $this->get_performance_measure($measure);
$comparison = $this->parse_comparison($comparison);
$expectedtime = $this->parse_time($expectedtime);
if (!call_user_func($comparison, $measuretiming->duration, $expectedtime)) {
throw new ExpectationException(
"Expected duration for '$measure' failed! (took {$measuretiming->duration}ms)",
$this->getSession()->getDriver()
);
}
$measuretiming->store();
}
/**
* Parse time.
*
* @param string $text Time string.
* @return float
*/
private function parse_time(string $text): float {
$spaceindex = strpos($text, ' ');
$value = floatval(substr($text, 0, $spaceindex));
if (substr($text, $spaceindex + 1) == 'seconds') {
$value *= 1000;
}
return $value;
}
/**
* Parse a comparison function.
*
* @param string $text Comparison string.
* @return Closure
*/
private function parse_comparison(string $text): Closure {
switch ($text) {
case 'less than':
return function ($a, $b) {
return $a < $b;
};
case 'more than':
return function ($a, $b) {
return $a > $b;
};
case 'exactly':
return function ($a, $b) {
return $a === $b;
};
default:
break;
}
}
/**
* Get performance measure by name.
*
* @param string $name Performance measure name.
* @return performance_measure Performance measure.
*/
private function get_performance_measure(string $name): performance_measure {
if (!isset($this->measures[$name])) {
throw new DriverException("'$name' performance measure does not exist.");
}
return $this->measures[$name];
}
}

View File

@ -1,346 +0,0 @@
<?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/>.
use Behat\Mink\Exception\DriverException;
use Facebook\WebDriver\Exception\InvalidArgumentException;
use Moodle\BehatExtension\Driver\WebDriver;
require_once(__DIR__ . '/../behat_app.php');
/**
* Performance measures for one particular metric.
*/
class performance_measure implements behat_app_listener {
const STORAGE_FOLDER = '/behatperformancemeasures/';
/**
* @var string
*/
public $name;
/**
* @var int
*/
public $start;
/**
* @var int
*/
public $end;
/**
* @var int
*/
public $duration;
/**
* @var int
*/
public $scripting;
/**
* @var int
*/
public $styling;
/**
* @var int
*/
public $blocking;
/**
* @var int
*/
public $databaseStart;
/**
* @var int
*/
public $database;
/**
* @var int
*/
public $networking;
/**
* @var array
*/
private $longTasks = [];
/**
* @var Closure
*/
private $behatAppUnsubscribe;
/**
* @var Moodle\BehatExtension\Driver\WebDriver
*/
private $driver;
public function __construct(string $name, WebDriver $driver) {
$this->name = $name;
$this->driver = $driver;
}
/**
* Start timing.
*/
public function start(): void {
$this->start = $this->now();
$this->observeLongTasks();
$this->startDatabaseCount();
$this->behatAppUnsubscribe = behat_app::listen($this);
}
/**
* Stop timing.
*/
public function end(): void {
$this->end = $this->now();
$this->stopLongTasksObserver();
call_user_func($this->behatAppUnsubscribe);
$this->behatAppUnsubscribe = null;
$this->analyseDuration();
$this->analyseLongTasks();
$this->analyseDatabaseUsage();
$this->analysePerformanceLogs();
}
/**
* Persist measure logs in storage.
*/
public function store(): void {
global $CFG;
$storagefolderpath = $CFG->dirroot . static::STORAGE_FOLDER;
if (!file_exists($storagefolderpath)) {
mkdir($storagefolderpath);
}
$data = [
'name' => $this->name,
'start' => $this->start,
'end' => $this->end,
'duration' => $this->duration,
'scripting' => $this->scripting,
'styling' => $this->styling,
'blocking' => $this->blocking,
'longTasks' => count($this->longTasks),
'database' => $this->database,
'networking' => $this->networking,
];
file_put_contents($storagefolderpath . time() . '.json', json_encode($data));
}
/**
* @inheritdoc
*/
public function on_app_load(): void {
if (is_null($this->start) || !is_null($this->end)) {
return;
}
$this->observeLongTasks();
}
/**
* @inheritdoc
*/
public function on_app_unload(): void {
$this->stopLongTasksObserver();
}
/**
* Get current time.
*
* @return int Current time in milliseconds.
*/
private function now(): int {
return $this->driver->evaluateScript('Date.now()');
}
/**
* Start observing long tasks.
*/
private function observeLongTasks(): void {
$this->driver->executeScript("
if (window.MA_PERFORMANCE_OBSERVER) return;
window.MA_LONG_TASKS = [];
window.MA_PERFORMANCE_OBSERVER = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
window.MA_LONG_TASKS.push(entry);
}
});
window.MA_PERFORMANCE_OBSERVER.observe({ entryTypes: ['longtask'] });
");
}
/**
* Record how many database queries have been logged so far.
*/
private function startDatabaseCount(): void {
try {
$this->databaseStart = $this->driver->evaluateScript('dbProvider.queryLogs.length') ?? 0;
} catch (Exception $e) {
$this->databaseStart = 0;
}
}
/**
* Flush Performance observer.
*/
private function stopLongTasksObserver(): void {
$newLongTasks = $this->driver->evaluateScript("
return (function() {
if (!window.MA_PERFORMANCE_OBSERVER) {
return [];
}
window.MA_PERFORMANCE_OBSERVER.disconnect();
const observer = window.MA_PERFORMANCE_OBSERVER;
const longTasks = window.MA_LONG_TASKS;
delete window.MA_PERFORMANCE_OBSERVER;
delete window.MA_LONG_TASKS;
return [...longTasks, ...observer.takeRecords()];
})();
");
if ($newLongTasks) {
$this->longTasks = array_merge($this->longTasks, $newLongTasks);
}
}
/**
* Analyse duration.
*/
private function analyseDuration(): void {
$this->duration = $this->end - $this->start;
}
/**
* Analyse long tasks.
*/
private function analyseLongTasks(): void {
$blockingDuration = 0;
foreach ($this->longTasks as $longTask) {
$blockingDuration += $longTask['duration'] - 50;
}
$this->blocking = $blockingDuration;
}
/**
* Analyse database usage.
*/
private function analyseDatabaseUsage(): void {
$this->database = $this->driver->evaluateScript('dbProvider.queryLogs.length') - $this->databaseStart;
}
/**
* Analyse performance logs.
*/
private function analysePerformanceLogs(): void {
global $CFG;
$scriptingDuration = 0;
$stylingDuration = 0;
$networkingCount = 0;
$logs = $this->getPerformanceLogs();
foreach ($logs as $log) {
// TODO this should filter by end time as well, but it seems like the timestamps are not
// working as expected.
if ($log['timestamp'] < $this->start) {
continue;
}
$message = json_decode($log['message'])->message;
$messagename = $message->params->name ?? '';
if (in_array($messagename, ['FunctionCall', 'GCEvent', 'MajorGC', 'MinorGC', 'EvaluateScript'])) {
$scriptingDuration += $message->params->dur;
continue;
}
if (in_array($messagename, ['UpdateLayoutTree', 'RecalculateStyles', 'ParseAuthorStyleSheet'])) {
$stylingDuration += $message->params->dur;
continue;
}
if (in_array($messagename, ['XHRLoad']) && !str_starts_with($message->params->args->data->url, $CFG->behat_ionic_wwwroot)) {
$networkingCount++;
continue;
}
}
$this->scripting = round($scriptingDuration / 1000);
$this->styling = round($stylingDuration / 1000);
$this->networking = $networkingCount;
}
/**
* Get performance logs.
*
* @return array Performance logs.
*/
private function getPerformanceLogs(): array {
try {
return $this->driver->getWebDriver()->manage()->getLog('performance');
} catch (InvalidArgumentException $e) {
throw new DriverException(
implode("\n", [
"It wasn't possible to get performance logs, make sure that you have configured the following capabilities:",
"",
"\$CFG->behat_profiles = [",
" 'default' => [",
" 'browser' => 'chrome',",
" 'wd_host' => 'http://selenium:4444/wd/hub',",
" 'capabilities' => [",
" 'extra_capabilities' => [",
" 'goog:loggingPrefs' => ['performance' => 'ALL'],",
" 'chromeOptions' => [",
" 'perfLoggingPrefs' => [",
" 'traceCategories' => 'devtools.timeline',",
" ],",
" ],",
" ],",
" ],",
" ],",
"];",
"",
])
);
}
}
}

View File

@ -1,87 +0,0 @@
#!/usr/bin/env node
// (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.
const { readdirSync, readFileSync } = require('fs');
if (process.argv.length < 3) {
console.error('Missing measure timings storage path argument');
process.exit(1);
}
const performanceMeasuresStoragePath = process.argv[2].trimRight('/') + '/';
const files = readdirSync(performanceMeasuresStoragePath);
const performanceMeasures = {};
if (files.length === 0) {
console.log('No logs found!');
process.exit(0);
}
// Aggregate data
for (const file of files) {
const performanceMeasure = JSON.parse(readFileSync(performanceMeasuresStoragePath + file));
performanceMeasures[performanceMeasure.name] = performanceMeasures[performanceMeasure.name] ?? {
duration: [],
scripting: [],
styling: [],
blocking: [],
longTasks: [],
database: [],
networking: [],
};
performanceMeasures[performanceMeasure.name].duration.push(performanceMeasure.duration);
performanceMeasures[performanceMeasure.name].scripting.push(performanceMeasure.scripting);
performanceMeasures[performanceMeasure.name].styling.push(performanceMeasure.styling);
performanceMeasures[performanceMeasure.name].blocking.push(performanceMeasure.blocking);
performanceMeasures[performanceMeasure.name].longTasks.push(performanceMeasure.longTasks);
performanceMeasures[performanceMeasure.name].database.push(performanceMeasure.database);
performanceMeasures[performanceMeasure.name].networking.push(performanceMeasure.networking);
}
// Calculate averages
for (const [name, { duration, scripting, styling, blocking, longTasks, database, networking }] of Object.entries(performanceMeasures)) {
const totalRuns = duration.length;
const averageDuration = Math.round(duration.reduce((total, duration) => total + duration) / totalRuns);
const averageScripting = Math.round(scripting.reduce((total, scripting) => total + scripting) / totalRuns);
const averageStyling = Math.round(styling.reduce((total, styling) => total + styling) / totalRuns);
const averageBlocking = Math.round(blocking.reduce((total, blocking) => total + blocking) / totalRuns);
const averageLongTasks = Math.round(longTasks.reduce((total, longTasks) => total + longTasks) / totalRuns);
const averageDatabase = Math.round(database.reduce((total, database) => total + database) / totalRuns);
const averageNetworking = Math.round(networking.reduce((total, networking) => total + networking) / totalRuns);
performanceMeasures[name] = {
'Total duration': `${averageDuration}ms`,
'Scripting': `${averageScripting}ms`,
'Styling': `${averageStyling}ms`,
'Blocking': `${averageBlocking}ms`,
'# Network requests': averageNetworking,
'# DB Queries': averageDatabase,
'# Long Tasks': averageLongTasks,
'# runs': totalRuns,
};
}
// Sort tests
const tests = Object.keys(performanceMeasures).sort();
const sortedPerformanceMeasures = {};
for (const test of tests) {
sortedPerformanceMeasures[test] = performanceMeasures[test];
}
// Display data
console.table(sortedPerformanceMeasures);

View File

@ -1,4 +1,4 @@
@block @block_timeline @app @javascript @lms_upto3.11 @addon_block_timeline @app @javascript @lms_upto3.11
Feature: Timeline block. Feature: Timeline block.
Background: Background:

View File

@ -1,4 +1,4 @@
@block @block_timeline @app @javascript @addon_block_timeline @app @javascript
Feature: Timeline block. Feature: Timeline block.
Background: Background:

View File

@ -1,4 +1,4 @@
@core @core_calendar @app @javascript @addon_calendar @app @javascript
Feature: Test creation of calendar events in app Feature: Test creation of calendar events in app
In order to take advantage of all the calendar features while using the mobile app In order to take advantage of all the calendar features while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @competency @app @javascript @addon_competency @app @javascript
Feature: Test competency navigation Feature: Test competency navigation
Background: Background:

View File

@ -1,4 +1,4 @@
@core @core_message @app @javascript @addon_messages @app @javascript
Feature: Test basic usage of messages in app Feature: Test basic usage of messages in app
In order to participate with messages while using the mobile app In order to participate with messages while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_message @app @javascript @addon_messages @app @javascript
Feature: Test messages navigation in the app Feature: Test messages navigation in the app
Background: Background:

View File

@ -1,4 +1,4 @@
@core @core_message @app @javascript @addon_messages @app @javascript
Feature: Test messages settings Feature: Test messages settings
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_assign @app @javascript @lms_upto3.10 @addon_mod_assign @app @javascript @lms_upto3.10
Feature: Test basic usage of assignment activity in app Feature: Test basic usage of assignment activity in app
In order to participate in the assignment while using the mobile app In order to participate in the assignment while using the mobile app
I need basic assignment functionality to work I need basic assignment functionality to work

View File

@ -1,4 +1,4 @@
@mod @mod_assign @app @javascript @addon_mod_assign @app @javascript
Feature: Test basic usage of assignment activity in app Feature: Test basic usage of assignment activity in app
In order to participate in the assignment while using the mobile app In order to participate in the assignment while using the mobile app
I need basic assignment functionality to work I need basic assignment functionality to work

View File

@ -1,4 +1,4 @@
@mod @mod_assign @app @javascript @lms_from4.0 @addon_mod_assign @app @javascript @lms_from4.0
Feature: Test marking workflow in assignment activity in app Feature: Test marking workflow in assignment activity in app
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_assign @app @javascript @addon_mod_assign @app @javascript
Feature: Test assignments navigation Feature: Test assignments navigation
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_bigbluebuttonbn @app @javascript @lms_from4.0 @addon_mod_bigbluebuttonbn @app @javascript @lms_from4.0
Feature: Test basic usage of BBB activity in app Feature: Test basic usage of BBB activity in app
In order to join a BBB meeting while using the mobile app In order to join a BBB meeting while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_bigbluebuttonbn @app @javascript @lms_from4.0 @addon_mod_bigbluebuttonbn @app @javascript @lms_from4.0
Feature: Test usage of BBB activity with groups in app Feature: Test usage of BBB activity with groups in app
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_book @app @javascript @addon_mod_book @app @javascript
Feature: Test basic usage of book activity in app Feature: Test basic usage of book activity in app
In order to view a book while using the mobile app In order to view a book while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@app @javascript @mod @mod_book @addon_mod_book @app @javascript
Feature: Test single activity of book type in app Feature: Test single activity of book type in app
In order to view a book while using the mobile app In order to view a book while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_chat @app @javascript @addon_mod_chat @app @javascript
Feature: Test basic usage of chat in app Feature: Test basic usage of chat in app
As a student As a student
I need basic chat functionality to work I need basic chat functionality to work

View File

@ -1,4 +1,4 @@
@mod @mod_chat @app @javascript @addon_mod_chat @app @javascript
Feature: Test chat navigation Feature: Test chat navigation
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_choice @app @javascript @lms_upto3.11 @addon_mod_choice @app @javascript @lms_upto3.11
Feature: Test basic usage of choice activity in app Feature: Test basic usage of choice activity in app
In order to participate in the choice while using the mobile app In order to participate in the choice while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_choice @app @javascript @addon_mod_choice @app @javascript
Feature: Test basic usage of choice activity in app Feature: Test basic usage of choice activity in app
In order to participate in the choice while using the mobile app In order to participate in the choice while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_data @app @javascript @addon_mod_data @app @javascript
Feature: Users can manage entries in database activities Feature: Users can manage entries in database activities
In order to populate databases In order to populate databases
As a user As a user

View File

@ -1,4 +1,4 @@
@mod @mod_data @app @javascript @addon_mod_data @app @javascript
Feature: Users can store entries in database activities when offline and sync when online Feature: Users can store entries in database activities when offline and sync when online
In order to populate databases while offline In order to populate databases while offline
As a user As a user

View File

@ -1,4 +1,4 @@
@mod @mod_feedback @app @javascript @addon_mod_feedback @app @javascript
Feature: Test feedback navigation Feature: Test feedback navigation
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_forum @app @javascript @addon_mod_forum @app @javascript
Feature: Test basic usage of forum activity in app Feature: Test basic usage of forum activity in app
In order to participate in the forum while using the mobile app In order to participate in the forum while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_forum @app @javascript @addon_mod_forum @app @javascript
Feature: Test usage of forum activity with groups in app Feature: Test usage of forum activity with groups in app
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_forum @app @javascript @addon_mod_forum @app @javascript
Feature: Test forum navigation Feature: Test forum navigation
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_forum @app @javascript @lms_from4.3 @addon_mod_forum @app @javascript @lms_from4.3
Feature: Test Forum Search Feature: Test Forum Search
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_glossary @app @javascript @addon_mod_glossary @app @javascript
Feature: Test basic usage of glossary in app Feature: Test basic usage of glossary in app
In order to participate in the glossaries while using the mobile app In order to participate in the glossaries while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_glossary @app @javascript @addon_mod_glossary @app @javascript
Feature: Test glossary navigation Feature: Test glossary navigation
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_lesson @app @javascript @lms_upto3.11 @addon_mod_lesson @app @javascript @lms_upto3.11
Feature: Test decimal separators in lesson Feature: Test decimal separators in lesson
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_lesson @app @javascript @addon_mod_lesson @app @javascript
Feature: Test decimal separators in lesson Feature: Test decimal separators in lesson
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_quiz @app @javascript @lms_from3.10 @lms_upto3.11 @addon_mod_quiz @app @javascript @lms_from3.10 @lms_upto3.11
Feature: Attempt a quiz in app Feature: Attempt a quiz in app
As a student As a student
In order to demonstrate what I know In order to demonstrate what I know

View File

@ -1,4 +1,4 @@
@mod @mod_quiz @app @javascript @lms_upto3.9 @addon_mod_quiz @app @javascript @lms_upto3.9
Feature: Attempt a quiz in app Feature: Attempt a quiz in app
As a student As a student
In order to demonstrate what I know In order to demonstrate what I know

View File

@ -1,4 +1,4 @@
@mod @mod_quiz @app @javascript @lms_from4.0 @addon_mod_quiz @app @javascript @lms_from4.0
Feature: Attempt a quiz in app Feature: Attempt a quiz in app
As a student As a student
In order to demonstrate what I know In order to demonstrate what I know

View File

@ -1,4 +1,4 @@
@mod @mod_quiz @app @javascript @addon_mod_quiz @app @javascript
Feature: Use quizzes with different behaviours in the app Feature: Use quizzes with different behaviours in the app
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_quiz @app @javascript @addon_mod_quiz @app @javascript
Feature: Navigate through a quiz in the app Feature: Navigate through a quiz in the app
Background: Background:

View File

@ -1,4 +1,4 @@
@mod @mod_scorm @app @javascript @addon_mod_scorm @app @javascript
Feature: Test appearance options of SCORM activity in app Feature: Test appearance options of SCORM activity in app
In order to play a SCORM while using the mobile app In order to play a SCORM while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_scorm @app @javascript @_switch_iframe @addon_mod_scorm @app @javascript @_switch_iframe
Feature: Test attempts and grading settings of SCORM activity in app Feature: Test attempts and grading settings of SCORM activity in app
In order to play a SCORM while using the mobile app In order to play a SCORM while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_scorm @app @javascript @addon_mod_scorm @app @javascript
Feature: Test availability options of SCORM activity in app Feature: Test availability options of SCORM activity in app
Only open SCORMs should be allowed to be played Only open SCORMs should be allowed to be played

View File

@ -1,4 +1,4 @@
@mod @mod_scorm @app @javascript @_switch_iframe @addon_mod_scorm @app @javascript @_switch_iframe
Feature: Test basic usage of SCORM activity in app Feature: Test basic usage of SCORM activity in app
In order to play a SCORM while using the mobile app In order to play a SCORM while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_survey @app @javascript @addon_mod_survey @app @javascript
Feature: Test basic usage of survey activity in app Feature: Test basic usage of survey activity in app
In order to participate in surveys while using the mobile app In order to participate in surveys while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@mod @mod_workshop @app @javascript @addon_mod_workshop @app @javascript
Feature: Test basic usage of workshop activity in app Feature: Test basic usage of workshop activity in app
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @addon_notifications @app @javascript
Feature: Notifications Feature: Notifications
Background: Background:

View File

@ -48,6 +48,7 @@ import { CoreUpdateManagerProvider } from '@services/update-manager';
import { CoreUrlUtilsProvider } from '@services/utils/url'; import { CoreUrlUtilsProvider } from '@services/utils/url';
import { CoreUtilsProvider } from '@services/utils/utils'; import { CoreUtilsProvider } from '@services/utils/utils';
import { CoreWSProvider } from '@services/ws'; import { CoreWSProvider } from '@services/ws';
import { CorePlatformService } from '@services/platform';
export const CORE_SERVICES: Type<unknown>[] = [ export const CORE_SERVICES: Type<unknown>[] = [
CoreAppProvider, CoreAppProvider,
@ -68,6 +69,7 @@ export const CORE_SERVICES: Type<unknown>[] = [
CoreMimetypeUtilsProvider, CoreMimetypeUtilsProvider,
CoreNavigatorService, CoreNavigatorService,
CorePluginFileDelegateService, CorePluginFileDelegateService,
CorePlatformService,
CoreScreenService, CoreScreenService,
CoreSitesProvider, CoreSitesProvider,
CoreSyncProvider, CoreSyncProvider,

View File

@ -1,4 +1,4 @@
@core @core_comments @app @javascript @core_comments @app @javascript
Feature: Test basic usage of comments in app Feature: Test basic usage of comments in app
In order to participate in the comments while using the mobile app In order to participate in the comments while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @lms_upto3.11 @core_course @app @javascript @lms_upto3.11
Feature: Test basic usage of one course in app Feature: Test basic usage of one course in app
In order to participate in one course while using the mobile app In order to participate in one course while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @core_course @app @javascript
Feature: Test basic usage of one course in app Feature: Test basic usage of one course in app
In order to participate in one course while using the mobile app In order to participate in one course while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @lms_upto3.10 @core_course @app @javascript @lms_upto3.10
Feature: Check course completion feature. Feature: Check course completion feature.
In order to track the progress of the course on mobile device In order to track the progress of the course on mobile device
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @core_course @app @javascript
Feature: Check course completion feature. Feature: Check course completion feature.
In order to track the progress of the course on mobile device In order to track the progress of the course on mobile device
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @lms_upto3.11 @core_course @app @javascript @lms_upto3.11
Feature: Test course list shown on app start tab Feature: Test course list shown on app start tab
In order to select a course In order to select a course
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @core_course @app @javascript
Feature: Test course list shown on app start tab Feature: Test course list shown on app start tab
In order to select a course In order to select a course
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @enrol @enrol_guest @core_course @app @javascript @enrol @enrol_guest
Feature: Test basic usage of guest access course in app Feature: Test basic usage of guest access course in app
Background: Background:

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @lms_from4.0 @core_course @app @javascript @lms_from4.0
Feature: Check relative dates feature. Feature: Check relative dates feature.
Background: Background:

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @lms_upto3.10 @core_courses @app @javascript @lms_upto3.10
Feature: Test basic usage of courses in app Feature: Test basic usage of courses in app
In order to participate in the courses while using the mobile app In order to participate in the courses while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @lms_upto3.11 @core_courses @app @javascript @lms_upto3.11
Feature: Test basic usage of courses in app Feature: Test basic usage of courses in app
In order to participate in the courses while using the mobile app In order to participate in the courses while using the mobile app
As a student As a student

View File

@ -1,4 +1,4 @@
@core @core_course @app @javascript @core_courses @app @javascript
Feature: Test basic usage of courses in app Feature: Test basic usage of courses in app
In order to participate in the courses while using the mobile app In order to participate in the courses while using the mobile app
As a student As a student

View File

@ -15,6 +15,8 @@
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { ILocalNotification, ILocalNotificationAction, LocalNotifications } from '@awesome-cordova-plugins/local-notifications/ngx'; import { ILocalNotification, ILocalNotificationAction, LocalNotifications } from '@awesome-cordova-plugins/local-notifications/ngx';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { CoreUtils } from '@services/utils/utils';
import { CorePlatform } from '@services/platform';
/** /**
* Mock LocalNotifications service. * Mock LocalNotifications service.
@ -67,6 +69,17 @@ export class LocalNotificationsMock extends LocalNotifications {
}); });
} }
/**
* Flush pending notifications.
*/
flush(): void {
for (const notification of this.scheduledNotifications) {
this.sendNotification(notification);
}
this.scheduledNotifications = [];
}
/** /**
* Sets timeout for next nofitication. * Sets timeout for next nofitication.
*/ */
@ -104,36 +117,7 @@ export class LocalNotificationsMock extends LocalNotifications {
const notificationTime = nextNotification.trigger?.at?.getTime() || 0; const notificationTime = nextNotification.trigger?.at?.getTime() || 0;
if (notificationTime === 0 || notificationTime <= dateNow) { if (notificationTime === 0 || notificationTime <= dateNow) {
const body = Array.isArray(nextNotification.text) ? nextNotification.text.join() : nextNotification.text; this.sendNotification(nextNotification);
const notification = new Notification(nextNotification.title || '', {
body,
data: nextNotification.data,
icon: nextNotification.icon,
requireInteraction: true,
tag: nextNotification.data?.component,
});
this.triggeredNotifications.push(nextNotification);
this.observables.trigger.next(nextNotification);
notification.addEventListener('click', () => {
this.observables.click.next(nextNotification);
notification.close();
if (nextNotification.id) {
delete(this.presentNotifications[nextNotification.id]);
}
});
if (nextNotification.id) {
this.presentNotifications[nextNotification.id] = notification;
notification.addEventListener('close', () => {
delete(this.presentNotifications[nextNotification.id ?? 0]);
});
}
this.scheduledNotifications.shift(); this.scheduledNotifications.shift();
this.triggerNextNotification(); this.triggerNextNotification();
} else { } else {
@ -141,6 +125,43 @@ export class LocalNotificationsMock extends LocalNotifications {
} }
} }
/**
* Send notification.
*
* @param localNotification Notification.
*/
protected sendNotification(localNotification: ILocalNotification): void {
const body = Array.isArray(localNotification.text) ? localNotification.text.join() : localNotification.text;
const notification = new Notification(localNotification.title || '', {
body,
data: localNotification.data,
icon: localNotification.icon,
requireInteraction: true,
tag: localNotification.data?.component,
});
this.triggeredNotifications.push(localNotification);
this.observables.trigger.next(localNotification);
notification.addEventListener('click', () => {
this.observables.click.next(localNotification);
notification.close();
if (localNotification.id) {
delete(this.presentNotifications[localNotification.id]);
}
});
if (localNotification.id) {
this.presentNotifications[localNotification.id] = notification;
notification.addEventListener('close', () => {
delete(this.presentNotifications[localNotification.id ?? 0]);
});
}
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
@ -313,7 +334,16 @@ export class LocalNotificationsMock extends LocalNotifications {
*/ */
async registerPermission(): Promise<boolean> { async registerPermission(): Promise<boolean> {
// We need to ask the user for permission // We need to ask the user for permission
const permission = await Notification.requestPermission(); const permissionRequests = [Notification.requestPermission()];
if (CorePlatform.isAutomated()) {
// In some testing environments, Notification.requestPermission gets stuck and never returns.
// Given that we don't actually need browser notifications to work in Behat tests, we can just
// continue if the permissions haven't been granted after 1 second.
permissionRequests.push(CoreUtils.wait(1000).then(() => 'granted'));
}
const permission = await Promise.race(permissionRequests);
this.hasGranted = permission === 'granted'; this.hasGranted = permission === 'granted';

View File

@ -41,9 +41,9 @@ import { CoreNavigator } from '@services/navigator';
import { makeSingleton, Translate } from '@singletons'; import { makeSingleton, Translate } from '@singletons';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreAppProvider } from '@services/app';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseAccess } from '@features/course/services/course-options-delegate'; import { CoreCourseAccess } from '@features/course/services/course-options-delegate';
import { CorePlatform } from '@services/platform';
export const GRADES_PAGE_NAME = 'grades'; export const GRADES_PAGE_NAME = 'grades';
export const GRADES_PARTICIPANTS_PAGE_NAME = 'participant-grades'; export const GRADES_PARTICIPANTS_PAGE_NAME = 'participant-grades';
@ -105,7 +105,7 @@ export class CoreGradesHelperProvider {
row.rowclass += itemNameColumn.class.indexOf('hidden') >= 0 ? ' hidden' : ''; row.rowclass += itemNameColumn.class.indexOf('hidden') >= 0 ? ' hidden' : '';
row.rowclass += itemNameColumn.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; row.rowclass += itemNameColumn.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
if (!useLegacyLayout && !CoreAppProvider.isAutomated()) { if (!useLegacyLayout && !CorePlatform.isAutomated()) {
// Activity name is only included in the webservice response from the latest version when behat is not running. // Activity name is only included in the webservice response from the latest version when behat is not running.
content = content.replace(/<span[^>]+>.+?<\/span>/i, ''); content = content.replace(/<span[^>]+>.+?<\/span>/i, '');
} }

View File

@ -1,4 +1,4 @@
@app @javascript @lms_upto4.1 @core_grades @app @javascript @lms_upto4.1
Feature: Grades navigation Feature: Grades navigation
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core_grades @app @javascript
Feature: Grades navigation Feature: Grades navigation
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @lms_upto3.11 @core_grades @app @javascript @lms_upto3.11
Feature: View grades Feature: View grades
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @lms_from4.0 @core_grades @app @javascript @lms_from4.0
Feature: View grades Feature: View grades
Background: Background:

View File

@ -1,4 +1,4 @@
@auth @core_auth @app @javascript @lms_from4.0 @lms_upto4.0 @core_login @app @javascript @lms_from4.0 @lms_upto4.0
Feature: Test basic usage of login in app Feature: Test basic usage of login in app
I need basic login functionality to work I need basic login functionality to work

View File

@ -1,4 +1,4 @@
@auth @core_auth @app @javascript @lms_upto3.11 @core_login @app @javascript @lms_upto3.11
Feature: Test basic usage of login in app Feature: Test basic usage of login in app
I need basic login functionality to work I need basic login functionality to work

View File

@ -1,4 +1,4 @@
@auth @core_auth @app @javascript @core_login @app @javascript
Feature: Test basic usage of login in app Feature: Test basic usage of login in app
I need basic login functionality to work I need basic login functionality to work
@ -96,7 +96,7 @@ Feature: Test basic usage of login in app
And I should find "You must change your password to proceed." in the app And I should find "You must change your password to proceed." in the app
When I press "Change password" "ion-button" in the app When I press "Change password" "ion-button" in the app
Then the app should have opened a browser tab with url "webserver" Then the app should have opened a browser tab with url "$WWWROOTPATTERN"
When I close the browser tab opened by the app When I close the browser tab opened by the app
Then I should find "If you didn't change your password correctly, you'll be asked to do it again." in the app Then I should find "If you didn't change your password correctly, you'll be asked to do it again." in the app
@ -115,7 +115,7 @@ Feature: Test basic usage of login in app
But I should not find "Reconnect" in the app But I should not find "Reconnect" in the app
When I press "Change password" "ion-button" in the app When I press "Change password" "ion-button" in the app
Then the app should have opened a browser tab with url "webserver" Then the app should have opened a browser tab with url "$WWWROOTPATTERN"
When I switch to the browser tab opened by the app When I switch to the browser tab opened by the app
And I set the field "username" to "student1" And I set the field "username" to "student1"

View File

@ -1,4 +1,4 @@
@auth @core_auth @app @javascript @lms_upto3.9 @core_login @app @javascript @lms_upto3.9
Feature: Test signup in app Feature: Test signup in app
I need basic signup functionality to work I need basic signup functionality to work

View File

@ -1,4 +1,4 @@
@auth @core_auth @app @javascript @core_login @app @javascript
Feature: Test signup in app Feature: Test signup in app
I need basic signup functionality to work I need basic signup functionality to work

View File

@ -1,4 +1,4 @@
@app @javascript @lms_upto3.11 @core_mainmenu @app @javascript @lms_upto3.11
Feature: Main Menu opens the right page Feature: Main Menu opens the right page
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core_mainmenu @app @javascript
Feature: Main Menu opens the right page Feature: Main Menu opens the right page
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core_reminders @lms_from4.0 @core_reminders @app @javascript @lms_from4.0
Feature: Set a new reminder on activity Feature: Set a new reminder on activity
Background: Background:
@ -62,20 +62,21 @@ Feature: Set a new reminder on activity
And I press "Custom..." in the app And I press "Custom..." in the app
Then I should find "Custom reminder" in the app Then I should find "Custom reminder" in the app
When I set the following fields to these values in the app: When I set the following fields to these values in the app:
| Value | 69 | | Value | 40 |
| Units | minutes | | Units | minutes |
And I press "Set reminder" in the app And I press "Set reminder" in the app
Then I should find "Reminder set for" in the app Then I should find "Reminder set for" in the app
When I wait "50" seconds And a notification with title "Due: Assignment 01" should be scheduled 40 minutes before the "Assignment 01" assignment due date in the app
Then a notification with title "Due: Assignment 01" is present in the app When I flush pending notifications in the app
And I close a notification with title "Due: Assignment 01" in the app Then a notification with title "Due: Assignment 01" should be present in the app
# Set and check reminder is cancelled # Set and check reminder is cancelled
When I press "Set a reminder for \"Assignment 01\" (Due)" in the app When I close a notification with title "Due: Assignment 01" in the app
And I press "Set a reminder for \"Assignment 01\" (Due)" in the app
And I press "Custom..." in the app And I press "Custom..." in the app
Then I should find "Custom reminder" in the app Then I should find "Custom reminder" in the app
When I set the following fields to these values in the app: When I set the following fields to these values in the app:
| Value | 68 | | Value | 20 |
| Units | minutes | | Units | minutes |
And I press "Set reminder" in the app And I press "Set reminder" in the app
Then I should find "Reminder set for" in the app Then I should find "Reminder set for" in the app
@ -83,8 +84,8 @@ Feature: Set a new reminder on activity
Then I should find "Reminder set for" in the app Then I should find "Reminder set for" in the app
When I press "Delete reminder" in the app When I press "Delete reminder" in the app
Then I should find "Reminder deleted" in the app Then I should find "Reminder deleted" in the app
When I wait "50" seconds But a notification with title "Due: Assignment 01" should not be scheduled in the app
Then a notification with title "Due: Assignment 01" is not present in the app And a notification with title "Due: Assignment 01" should not be present in the app
Scenario: Check toast is correct Scenario: Check toast is correct
Given I entered the assign activity "Assignment 02" on course "Course 1" as "student1" in the app Given I entered the assign activity "Assignment 02" on course "Course 1" as "student1" in the app

View File

@ -1,4 +1,4 @@
@app @javascript @core_reminders @core_reminders @app @javascript
Feature: Set a new reminder on course Feature: Set a new reminder on course
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core_reportbuilder @lms_from4.1 @core_reportbuilder @app @javascript @lms_from4.1
Feature: Report builder Feature: Report builder
Background: Background:

View File

@ -1,4 +1,4 @@
@core @core_search @app @javascript @lms_from4.3 @core_search @app @javascript @lms_from4.3
Feature: Test Global Search Feature: Test Global Search
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core_settings @core_settings @app @javascript
Feature: It navigates properly within settings. Feature: It navigates properly within settings.
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core_settings @core_settings @app @javascript
Feature: It synchronise sites properly Feature: It synchronise sites properly
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core_siteplugins @app @javascript
Feature: Plugins work properly. Feature: Plugins work properly.
Background: Background:

View File

@ -1,4 +1,4 @@
@core @core_user @app @javascript @core_user @app @javascript
Feature: Test basic usage of user features Feature: Test basic usage of user features
Background: Background:
@ -17,7 +17,7 @@ Feature: Test basic usage of user features
And I should find "Before you continue, please fill in the required fields in your user profile." in the app And I should find "Before you continue, please fill in the required fields in your user profile." in the app
When I press "Complete profile" in the app When I press "Complete profile" in the app
Then the app should have opened a browser tab with url "webserver" Then the app should have opened a browser tab with url "$WWWROOTPATTERN"
When I close the browser tab opened by the app When I close the browser tab opened by the app
Then I should find "If you didn't complete your profile correctly, you'll be asked to do it again." in the app Then I should find "If you didn't complete your profile correctly, you'll be asked to do it again." in the app
@ -36,7 +36,7 @@ Feature: Test basic usage of user features
But I should not find "Reconnect" in the app But I should not find "Reconnect" in the app
When I press "Complete profile" in the app When I press "Complete profile" in the app
Then the app should have opened a browser tab with url "webserver" Then the app should have opened a browser tab with url "$WWWROOTPATTERN"
When I switch to the browser tab opened by the app When I switch to the browser tab opened by the app
And I set the field "username" to "student1" And I set the field "username" to "student1"

View File

@ -1,4 +1,4 @@
@core @core_user @app @javascript @lms_upto3.11 @core_user @app @javascript @lms_upto3.11
Feature: Site support Feature: Site support
Background: Background:

View File

@ -1,4 +1,4 @@
@core @core_user @app @javascript @lms_from4.0 @core_user @app @javascript @lms_from4.0
Feature: Site support Feature: Site support
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core_usertours @app @javascript
Feature: User Tours work properly. Feature: User Tours work properly.
Background: Background:

View File

@ -70,10 +70,11 @@ export class CoreAppProvider {
/** /**
* Returns whether the user agent is controlled by automation. I.e. Behat testing. * Returns whether the user agent is controlled by automation. I.e. Behat testing.
* *
* @deprecated since 4.4. Use CorePlatform.isAutomated() instead.
* @returns True if the user agent is controlled by automation, false otherwise. * @returns True if the user agent is controlled by automation, false otherwise.
*/ */
static isAutomated(): boolean { static isAutomated(): boolean {
return !!navigator.webdriver; return CorePlatform.isAutomated();
} }
/** /**
@ -82,7 +83,7 @@ export class CoreAppProvider {
* @returns Timezone. Undefined to use the user's timezone. * @returns Timezone. Undefined to use the user's timezone.
*/ */
static getForcedTimezone(): string | undefined { static getForcedTimezone(): string | undefined {
if (CoreAppProvider.isAutomated()) { if (CorePlatform.isAutomated()) {
// Use the same timezone forced for LMS in tests. // Use the same timezone forced for LMS in tests.
return 'Australia/Perth'; return 'Australia/Perth';
} }

View File

@ -16,7 +16,6 @@ import { Injectable } from '@angular/core';
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { LangChangeEvent } from '@ngx-translate/core'; import { LangChangeEvent } from '@ngx-translate/core';
import { CoreAppProvider } from '@services/app';
import { CoreConfig } from '@services/config'; import { CoreConfig } from '@services/config';
import { CoreSubscriptions } from '@singletons/subscriptions'; import { CoreSubscriptions } from '@singletons/subscriptions';
import { makeSingleton, Translate, Http } from '@singletons'; import { makeSingleton, Translate, Http } from '@singletons';
@ -72,7 +71,7 @@ export class CoreLangProvider {
let language: string; let language: string;
if (CoreAppProvider.isAutomated()) { if (CorePlatform.isAutomated()) {
// Force current language to English when Behat is running. // Force current language to English when Behat is running.
language = 'en'; language = 'en';
} else { } else {

View File

@ -44,6 +44,15 @@ export class CorePlatformService extends Platform {
return this.isMobile() && this.is('android'); return this.isMobile() && this.is('android');
} }
/**
* Returns whether the user agent is controlled by automation. I.e. Behat testing.
*
* @returns True if the user agent is controlled by automation, false otherwise.
*/
isAutomated(): boolean {
return !!navigator.webdriver;
}
/** /**
* Checks if the app is running in an iOS mobile or tablet device. * Checks if the app is running in an iOS mobile or tablet device.
* *

View File

@ -1,4 +1,4 @@
@app @javascript @lms_from4.0 @core @app @javascript @lms_from4.0
Feature: Custom lang strings Feature: Custom lang strings
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core @app @javascript
Feature: It navigates properly within activities. Feature: It navigates properly within activities.
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core @app @javascript
Feature: It navigates properly using deep links. Feature: It navigates properly using deep links.
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core @app @javascript
Feature: It opens external links properly. Feature: It opens external links properly.
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core @app @javascript
Feature: It navigates using gestures. Feature: It navigates using gestures.
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core @app @javascript
Feature: It navigates properly in pages with a split-view component. Feature: It navigates properly in pages with a split-view component.
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @lms_upto3.9 @core @app @javascript @lms_upto3.9
Feature: It opens files properly. Feature: It opens files properly.
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core @app @javascript
Feature: It opens files properly. Feature: It opens files properly.
Background: Background:

View File

@ -1,4 +1,4 @@
@app @javascript @core @app @javascript
Feature: It has a Behat runtime with testing helpers. Feature: It has a Behat runtime with testing helpers.
Background: Background:

View File

@ -30,6 +30,7 @@ import { CoreSites, CoreSitesProvider } from '@services/sites';
import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
import { CoreSwipeNavigationDirective } from '@directives/swipe-navigation'; import { CoreSwipeNavigationDirective } from '@directives/swipe-navigation';
import { Swiper } from 'swiper'; import { Swiper } from 'swiper';
import { LocalNotificationsMock } from '@features/emulator/services/local-notifications';
/** /**
* Behat runtime servive with public API. * Behat runtime servive with public API.
@ -585,6 +586,13 @@ export class TestingBehatRuntimeService {
console.log('BEHAT: ' + nowFormatted, ...args); // eslint-disable-line no-console console.log('BEHAT: ' + nowFormatted, ...args); // eslint-disable-line no-console
} }
/**
* Flush pending notifications.
*/
flushNotifications(): void {
(LocalNotifications as unknown as LocalNotificationsMock).flush();
}
/** /**
* Check a notification is present. * Check a notification is present.
* *
@ -608,6 +616,23 @@ export class TestingBehatRuntimeService {
return (await LocalNotifications.isPresent(notification.id)) ? 'YES' : 'NO'; return (await LocalNotifications.isPresent(notification.id)) ? 'YES' : 'NO';
} }
/**
* Check a notification is scheduled.
*
* @param title Title of the notification
* @param date Scheduled notification date.
* @returns YES or NO: depending on the result.
*/
async notificationIsScheduledWithText(title: string, date?: number): Promise<string> {
const notifications = await LocalNotifications.getAllScheduled();
const notification = notifications.find(
(notification) => notification.title?.includes(title) && (!date || notification.trigger?.at?.getTime() === date),
);
return notification ? 'YES' : 'NO';
}
/** /**
* Close notification. * Close notification.
* *

Some files were not shown because too many files have changed in this diff Show More