commit
1f81ea3513
|
@ -63,3 +63,24 @@ function notify_on_error_exit {
|
|||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function get_behat_plugin_changes_diff {
|
||||
i=0
|
||||
previoushash=""
|
||||
currenthash=`git rev-parse HEAD`
|
||||
initialhash=`git rev-list HEAD | tail -n 1`
|
||||
totalcommits=`git log --oneline | wc -l`
|
||||
repositoryname=`echo $GITHUB_REPOSITORY | sed "s/\\//\\\\\\\\\\//"`
|
||||
|
||||
((totalcommits--))
|
||||
while [ $i -lt $totalcommits ] && [[ -z $previoushash ]]; do
|
||||
previoushash=`git rev-list --format=%B --max-count=1 HEAD~$i | grep -o "https:\/\/github\.com\/$repositoryname\/compare\/[^.]\+\.\.\.[^.]\+" | sed "s/https:\/\/github\.com\/$repositoryname\/compare\/[^.]\+\.\.\.//"`
|
||||
((i++))
|
||||
done
|
||||
|
||||
if [[ -z $previoushash ]]; then
|
||||
previoushash=$initialhash
|
||||
fi
|
||||
|
||||
echo "$previoushash...$currenthash"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
#!/bin/bash
|
||||
source "./.github/scripts/functions.sh"
|
||||
|
||||
if [ -z $GIT_TOKEN ] || [ -z $BEHAT_PLUGIN_GITHUB_REPOSITORY ] || [ -z $BEHAT_PLUGIN_BRANCH ]; then
|
||||
print_error "Env vars not correctly defined"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $BEHAT_PLUGIN_BRANCH != $GITHUB_REF_NAME ]]; then
|
||||
echo "Script disabled for this branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Clone plugin repository.
|
||||
print_title "Cloning Behat plugin repository..."
|
||||
|
||||
git clone https://$GIT_TOKEN@github.com/$BEHAT_PLUGIN_GITHUB_REPOSITORY.git tmp/local_moodleappbehat -b integration
|
||||
pluginversion=$(cat tmp/local_moodleappbehat/version.php | grep "\$plugin->version" | grep -o -E "[0-9]+")
|
||||
|
||||
# Auto-generate plugin.
|
||||
print_title "Building Behat plugin..."
|
||||
|
||||
if [ -z $BEHAT_PLUGIN_EXCLUDE_FEATURES ]; then
|
||||
scripts/build-behat-plugin.js tmp/local_moodleappbehat
|
||||
else
|
||||
scripts/build-behat-plugin.js tmp/local_moodleappbehat --exclude-features
|
||||
fi
|
||||
notify_on_error_exit "Unsuccessful build, stopping..."
|
||||
|
||||
# Check if there are any changes (ignoring plugin version).
|
||||
print_title "Checking changes..."
|
||||
|
||||
newpluginversion=$(cat tmp/local_moodleappbehat/version.php | grep "\$plugin->version" | grep -o -E "[0-9]+")
|
||||
sed -i s/\$plugin-\>version\ =\ [0-9]\\+\;/\$plugin-\>version\ =\ $pluginversion\;/ tmp/local_moodleappbehat/version.php
|
||||
|
||||
if [[ -z `git -C tmp/local_moodleappbehat/ status --short` ]]; then
|
||||
echo "There weren't any changes to apply!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ $pluginversion -eq $newpluginversion ]]; then
|
||||
((newpluginversion++))
|
||||
fi
|
||||
|
||||
sed -i s/\$plugin-\>version\ =\ [0-9]\\+\;/\$plugin-\>version\ =\ $newpluginversion\;/ tmp/local_moodleappbehat/version.php
|
||||
|
||||
# Apply new changes
|
||||
print_title "Applying changes to repository..."
|
||||
|
||||
cd tmp/local_moodleappbehat
|
||||
|
||||
diff=`get_behat_plugin_changes_diff`
|
||||
|
||||
# Set up Github Actions bot user
|
||||
# See https://github.community/t/github-actions-bot-email-address/17204/6
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add .
|
||||
git commit -m "[auto-generated] Update plugin files
|
||||
Check out the commits that caused these changes: https://github.com/$GITHUB_REPOSITORY/compare/$diff
|
||||
"
|
||||
notify_on_error_exit "Unsuccessful commit, stopping..."
|
||||
|
||||
echo "Pushing changes..."
|
||||
git push
|
||||
notify_on_error_exit "Unsuccessful push, stopping..."
|
||||
|
||||
echo "Behat plugin updated!"
|
|
@ -1,10 +1,10 @@
|
|||
name: Behat tests
|
||||
name: Acceptance tests (Behat)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tags:
|
||||
description: 'Execute tags'
|
||||
behat_tags:
|
||||
description: 'Behat tags to execute'
|
||||
required: true
|
||||
default: '~@performance'
|
||||
moodle_branch:
|
||||
|
@ -15,6 +15,10 @@ on:
|
|||
description: 'Moodle repository'
|
||||
required: true
|
||||
default: 'https://github.com/moodle/moodle'
|
||||
pull_request:
|
||||
branches:
|
||||
- integration
|
||||
- unscheduled
|
||||
|
||||
jobs:
|
||||
behat:
|
||||
|
@ -23,6 +27,9 @@ jobs:
|
|||
MOODLE_DOCKER_DB: pgsql
|
||||
MOODLE_DOCKER_BROWSER: chrome
|
||||
MOODLE_DOCKER_PHP_VERSION: 7.3
|
||||
MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'master' }}
|
||||
MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }}
|
||||
BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- id: nvmrc
|
||||
|
@ -32,7 +39,7 @@ jobs:
|
|||
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 $MOODLE_BRANCH --depth 1 $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 ci --no-audit
|
||||
|
@ -48,9 +55,9 @@ jobs:
|
|||
$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
|
||||
- name: Compile & launch app with Docker
|
||||
run: |
|
||||
docker build -t moodlehq/moodleapp:behat .
|
||||
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
|
||||
- name: Init Behat
|
||||
|
@ -60,4 +67,4 @@ jobs:
|
|||
- 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"
|
||||
$GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --verbose --tags='@app&&$BEHAT_TAGS' --auto-rerun"
|
||||
|
|
|
@ -40,9 +40,9 @@ jobs:
|
|||
$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
|
||||
- name: Compile & launch app with Docker
|
||||
run: |
|
||||
docker build -t moodlehq/moodleapp:performance .
|
||||
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
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
name: Update Behat plugin
|
||||
|
||||
on: ['push']
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Install npm packages
|
||||
run: npm ci --no-audit
|
||||
- name: Update Behat plugin
|
||||
env:
|
||||
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
BEHAT_PLUGIN_GITHUB_REPOSITORY: ${{ secrets.BEHAT_PLUGIN_GITHUB_REPOSITORY }}
|
||||
BEHAT_PLUGIN_BRANCH: ${{ secrets.BEHAT_PLUGIN_BRANCH }}
|
||||
run: ./.github/scripts/update_behat_plugin.sh
|
18
.travis.yml
18
.travis.yml
|
@ -68,21 +68,3 @@ jobs:
|
|||
homebrew:
|
||||
packages:
|
||||
- jq
|
||||
- stage: test
|
||||
name: "End to end tests (mod_forum and mod_messages)"
|
||||
services:
|
||||
- docker
|
||||
if: type = cron
|
||||
script: scripts/test_e2e.sh "@app&&@mod_forum" "@app&&@mod_messages"
|
||||
- stage: test
|
||||
name: "End to end tests (mod_course, core_course and mod_courses)"
|
||||
services:
|
||||
- docker
|
||||
if: type = cron
|
||||
script: scripts/test_e2e.sh "@app&&@mod_course" "@app&&@core_course" "@app&&@mod_courses"
|
||||
- stage: test
|
||||
name: "End to end tests (others)"
|
||||
services:
|
||||
- docker
|
||||
if: type = cron
|
||||
script: scripts/test_e2e.sh "@app&&~@mod_forum&&~@mod_messages&&~@mod_course&&~@core_course&&~@mod_courses"
|
||||
|
|
|
@ -46,6 +46,12 @@
|
|||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/testing/testing.module.ts",
|
||||
"with": "src/testing/testing.module.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": {
|
||||
"scripts": false,
|
||||
"styles": true
|
||||
|
|
|
@ -71,5 +71,5 @@ gulp.task('watch', () => {
|
|||
});
|
||||
|
||||
gulp.task('watch-behat', () => {
|
||||
gulp.watch(['./src/**/*.feature', './local-moodleappbehat'], { interval: 500 }, gulp.parallel('behat'));
|
||||
gulp.watch(['./src/**/*.feature', './local_moodleappbehat'], { interval: 500 }, gulp.parallel('behat'));
|
||||
});
|
||||
|
|
|
@ -96,9 +96,8 @@ class behat_app extends behat_app_helper {
|
|||
public function i_wait_the_app_to_restart() {
|
||||
// Wait window to reload.
|
||||
$this->spin(function() {
|
||||
$result = $this->js("return !window.behat;");
|
||||
|
||||
if (!$result) {
|
||||
if ($this->runtime_js('hasInitialized()')) {
|
||||
// Behat runtime shouldn't be initialized after reload.
|
||||
throw new DriverException('Window is not reloading properly.');
|
||||
}
|
||||
|
||||
|
@ -115,25 +114,25 @@ class behat_app extends behat_app_helper {
|
|||
* @Then /^I should( not)? find (".+")( inside the .+)? in the app$/
|
||||
* @param bool $not Whether assert that the element was not found
|
||||
* @param string $locator Element locator
|
||||
* @param string $containerName Container name
|
||||
* @param string $container Container name
|
||||
*/
|
||||
public function i_find_in_the_app(bool $not, string $locator, string $containerName = '') {
|
||||
public function i_find_in_the_app(bool $not, string $locator, string $container = '') {
|
||||
$locator = $this->parse_element_locator($locator);
|
||||
if (!empty($containerName)) {
|
||||
preg_match('/^ inside the (.+)$/', $containerName, $matches);
|
||||
$containerName = $matches[1];
|
||||
if (!empty($container)) {
|
||||
preg_match('/^ inside the (.+)$/', $container, $matches);
|
||||
$container = $matches[1];
|
||||
}
|
||||
$containerName = json_encode($containerName);
|
||||
$options = json_encode(['containerName' => $container]);
|
||||
|
||||
$this->spin(function() use ($not, $locator, $containerName) {
|
||||
$result = $this->js("return window.behat.find($locator, $containerName);");
|
||||
$this->spin(function() use ($not, $locator, $options) {
|
||||
$result = $this->runtime_js("find($locator, $options)");
|
||||
|
||||
if ($not && $result === 'OK') {
|
||||
throw new DriverException('Error, found an item that should not be found');
|
||||
throw new DriverException('Error, found an element that should not be found');
|
||||
}
|
||||
|
||||
if (!$not && $result !== 'OK') {
|
||||
throw new DriverException('Error finding item - ' . $result);
|
||||
throw new DriverException('Error finding element - ' . $result);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -152,10 +151,10 @@ class behat_app extends behat_app_helper {
|
|||
$locator = $this->parse_element_locator($locator);
|
||||
|
||||
$this->spin(function() use ($locator) {
|
||||
$result = $this->js("return window.behat.scrollTo($locator);");
|
||||
$result = $this->runtime_js("scrollTo($locator)");
|
||||
|
||||
if ($result !== 'OK') {
|
||||
throw new DriverException('Error finding item - ' . $result);
|
||||
throw new DriverException('Error finding element - ' . $result);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -175,7 +174,7 @@ class behat_app extends behat_app_helper {
|
|||
*/
|
||||
public function i_load_more_items_in_the_app(bool $not = false) {
|
||||
$this->spin(function() use ($not) {
|
||||
$result = $this->js('return await window.behat.loadMoreItems();');
|
||||
$result = $this->runtime_js('loadMoreItems()');
|
||||
|
||||
if ($not && $result !== 'ERROR: All items are already loaded.') {
|
||||
throw new DriverException('It should not have been possible to load more items');
|
||||
|
@ -200,7 +199,7 @@ class behat_app extends behat_app_helper {
|
|||
public function i_swipe_in_the_app(string $direction) {
|
||||
$method = 'swipe' . ucwords($direction);
|
||||
|
||||
$this->js("window.behat.getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()");
|
||||
$this->runtime_js("getAngularInstance('ion-content', 'CoreSwipeNavigationDirective').$method()");
|
||||
|
||||
$this->wait_for_pending_js();
|
||||
|
||||
|
@ -219,21 +218,21 @@ class behat_app extends behat_app_helper {
|
|||
$locator = $this->parse_element_locator($locator);
|
||||
|
||||
$this->spin(function() use ($locator, $not) {
|
||||
$result = $this->js("return window.behat.isSelected($locator);");
|
||||
$result = $this->runtime_js("isSelected($locator)");
|
||||
|
||||
switch ($result) {
|
||||
case 'YES':
|
||||
if ($not) {
|
||||
throw new ExpectationException("Item was selected and shouldn't have", $this->getSession()->getDriver());
|
||||
throw new ExpectationException("Element 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());
|
||||
throw new ExpectationException("Element wasn't selected and should have", $this->getSession()->getDriver());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new DriverException('Error finding item - ' . $result);
|
||||
throw new DriverException('Error finding element - ' . $result);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -326,7 +325,7 @@ class behat_app extends behat_app_helper {
|
|||
$this->login($username);
|
||||
}
|
||||
|
||||
$mycoursesfound = $this->js("return window.behat.find({ text: 'My courses', selector: 'ion-tab-button'});");
|
||||
$mycoursesfound = $this->runtime_js("find({ text: 'My courses', selector: 'ion-tab-button'})");
|
||||
|
||||
if ($mycoursesfound !== 'OK') {
|
||||
// My courses not present enter from Dashboard.
|
||||
|
@ -382,7 +381,7 @@ class behat_app extends behat_app_helper {
|
|||
*/
|
||||
public function i_press_the_standard_button_in_the_app(string $button) {
|
||||
$this->spin(function() use ($button) {
|
||||
$result = $this->js("return await window.behat.pressStandard('$button');");
|
||||
$result = $this->runtime_js("pressStandard('$button')");
|
||||
|
||||
if ($result !== 'OK') {
|
||||
throw new DriverException('Error pressing standard button - ' . $result);
|
||||
|
@ -420,7 +419,7 @@ class behat_app extends behat_app_helper {
|
|||
],
|
||||
]);
|
||||
|
||||
$this->js("window.behat.notificationClicked($notification)");
|
||||
$this->zone_js("pushNotifications.notificationClicked($notification)", true);
|
||||
$this->wait_for_pending_js();
|
||||
}
|
||||
|
||||
|
@ -508,7 +507,7 @@ class behat_app extends behat_app_helper {
|
|||
*/
|
||||
public function i_close_the_popup_in_the_app() {
|
||||
$this->spin(function() {
|
||||
$result = $this->js("return window.behat.closePopup();");
|
||||
$result = $this->runtime_js('closePopup()');
|
||||
|
||||
if ($result !== 'OK') {
|
||||
throw new DriverException('Error closing popup - ' . $result);
|
||||
|
@ -536,7 +535,7 @@ class behat_app extends behat_app_helper {
|
|||
* 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.
|
||||
* distinguish visible elements and the app always has many non-visible elements in the DOM.
|
||||
*
|
||||
* @When /^I press (".+") in the app$/
|
||||
* @param string $locator Element locator
|
||||
|
@ -546,7 +545,7 @@ class behat_app extends behat_app_helper {
|
|||
$locator = $this->parse_element_locator($locator);
|
||||
|
||||
$this->spin(function() use ($locator) {
|
||||
$result = $this->js("return await window.behat.press($locator);");
|
||||
$result = $this->runtime_js("press($locator)");
|
||||
|
||||
if ($result !== 'OK') {
|
||||
throw new DriverException('Error pressing item - ' . $result);
|
||||
|
@ -578,6 +577,33 @@ class behat_app extends behat_app_helper {
|
|||
$this->wait_for_pending_js();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if elements can be pressed in the app.
|
||||
*
|
||||
* @Then /^I should( not)? be able to press (".+") in the app$/
|
||||
* @param bool $not Whether to assert that the element cannot be pressed
|
||||
* @param string $locator Element locator
|
||||
*/
|
||||
public function i_should_be_able_to_press_in_the_app(bool $not, string $locator) {
|
||||
$locator = $this->parse_element_locator($locator);
|
||||
|
||||
$this->spin(function() use ($not, $locator) {
|
||||
$result = $this->runtime_js("find($locator, { onlyClickable: true })");
|
||||
|
||||
if ($not && $result === 'OK') {
|
||||
throw new DriverException('Error, found a clickable element that should not be found');
|
||||
}
|
||||
|
||||
if (!$not && $result !== 'OK') {
|
||||
throw new DriverException('Error finding clickable element - ' . $result);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$this->wait_for_pending_js();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an item from a list of options, such as a radio button.
|
||||
*
|
||||
|
@ -596,23 +622,23 @@ class behat_app extends behat_app_helper {
|
|||
|
||||
$this->spin(function() use ($selectedtext, $selected, $locator) {
|
||||
// Don't do anything if the item is already in the expected state.
|
||||
$result = $this->js("return window.behat.isSelected($locator);");
|
||||
$result = $this->runtime_js("isSelected($locator)");
|
||||
|
||||
if ($result === $selected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Press item.
|
||||
$result = $this->js("return await window.behat.press($locator);");
|
||||
// Press element.
|
||||
$result = $this->runtime_js("press($locator)");
|
||||
|
||||
if ($result !== 'OK') {
|
||||
throw new DriverException('Error pressing item - ' . $result);
|
||||
throw new DriverException('Error pressing element - ' . $result);
|
||||
}
|
||||
|
||||
// Check that it worked as expected.
|
||||
$this->wait_for_pending_js();
|
||||
|
||||
$result = $this->js("return window.behat.isSelected($locator);");
|
||||
$result = $this->runtime_js("isSelected($locator)");
|
||||
|
||||
switch ($result) {
|
||||
case 'YES':
|
||||
|
@ -646,7 +672,7 @@ class behat_app extends behat_app_helper {
|
|||
$value = addslashes_js($value);
|
||||
|
||||
$this->spin(function() use ($field, $value) {
|
||||
$result = $this->js("return await window.behat.setField(\"$field\", \"$value\");");
|
||||
$result = $this->runtime_js("setField('$field', '$value')");
|
||||
|
||||
if ($result !== 'OK') {
|
||||
throw new DriverException('Error setting field - ' . $result);
|
||||
|
@ -685,7 +711,7 @@ class behat_app extends behat_app_helper {
|
|||
*/
|
||||
public function the_header_should_be_in_the_app(string $text) {
|
||||
$this->spin(function() use ($text) {
|
||||
$result = $this->js('return window.behat.getHeader();');
|
||||
$result = $this->runtime_js('getHeader()');
|
||||
|
||||
if (substr($result, 0, 3) !== 'OK:') {
|
||||
throw new DriverException('Error getting header - ' . $result);
|
||||
|
@ -766,7 +792,7 @@ class behat_app extends behat_app_helper {
|
|||
* @When I run cron tasks in the app
|
||||
*/
|
||||
public function i_run_cron_tasks_in_the_app() {
|
||||
$this->js('await window.behat.forceSyncExecution()');
|
||||
$this->zone_js('cronDelegate.forceSyncExecution()');
|
||||
$this->wait_for_pending_js();
|
||||
}
|
||||
|
||||
|
@ -776,7 +802,7 @@ class behat_app extends behat_app_helper {
|
|||
* @When I wait loading to finish in the app
|
||||
*/
|
||||
public function i_wait_loading_to_finish_in_the_app() {
|
||||
$this->js('await window.behat.waitLoadingToFinish()');
|
||||
$this->runtime_js('waitLoadingToFinish()');
|
||||
$this->wait_for_pending_js();
|
||||
}
|
||||
|
||||
|
@ -798,7 +824,7 @@ class behat_app extends behat_app_helper {
|
|||
$this->getSession()->switchToWindow($names[1]);
|
||||
}
|
||||
|
||||
$this->js('window.close();');
|
||||
$this->js('window.close()');
|
||||
$this->getSession()->switchToWindow($names[0]);
|
||||
}
|
||||
|
||||
|
@ -810,7 +836,7 @@ class behat_app extends behat_app_helper {
|
|||
* @throws DriverException If the navigator.online mode is not available
|
||||
*/
|
||||
public function i_switch_offline_mode(string $offline) {
|
||||
$this->js("window.behat.network.setForceOffline($offline);");
|
||||
$this->runtime_js("network.setForceOffline($offline)");
|
||||
}
|
||||
|
||||
}
|
|
@ -313,12 +313,12 @@ class behat_app_helper extends behat_base {
|
|||
|
||||
try {
|
||||
// Init Behat JavaScript runtime.
|
||||
$initoptions = json_encode([
|
||||
'skipOnBoarding' => $options['skiponboarding'] ?? true,
|
||||
'configOverrides' => $this->appconfig,
|
||||
]);
|
||||
|
||||
$initOptions = new StdClass();
|
||||
$initOptions->skipOnBoarding = $options['skiponboarding'] ?? true;
|
||||
$initOptions->configOverrides = $this->appconfig;
|
||||
|
||||
$this->js('window.behatInit(' . json_encode($initOptions) . ');');
|
||||
$this->runtime_js("init($initoptions)");
|
||||
} catch (Exception $error) {
|
||||
throw new DriverException('Moodle App not running or not running on Automated mode.');
|
||||
}
|
||||
|
@ -456,7 +456,7 @@ class behat_app_helper extends behat_base {
|
|||
|
||||
$res = $this->evaluate_script("Promise.resolve($script)
|
||||
.then(result => window.$promisevariable = result)
|
||||
.catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message);");
|
||||
.catch(error => window.$promisevariable = 'Async code rejected: ' + error?.message)");
|
||||
|
||||
do {
|
||||
if (microtime(true) - $start > $timeout) {
|
||||
|
@ -465,15 +465,42 @@ class behat_app_helper extends behat_base {
|
|||
|
||||
// 0.1 seconds.
|
||||
usleep(100000);
|
||||
} while (!$this->evaluate_script("return '$promisevariable' in window;"));
|
||||
} while (!$this->evaluate_script("'$promisevariable' in window"));
|
||||
|
||||
$result = $this->evaluate_script("return window.$promisevariable;");
|
||||
$result = $this->evaluate_script("window.$promisevariable");
|
||||
|
||||
$this->evaluate_script("delete window.$promisevariable;");
|
||||
$this->evaluate_script("delete window.$promisevariable");
|
||||
|
||||
if (is_string($result) && strrpos($result, 'Async code rejected:') === 0) {
|
||||
throw new DriverException($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate and execute methods from the Behat runtime.
|
||||
*
|
||||
* @param string $script
|
||||
* @return mixed Result.
|
||||
*/
|
||||
protected function runtime_js(string $script) {
|
||||
return $this->js("window.behat.$script");
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate and execute methods from the Behat runtime inside the Angular zone.
|
||||
*
|
||||
* @param string $script
|
||||
* @param bool $blocking
|
||||
* @return mixed Result.
|
||||
*/
|
||||
protected function zone_js(string $script, bool $blocking = false) {
|
||||
$blockingjson = json_encode($blocking);
|
||||
|
||||
return $this->runtime_js("runInZone(() => window.behat.$script, $blockingjson)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a custom URL for automatic login and redirect from the Moodle App (and waits to finish.)
|
||||
*
|
||||
|
@ -513,9 +540,10 @@ class behat_app_helper extends behat_base {
|
|||
|
||||
// Generate custom URL.
|
||||
$parsed_url = parse_url($CFG->behat_wwwroot);
|
||||
$domain = $parsed_url['host'] ?? '';
|
||||
$rootpath = $parsed_url['path'] ?? '';
|
||||
$url = $this->get_mobile_url_scheme() . "://$username@$domain$rootpath?token=$token&privatetoken=$privatetoken";
|
||||
$site = $parsed_url['host'] ?? '';
|
||||
$site .= isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
|
||||
$site .= $parsed_url['path'] ?? '';
|
||||
$url = $this->get_mobile_url_scheme() . "://$username@$site?token=$token&privatetoken=$privatetoken";
|
||||
|
||||
if (!empty($path)) {
|
||||
$url .= '&redirect='.urlencode($CFG->behat_wwwroot.$path);
|
||||
|
@ -548,8 +576,7 @@ class behat_app_helper extends behat_base {
|
|||
* @param string $successXPath The XPath of the element to lookat after navigation.
|
||||
*/
|
||||
protected function handle_url(string $customurl, string $successXPath = '') {
|
||||
// Instead of using evaluate_async_script, we wait for the path to load.
|
||||
$result = $this->js("return await window.behat.handleCustomURL('$customurl');");
|
||||
$result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')");
|
||||
|
||||
if ($result !== 'OK') {
|
||||
throw new DriverException('Error handling url - ' . $result);
|
|
@ -178,7 +178,7 @@ class performance_measure implements behat_app_listener {
|
|||
* @return int Current time in milliseconds.
|
||||
*/
|
||||
private function now(): int {
|
||||
return $this->driver->evaluateScript('Date.now();');
|
||||
return $this->driver->evaluateScript('Date.now()');
|
||||
}
|
||||
|
||||
/**
|
|
@ -229,12 +229,31 @@
|
|||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
|
||||
"integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"open": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz",
|
||||
|
@ -3889,6 +3908,16 @@
|
|||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
|
@ -3913,6 +3942,15 @@
|
|||
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
@ -11772,12 +11810,12 @@
|
|||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
|
@ -12327,6 +12365,16 @@
|
|||
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
|
@ -12447,6 +12495,17 @@
|
|||
"table": "^5.2.3",
|
||||
"text-table": "^0.2.0",
|
||||
"v8-compile-cache": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-scope": {
|
||||
|
@ -13433,7 +13492,7 @@
|
|||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"concat-stream": {
|
||||
"version": "1.6.2",
|
||||
|
@ -16449,6 +16508,16 @@
|
|||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||
|
@ -16493,6 +16562,15 @@
|
|||
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
@ -16625,6 +16703,16 @@
|
|||
"tsconfig-paths": "^3.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"doctrine": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz",
|
||||
|
@ -16634,6 +16722,15 @@
|
|||
"esutils": "^2.0.2",
|
||||
"isarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -17635,6 +17732,16 @@
|
|||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
|
||||
|
@ -17766,6 +17873,15 @@
|
|||
"to-regex": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
|
@ -18326,6 +18442,25 @@
|
|||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"glob-base": {
|
||||
|
@ -19962,6 +20097,25 @@
|
|||
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"image-size": {
|
||||
|
@ -20959,6 +21113,16 @@
|
|||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
@ -20968,6 +21132,15 @@
|
|||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -23349,11 +23522,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
|
@ -25138,6 +25312,16 @@
|
|||
"which": "^1.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz",
|
||||
|
@ -25200,6 +25384,15 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
|
||||
|
@ -28127,6 +28320,27 @@
|
|||
"dev": true,
|
||||
"requires": {
|
||||
"minimatch": "3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"reflect-metadata": {
|
||||
|
@ -31856,6 +32070,27 @@
|
|||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^7.1.4",
|
||||
"minimatch": "^3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"text-table": {
|
||||
|
|
|
@ -172,6 +172,7 @@
|
|||
"jest": "^26.5.2",
|
||||
"jest-preset-angular": "^8.3.1",
|
||||
"jsonc-parser": "^2.3.1",
|
||||
"minimatch": "^5.1.0",
|
||||
"native-run": "^1.4.0",
|
||||
"patch-package": "^6.4.7",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const minimatch = require('minimatch');
|
||||
const { existsSync, readFileSync, writeFileSync, statSync, renameSync, rmSync } = require('fs');
|
||||
const { readdir } = require('fs').promises;
|
||||
const { mkdirSync, copySync } = require('fs-extra');
|
||||
|
@ -21,12 +22,22 @@ const { resolve, extname, dirname, basename, relative } = require('path');
|
|||
|
||||
async function main() {
|
||||
const pluginPath = process.argv[2] || guessPluginPath() || fail('Folder argument missing!');
|
||||
const excludeFeatures = process.argv.some(arg => arg === '--exclude-features');
|
||||
const exclusions = excludeFeatures
|
||||
? [
|
||||
'*.feature',
|
||||
'**/js/mobile/index.js',
|
||||
'**/db/mobile.php',
|
||||
'**/classes/output/mobile.php',
|
||||
]
|
||||
: [];
|
||||
|
||||
if (!existsSync(pluginPath)) {
|
||||
mkdirSync(pluginPath);
|
||||
} else {
|
||||
// Empty directory, except the excluding list.
|
||||
const excludeFromErase = [
|
||||
...exclusions,
|
||||
'.git',
|
||||
'.gitignore',
|
||||
'README.md',
|
||||
|
@ -34,7 +45,7 @@ async function main() {
|
|||
|
||||
const files = await readdir(pluginPath, { withFileTypes: true });
|
||||
for (const file of files) {
|
||||
if (excludeFromErase.indexOf(file.name) >= 0) {
|
||||
if (isExcluded(file.name, excludeFromErase)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -43,13 +54,17 @@ async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Copy plugin template.
|
||||
const { version: appVersion } = require(projectPath('package.json'));
|
||||
const templatePath = projectPath('local-moodleappbehat');
|
||||
const templatePath = projectPath('local_moodleappbehat');
|
||||
|
||||
for await (const file of getDirectoryFiles(templatePath)) {
|
||||
if (isExcluded(file, exclusions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
copySync(templatePath, pluginPath);
|
||||
copySync(file, file.replace(templatePath, pluginPath));
|
||||
}
|
||||
|
||||
// Update version.php
|
||||
const pluginFilePath = pluginPath + '/version.php';
|
||||
|
@ -62,28 +77,30 @@ async function main() {
|
|||
writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements));
|
||||
|
||||
// Copy feature files.
|
||||
const behatTempFeaturesPath = `${pluginPath}/behat-tmp`;
|
||||
copySync(projectPath('src'), behatTempFeaturesPath, { filter: isFeatureFileOrDirectory });
|
||||
if (!excludeFeatures) {
|
||||
const behatTempFeaturesPath = `${pluginPath}/behat-tmp`;
|
||||
copySync(projectPath('src'), behatTempFeaturesPath, { filter: isFeatureFileOrDirectory });
|
||||
|
||||
const behatFeaturesPath = `${pluginPath}/tests/behat`;
|
||||
if (!existsSync(behatFeaturesPath)) {
|
||||
mkdirSync(behatFeaturesPath, {recursive: true});
|
||||
}
|
||||
|
||||
for await (const featureFile of getDirectoryFiles(behatTempFeaturesPath)) {
|
||||
const featurePath = dirname(featureFile);
|
||||
if (!featurePath.endsWith('/tests/behat')) {
|
||||
continue;
|
||||
const behatFeaturesPath = `${pluginPath}/tests/behat`;
|
||||
if (!existsSync(behatFeaturesPath)) {
|
||||
mkdirSync(behatFeaturesPath, {recursive: true});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
for await (const featureFile of getDirectoryFiles(behatTempFeaturesPath)) {
|
||||
const featurePath = dirname(featureFile);
|
||||
if (!featurePath.endsWith('/tests/behat')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rmSync(behatTempFeaturesPath, {recursive: true});
|
||||
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) {
|
||||
|
@ -92,6 +109,10 @@ function isFeatureFileOrDirectory(src) {
|
|||
return stats.isDirectory() || extname(src) === '.feature';
|
||||
}
|
||||
|
||||
function isExcluded(file, exclusions) {
|
||||
return exclusions.some(exclusion => minimatch(file, exclusion));
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
source "scripts/functions.sh"
|
||||
|
||||
# Prepare variables
|
||||
basedir="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../" && pwd )"
|
||||
dockerscripts="$HOME/moodle-docker/bin/"
|
||||
dockercompose="$dockerscripts/moodle-docker-compose"
|
||||
|
||||
export MOODLE_DOCKER_DB=pgsql
|
||||
export MOODLE_DOCKER_BROWSER=chrome
|
||||
export MOODLE_DOCKER_WWWROOT="$HOME/moodle"
|
||||
export MOODLE_DOCKER_PHP_VERSION=7.4
|
||||
export MOODLE_DOCKER_APP_PATH=$basedir
|
||||
|
||||
# Prepare dependencies
|
||||
print_title "Preparing dependencies"
|
||||
git clone --branch master --depth 1 git://github.com/moodle/moodle $HOME/moodle
|
||||
git clone --branch ionic5 --depth 1 git://github.com/moodlehq/moodle-local_moodlemobileapp $HOME/moodle/local/moodlemobileapp
|
||||
|
||||
# TODO replace for moodlehq/moodle-docker after merging https://github.com/moodlehq/moodle-docker/pull/156
|
||||
git clone --branch MOBILE-3738 --depth 1 git://github.com/NoelDeMartin/moodle-docker $HOME/moodle-docker
|
||||
|
||||
cp $HOME/moodle-docker/config.docker-template.php $HOME/moodle/config.php
|
||||
|
||||
# Build app
|
||||
print_title "Building app"
|
||||
npm ci
|
||||
|
||||
# Start containers
|
||||
print_title "Starting containers"
|
||||
$dockercompose pull
|
||||
$dockercompose up -d
|
||||
$dockerscripts/moodle-docker-wait-for-db
|
||||
$dockerscripts/moodle-docker-wait-for-app
|
||||
|
||||
$dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/init.php"
|
||||
notify_on_error_exit "e2e failed initializing behat"
|
||||
|
||||
print_title "Running e2e tests"
|
||||
|
||||
# Run tests
|
||||
for tags in "$@"
|
||||
do
|
||||
$dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags=\"$tags\" --auto-rerun"
|
||||
notify_on_error_exit "Some e2e tests are failing, please review"
|
||||
done
|
||||
|
||||
# Clean up
|
||||
$dockercompose down
|
|
@ -45,8 +45,9 @@ Feature: Users can manage entries in database activities
|
|||
|
||||
Scenario: Browse entry
|
||||
Given I entered the data activity "Web links" on course "Course 1" as "student1" in the app
|
||||
|
||||
# TODO Create and use a generator for database entries.
|
||||
And I press "Add entries" in the app
|
||||
When I press "Add entries" in the app
|
||||
And I set the following fields to these values in the app:
|
||||
| URL | https://moodle.org/ |
|
||||
| Description | Moodle community site |
|
||||
|
@ -59,16 +60,19 @@ Feature: Users can manage entries in database activities
|
|||
And I press "Save" near "Web links" in the app
|
||||
And I press "More" near "Moodle community site" in the app
|
||||
Then I should find "Moodle community site" in the app
|
||||
And I should not find "Next" in the app
|
||||
And I should find "Previous" in the app
|
||||
And I press "Previous" in the app
|
||||
And I should find "Moodle Cloud" in the app
|
||||
And I should find "Next" in the app
|
||||
And I should not find "Previous" in the app
|
||||
And I press "Next" in the app
|
||||
And I should find "Moodle community site" in the app
|
||||
And I should not find "Moodle Cloud" in the app
|
||||
And I press the back button in the app
|
||||
And I should be able to press "Previous" in the app
|
||||
But I should not be able to press "Next" in the app
|
||||
|
||||
When I press "Previous" in the app
|
||||
Then I should find "Moodle Cloud" in the app
|
||||
And I should be able to press "Next" in the app
|
||||
But I should not be able to press "Previous" in the app
|
||||
|
||||
When I press "Next" in the app
|
||||
Then I should find "Moodle community site" in the app
|
||||
But I should not find "Moodle Cloud" in the app
|
||||
|
||||
When I press the back button in the app
|
||||
And I should find "Moodle community site" in the app
|
||||
And I should find "Moodle Cloud" in the app
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { IonRouterOutlet } from '@ionic/angular';
|
||||
import { BackButtonEvent, ScrollDetail } from '@ionic/core';
|
||||
|
||||
|
@ -21,7 +21,7 @@ import { CoreLoginHelper } from '@features/login/services/login-helper';
|
|||
import { CoreEvents } from '@singletons/events';
|
||||
import { NgZone, SplashScreen, Translate } from '@singletons';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
import { CoreApp, CoreAppProvider } from '@services/app';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSubscriptions } from '@singletons/subscriptions';
|
||||
|
@ -38,10 +38,6 @@ import { CorePlatform } from '@services/platform';
|
|||
const MOODLE_VERSION_PREFIX = 'version-';
|
||||
const MOODLEAPP_VERSION_PREFIX = 'moodleapp-';
|
||||
|
||||
type AutomatedTestsWindow = Window & {
|
||||
changeDetector?: ChangeDetectorRef;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
|
@ -54,12 +50,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||
protected lastUrls: Record<string, number> = {};
|
||||
protected lastInAppUrl?: string;
|
||||
|
||||
constructor(changeDetector: ChangeDetectorRef) {
|
||||
if (CoreAppProvider.isAutomated()) {
|
||||
(window as AutomatedTestsWindow).changeDetector = changeDetector;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*
|
||||
|
|
|
@ -32,7 +32,7 @@ import { JitCompilerFactory } from '@angular/platform-browser-dynamic';
|
|||
import { CoreCronDelegate } from '@services/cron';
|
||||
import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron';
|
||||
import { moodleTransitionAnimation } from '@classes/page-transition';
|
||||
import { BehatTestingModule } from '@/testing/behat-testing.module';
|
||||
import { TestingModule } from '@/testing/testing.module';
|
||||
|
||||
// For translate loader. AoT requires an exported function for factories.
|
||||
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
||||
|
@ -60,7 +60,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
|||
AppRoutingModule,
|
||||
CoreModule,
|
||||
AddonsModule,
|
||||
BehatTestingModule,
|
||||
TestingModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
|
|
|
@ -220,22 +220,9 @@ Feature: Test basic usage of comments in app
|
|||
|
||||
Scenario: Add comments & Delete comments (blogs)
|
||||
# Create blog as a teacher
|
||||
Given the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion | configdata |
|
||||
| blog_menu | Course | C1 | course-view-* | site-pre | |
|
||||
And I entered the course "Course 1" as "teacher1" in the app
|
||||
And I press "Course summary" in the app
|
||||
# TODO Create and use a generator blog entries.
|
||||
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 "Open block drawer" "button"
|
||||
And I click on "Add an entry about this course" "link" in the "Blog menu" "block"
|
||||
And I set the following fields to these values:
|
||||
| Entry title | Blog test |
|
||||
| Blog entry body | Blog body |
|
||||
And I press "Save changes"
|
||||
And I close the browser tab opened by the app
|
||||
Given the following "core_blog > entries" exist:
|
||||
| subject | body | user |
|
||||
| Blog test | Blog body | teacher1 |
|
||||
|
||||
# Create and delete comments as a student
|
||||
When I entered the app as "student1"
|
||||
|
@ -263,21 +250,9 @@ Feature: Test basic usage of comments in app
|
|||
|
||||
Scenario: Add comments offline & Delete comments offline & Sync comments (blogs)
|
||||
# Create blog as a teacher
|
||||
Given the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion | configdata |
|
||||
| blog_menu | Course | C1 | course-view-* | site-pre | |
|
||||
And 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 "Open block drawer" "button"
|
||||
And I click on "Add an entry about this course" "link" in the "Blog menu" "block"
|
||||
And I set the following fields to these values:
|
||||
| Entry title | Blog test |
|
||||
| Blog entry body | Blog body |
|
||||
And I press "Save changes"
|
||||
And I close the browser tab opened by the app
|
||||
Given the following "core_blog > entries" exist:
|
||||
| subject | body | user |
|
||||
| Blog test | Blog body | teacher1 |
|
||||
|
||||
# Create and delete comments as a student
|
||||
When I entered the app as "student1"
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
// (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 {}
|
|
@ -15,13 +15,13 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, NgZone } from '@singletons';
|
||||
import { BehatTestsWindow, TestsBehatRuntime } from './behat-runtime';
|
||||
import { BehatTestsWindow, TestingBehatRuntime } from './behat-runtime';
|
||||
|
||||
/**
|
||||
* Behat block JS manager.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TestsBehatBlockingService {
|
||||
export class TestingBehatBlockingService {
|
||||
|
||||
protected waitingBlocked = false;
|
||||
protected recentMutation = false;
|
||||
|
@ -48,7 +48,7 @@ export class TestsBehatBlockingService {
|
|||
win.M.util = win.M.util ?? {};
|
||||
win.M.util.pending_js = win.M.util.pending_js ?? [];
|
||||
|
||||
TestsBehatRuntime.log('Initialized!');
|
||||
TestingBehatRuntime.log('Initialized!');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,7 +90,7 @@ export class TestsBehatBlockingService {
|
|||
}
|
||||
this.pendingList.push(key);
|
||||
|
||||
TestsBehatRuntime.log('PENDING+: ' + this.pendingList);
|
||||
TestingBehatRuntime.log('PENDING+: ' + this.pendingList);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ export class TestsBehatBlockingService {
|
|||
// Remove the key immediately.
|
||||
this.pendingList = this.pendingList.filter((x) => x !== key);
|
||||
|
||||
TestsBehatRuntime.log('PENDING-: ' + this.pendingList);
|
||||
TestingBehatRuntime.log('PENDING-: ' + this.pendingList);
|
||||
|
||||
// If the only thing left is DELAY, then remove that as well, later...
|
||||
if (this.pendingList.length === 1) {
|
||||
|
@ -124,7 +124,7 @@ export class TestsBehatBlockingService {
|
|||
// 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);
|
||||
TestingBehatRuntime.log('PENDING-: ' + this.pendingList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -221,16 +221,16 @@ export class TestsBehatBlockingService {
|
|||
|
||||
try {
|
||||
// Add to the list of pending requests.
|
||||
TestsBehatBlocking.block(key);
|
||||
TestingBehatBlocking.block(key);
|
||||
|
||||
// Detect when it finishes and remove it from the list.
|
||||
this.addEventListener('loadend', () => {
|
||||
TestsBehatBlocking.unblock(key);
|
||||
TestingBehatBlocking.unblock(key);
|
||||
});
|
||||
|
||||
return realOpen.apply(this, args);
|
||||
} catch (error) {
|
||||
TestsBehatBlocking.unblock(key);
|
||||
TestingBehatBlocking.unblock(key);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
@ -239,4 +239,4 @@ export class TestsBehatBlockingService {
|
|||
|
||||
}
|
||||
|
||||
export const TestsBehatBlocking = makeSingleton(TestsBehatBlockingService);
|
||||
export const TestingBehatBlocking = makeSingleton(TestingBehatBlockingService);
|
||||
|
|
|
@ -12,10 +12,11 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { NgZone } from '@singletons';
|
||||
import { TestBehatElementLocator } from './behat-runtime';
|
||||
import { makeSingleton, NgZone } from '@singletons';
|
||||
import { TestingBehatElementLocator, TestingBehatFindOptions } from './behat-runtime';
|
||||
|
||||
// Containers that block containers behind them.
|
||||
const blockingContainers = ['ION-ALERT', 'ION-POPOVER', 'ION-ACTION-SHEET', 'CORE-USER-TOURS-USER-TOUR', 'ION-PAGE'];
|
||||
|
@ -23,7 +24,8 @@ const blockingContainers = ['ION-ALERT', 'ION-POPOVER', 'ION-ACTION-SHEET', 'COR
|
|||
/**
|
||||
* Behat Dom Utils helper functions.
|
||||
*/
|
||||
export class TestsBehatDomUtils {
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TestingBehatDomUtilsService {
|
||||
|
||||
/**
|
||||
* Check if an element is visible.
|
||||
|
@ -32,7 +34,7 @@ export class TestsBehatDomUtils {
|
|||
* @param container Container.
|
||||
* @return Whether the element is visible or not.
|
||||
*/
|
||||
static isElementVisible(element: HTMLElement, container: HTMLElement): boolean {
|
||||
isElementVisible(element: HTMLElement, container: HTMLElement): boolean {
|
||||
if (element.getAttribute('aria-hidden') === 'true' || getComputedStyle(element).display === 'none') {
|
||||
return false;
|
||||
}
|
||||
|
@ -56,7 +58,7 @@ export class TestsBehatDomUtils {
|
|||
* @param container Container.
|
||||
* @return Whether the element is selected or not.
|
||||
*/
|
||||
static isElementSelected(element: HTMLElement, container: HTMLElement): boolean {
|
||||
isElementSelected(element: HTMLElement, container: HTMLElement): boolean {
|
||||
const ariaCurrent = element.getAttribute('aria-current');
|
||||
if (
|
||||
(ariaCurrent && ariaCurrent !== 'false') ||
|
||||
|
@ -79,9 +81,14 @@ export class TestsBehatDomUtils {
|
|||
*
|
||||
* @param container Parent element to search the element within
|
||||
* @param text Text to look for
|
||||
* @param options Search options.
|
||||
* @return Elements containing the given text with exact boolean.
|
||||
*/
|
||||
protected static findElementsBasedOnTextWithinWithExact(container: HTMLElement, text: string): ElementsWithExact[] {
|
||||
protected findElementsBasedOnTextWithinWithExact(
|
||||
container: HTMLElement,
|
||||
text: string,
|
||||
options: TestingBehatFindOptions,
|
||||
): ElementsWithExact[] {
|
||||
const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"], [placeholder*="${text}"]`;
|
||||
|
||||
const elements = Array.from(container.querySelectorAll<HTMLElement>(attributesSelector))
|
||||
|
@ -97,16 +104,23 @@ export class TestsBehatDomUtils {
|
|||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_DOCUMENT_FRAGMENT | NodeFilter.SHOW_TEXT, // eslint-disable-line no-bitwise
|
||||
{
|
||||
acceptNode: node => {
|
||||
if (node instanceof HTMLStyleElement ||
|
||||
if (
|
||||
node instanceof HTMLStyleElement ||
|
||||
node instanceof HTMLLinkElement ||
|
||||
node instanceof HTMLScriptElement) {
|
||||
node instanceof HTMLScriptElement
|
||||
) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
|
||||
if (node instanceof HTMLElement &&
|
||||
(node.getAttribute('aria-hidden') === 'true' ||
|
||||
node.getAttribute('aria-disabled') === 'true' ||
|
||||
getComputedStyle(node).display === 'none')) {
|
||||
if (!(node instanceof HTMLElement)) {
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
|
||||
if (options.onlyClickable && (node.getAttribute('aria-disabled') === 'true' || node.hasAttribute('disabled'))) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
|
||||
if (node.getAttribute('aria-hidden') === 'true' || getComputedStyle(node).display === 'none') {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
|
||||
|
@ -160,7 +174,7 @@ export class TestsBehatDomUtils {
|
|||
continue;
|
||||
}
|
||||
|
||||
elements.push(...this.findElementsBasedOnTextWithinWithExact(childNode, text));
|
||||
elements.push(...this.findElementsBasedOnTextWithinWithExact(childNode, text, options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +189,7 @@ export class TestsBehatDomUtils {
|
|||
* @param text Text to check.
|
||||
* @return If text matches any of the label attributes.
|
||||
*/
|
||||
protected static checkElementLabel(element: HTMLElement, text: string): boolean {
|
||||
protected checkElementLabel(element: HTMLElement, text: string): boolean {
|
||||
return element.title === text ||
|
||||
element.getAttribute('alt') === text ||
|
||||
element.getAttribute('aria-label') === text ||
|
||||
|
@ -187,10 +201,15 @@ export class TestsBehatDomUtils {
|
|||
*
|
||||
* @param container Parent element to search the element within.
|
||||
* @param text Text to look for.
|
||||
* @param options Search options.
|
||||
* @return Elements containing the given text.
|
||||
*/
|
||||
protected static findElementsBasedOnTextWithin(container: HTMLElement, text: string): HTMLElement[] {
|
||||
const elements = this.findElementsBasedOnTextWithinWithExact(container, text);
|
||||
protected findElementsBasedOnTextWithin(
|
||||
container: HTMLElement,
|
||||
text: string,
|
||||
options: TestingBehatFindOptions,
|
||||
): HTMLElement[] {
|
||||
const elements = this.findElementsBasedOnTextWithinWithExact(container, text, options);
|
||||
|
||||
// Give more relevance to exact matches.
|
||||
elements.sort((a, b) => Number(b.exact) - Number(a.exact));
|
||||
|
@ -206,7 +225,7 @@ export class TestsBehatDomUtils {
|
|||
* @param elements Elements list.
|
||||
* @return Top ancestors.
|
||||
*/
|
||||
protected static getTopAncestors(elements: HTMLElement[]): HTMLElement[] {
|
||||
protected getTopAncestors(elements: HTMLElement[]): HTMLElement[] {
|
||||
const uniqueElements = new Set(elements);
|
||||
|
||||
for (const element of uniqueElements) {
|
||||
|
@ -230,7 +249,7 @@ export class TestsBehatDomUtils {
|
|||
* @param element Element.
|
||||
* @return Parent element.
|
||||
*/
|
||||
protected static getParentElement(element: HTMLElement): HTMLElement | null {
|
||||
protected getParentElement(element: HTMLElement): HTMLElement | null {
|
||||
return element.parentElement ||
|
||||
(element.getRootNode() && (element.getRootNode() as ShadowRoot).host as HTMLElement) ||
|
||||
null;
|
||||
|
@ -244,7 +263,7 @@ export class TestsBehatDomUtils {
|
|||
* @param container Topmost container to search within.
|
||||
* @return Closest matching element.
|
||||
*/
|
||||
protected static getClosestMatching(element: HTMLElement, selector: string, container: HTMLElement | null): HTMLElement | null {
|
||||
protected getClosestMatching(element: HTMLElement, selector: string, container: HTMLElement | null): HTMLElement | null {
|
||||
if (element.matches(selector)) {
|
||||
return element;
|
||||
}
|
||||
|
@ -262,7 +281,7 @@ export class TestsBehatDomUtils {
|
|||
* @param containerName Whether to search inside the a container name.
|
||||
* @return Found top container elements.
|
||||
*/
|
||||
protected static getCurrentTopContainerElements(containerName: string): HTMLElement[] {
|
||||
protected getCurrentTopContainerElements(containerName: string): HTMLElement[] {
|
||||
const topContainers: HTMLElement[] = [];
|
||||
let containers = Array.from(document.querySelectorAll<HTMLElement>([
|
||||
'ion-alert.hydrated',
|
||||
|
@ -325,32 +344,33 @@ export class TestsBehatDomUtils {
|
|||
* 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.
|
||||
* @param options Search options.
|
||||
* @return First found element.
|
||||
*/
|
||||
static findElementBasedOnText(locator: TestBehatElementLocator, containerName = ''): HTMLElement {
|
||||
return this.findElementsBasedOnText(locator, containerName, true)[0];
|
||||
findElementBasedOnText(
|
||||
locator: TestingBehatElementLocator,
|
||||
options: TestingBehatFindOptions,
|
||||
): HTMLElement {
|
||||
return this.findElementsBasedOnText(locator, options)[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.
|
||||
* @param stopWhenFound Stop looking in containers once an element is found.
|
||||
* @param options Search options.
|
||||
* @return Found elements
|
||||
*/
|
||||
protected static findElementsBasedOnText(
|
||||
locator: TestBehatElementLocator,
|
||||
containerName = '',
|
||||
stopWhenFound = false,
|
||||
protected findElementsBasedOnText(
|
||||
locator: TestingBehatElementLocator,
|
||||
options: TestingBehatFindOptions,
|
||||
): HTMLElement[] {
|
||||
const topContainers = this.getCurrentTopContainerElements(containerName);
|
||||
const topContainers = this.getCurrentTopContainerElements(options.containerName);
|
||||
let elements: HTMLElement[] = [];
|
||||
|
||||
for (let i = 0; i < topContainers.length; i++) {
|
||||
elements = elements.concat(this.findElementsBasedOnTextInContainer(locator, topContainers[i]));
|
||||
if (stopWhenFound && elements.length) {
|
||||
elements = elements.concat(this.findElementsBasedOnTextInContainer(locator, topContainers[i], options));
|
||||
if (elements.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -363,16 +383,18 @@ export class TestsBehatDomUtils {
|
|||
*
|
||||
* @param locator Element locator.
|
||||
* @param topContainer Container to search in.
|
||||
* @param options Search options.
|
||||
* @return Found elements
|
||||
*/
|
||||
protected static findElementsBasedOnTextInContainer(
|
||||
locator: TestBehatElementLocator,
|
||||
protected findElementsBasedOnTextInContainer(
|
||||
locator: TestingBehatElementLocator,
|
||||
topContainer: HTMLElement,
|
||||
options: TestingBehatFindOptions,
|
||||
): HTMLElement[] {
|
||||
let container: HTMLElement | null = topContainer;
|
||||
|
||||
if (locator.within) {
|
||||
const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer);
|
||||
const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer, options);
|
||||
|
||||
if (withinElements.length === 0) {
|
||||
throw new Error('There was no match for within text');
|
||||
|
@ -390,7 +412,10 @@ export class TestsBehatDomUtils {
|
|||
}
|
||||
|
||||
if (topContainer && locator.near) {
|
||||
const nearElements = this.findElementsBasedOnTextInContainer(locator.near, topContainer);
|
||||
const nearElements = this.findElementsBasedOnTextInContainer(locator.near, topContainer, {
|
||||
...options,
|
||||
onlyClickable: false,
|
||||
});
|
||||
|
||||
if (nearElements.length === 0) {
|
||||
throw new Error('There was no match for near text');
|
||||
|
@ -412,7 +437,7 @@ export class TestsBehatDomUtils {
|
|||
break;
|
||||
}
|
||||
|
||||
const elements = this.findElementsBasedOnTextWithin(container, locator.text);
|
||||
const elements = this.findElementsBasedOnTextWithin(container, locator.text, options);
|
||||
|
||||
let filteredElements: HTMLElement[] = elements;
|
||||
|
||||
|
@ -442,7 +467,7 @@ export class TestsBehatDomUtils {
|
|||
*
|
||||
* @param element Element.
|
||||
*/
|
||||
protected static async ensureElementVisible(element: HTMLElement): Promise<DOMRect> {
|
||||
protected async ensureElementVisible(element: HTMLElement): Promise<DOMRect> {
|
||||
const initialRect = element.getBoundingClientRect();
|
||||
|
||||
element.scrollIntoView(false);
|
||||
|
@ -471,7 +496,7 @@ export class TestsBehatDomUtils {
|
|||
*
|
||||
* @param element Element to press.
|
||||
*/
|
||||
static async pressElement(element: HTMLElement): Promise<void> {
|
||||
async pressElement(element: HTMLElement): Promise<void> {
|
||||
await NgZone.run(async () => {
|
||||
const promise = new CorePromisedValue<void>();
|
||||
|
||||
|
@ -516,7 +541,7 @@ export class TestsBehatDomUtils {
|
|||
* @param element HTML to set.
|
||||
* @param value Value to be set.
|
||||
*/
|
||||
static async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> {
|
||||
async setElementValue(element: HTMLInputElement | HTMLElement, value: string): Promise<void> {
|
||||
await NgZone.run(async () => {
|
||||
const promise = new CorePromisedValue<void>();
|
||||
|
||||
|
@ -581,6 +606,8 @@ export class TestsBehatDomUtils {
|
|||
|
||||
}
|
||||
|
||||
export const TestingBehatDomUtils = makeSingleton(TestingBehatDomUtilsService);
|
||||
|
||||
type ElementsWithExact = {
|
||||
element: HTMLElement;
|
||||
exact: boolean;
|
||||
|
|
|
@ -12,127 +12,106 @@
|
|||
// 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 { TestingBehatDomUtils } from './behat-dom';
|
||||
import { TestingBehatBlocking } from './behat-blocking';
|
||||
import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes';
|
||||
import { CoreLoginHelperProvider } from '@features/login/services/login-helper';
|
||||
import { CoreConfig } from '@services/config';
|
||||
import { EnvironmentConfig } from '@/types/config';
|
||||
import { NgZone } from '@singletons';
|
||||
import { CoreNetwork } from '@services/network';
|
||||
import {
|
||||
CorePushNotifications,
|
||||
CorePushNotificationsNotificationBasicData,
|
||||
} from '@features/pushnotifications/services/pushnotifications';
|
||||
import { CoreCronDelegate } from '@services/cron';
|
||||
import { makeSingleton, NgZone } from '@singletons';
|
||||
import { CoreNetwork, CoreNetworkService } from '@services/network';
|
||||
import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications';
|
||||
import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron';
|
||||
import { CoreLoadingComponent } from '@components/loading/loading';
|
||||
import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||
import { CoreDom } from '@singletons/dom';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreCoursesDashboardPage } from '@features/courses/pages/dashboard/dashboard';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Behat runtime servive with public API.
|
||||
*/
|
||||
export class TestsBehatRuntime {
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TestingBehatRuntimeService {
|
||||
|
||||
protected initialized = false;
|
||||
|
||||
get cronDelegate(): CoreCronDelegateService {
|
||||
return CoreCronDelegate.instance;
|
||||
}
|
||||
|
||||
get customUrlSchemes(): CoreCustomURLSchemesProvider {
|
||||
return CoreCustomURLSchemes.instance;
|
||||
}
|
||||
|
||||
get network(): CoreNetworkService {
|
||||
return CoreNetwork.instance;
|
||||
}
|
||||
|
||||
get pushNotifications(): CorePushNotificationsProvider {
|
||||
return CorePushNotifications.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
pullToRefresh: TestsBehatRuntime.pullToRefresh,
|
||||
scrollTo: TestsBehatRuntime.scrollTo,
|
||||
setField: TestsBehatRuntime.setField,
|
||||
handleCustomURL: TestsBehatRuntime.handleCustomURL,
|
||||
notificationClicked: TestsBehatRuntime.notificationClicked,
|
||||
forceSyncExecution: TestsBehatRuntime.forceSyncExecution,
|
||||
waitLoadingToFinish: TestsBehatRuntime.waitLoadingToFinish,
|
||||
network: CoreNetwork.instance,
|
||||
};
|
||||
|
||||
if (!options) {
|
||||
init(options: TestingBehatInitOptions = {}): void {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.skipOnBoarding === true) {
|
||||
this.initialized = true;
|
||||
TestingBehatBlocking.init();
|
||||
|
||||
if (options.skipOnBoarding) {
|
||||
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);
|
||||
CoreConfig.patchEnvironment(options.configOverrides, { patchDefault: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a custom URL.
|
||||
* Check whether the service has been initialized or not.
|
||||
*
|
||||
* @param url Url to open.
|
||||
* @returns Whether the service has been initialized or not.
|
||||
*/
|
||||
hasInitialized(): boolean {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an operation inside the angular zone and return result.
|
||||
*
|
||||
* @param operation Operation callback.
|
||||
* @return OK if successful, or ERROR: followed by message.
|
||||
*/
|
||||
static async handleCustomURL(url: string): Promise<string> {
|
||||
async runInZone(operation: () => unknown, blocking: boolean = false): Promise<string> {
|
||||
const blockKey = blocking && TestingBehatBlocking.block();
|
||||
|
||||
try {
|
||||
await NgZone.run(async () => {
|
||||
await CoreCustomURLSchemes.handleCustomURL(url);
|
||||
});
|
||||
await NgZone.run(operation);
|
||||
|
||||
return 'OK';
|
||||
} catch (error) {
|
||||
return 'ERROR: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when a push notification is clicked. Redirect the user to the right state.
|
||||
*
|
||||
* @param data Notification data.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
static async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise<void> {
|
||||
const blockKey = TestsBehatBlocking.block();
|
||||
|
||||
try {
|
||||
await NgZone.run(async () => {
|
||||
await CorePushNotifications.notificationClicked(data);
|
||||
});
|
||||
} finally {
|
||||
TestsBehatBlocking.unblock(blockKey);
|
||||
blockKey && TestingBehatBlocking.unblock(blockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force execution of synchronization cron tasks without waiting for the scheduled time.
|
||||
* Please notice that some tasks may not be executed depending on the network connection and sync settings.
|
||||
*
|
||||
* @return Promise resolved if all handlers are executed successfully, rejected otherwise.
|
||||
*/
|
||||
static async forceSyncExecution(): Promise<void> {
|
||||
await NgZone.run(async () => {
|
||||
await CoreCronDelegate.forceSyncExecution();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait all controlled components to be rendered.
|
||||
*
|
||||
* @return Promise resolved when all components have been rendered.
|
||||
*/
|
||||
static async waitLoadingToFinish(): Promise<void> {
|
||||
async waitLoadingToFinish(): Promise<void> {
|
||||
await NgZone.run(async () => {
|
||||
const elements = Array.from(document.body.querySelectorAll<HTMLElement>('core-loading'))
|
||||
.filter((element) => CoreDom.isElementVisible(element));
|
||||
|
@ -148,28 +127,32 @@ export class TestsBehatRuntime {
|
|||
* @param button Type of button to press.
|
||||
* @return OK if successful, or ERROR: followed by message.
|
||||
*/
|
||||
static async pressStandard(button: string): Promise<string> {
|
||||
async pressStandard(button: string): Promise<string> {
|
||||
this.log('Action - Click standard button: ' + button);
|
||||
|
||||
// Find button
|
||||
let foundButton: HTMLElement | undefined;
|
||||
const options: TestingBehatFindOptions = {
|
||||
onlyClickable: true,
|
||||
containerName: '',
|
||||
};
|
||||
|
||||
switch (button) {
|
||||
case 'back':
|
||||
foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'Back' });
|
||||
foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Back' }, options);
|
||||
break;
|
||||
case 'main menu': // Deprecated name.
|
||||
case 'more menu':
|
||||
foundButton = TestsBehatDomUtils.findElementBasedOnText({
|
||||
foundButton = TestingBehatDomUtils.findElementBasedOnText({
|
||||
text: 'More',
|
||||
selector: 'ion-tab-button',
|
||||
});
|
||||
}, options);
|
||||
break;
|
||||
case 'user menu' :
|
||||
foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'User account' });
|
||||
foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'User account' }, options);
|
||||
break;
|
||||
case 'page menu':
|
||||
foundButton = TestsBehatDomUtils.findElementBasedOnText({ text: 'Display options' });
|
||||
foundButton = TestingBehatDomUtils.findElementBasedOnText({ text: 'Display options' }, options);
|
||||
break;
|
||||
default:
|
||||
return 'ERROR: Unsupported standard button type';
|
||||
|
@ -180,7 +163,7 @@ export class TestsBehatRuntime {
|
|||
}
|
||||
|
||||
// Click button
|
||||
await TestsBehatDomUtils.pressElement(foundButton);
|
||||
await TestingBehatDomUtils.pressElement(foundButton);
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
@ -190,7 +173,7 @@ export class TestsBehatRuntime {
|
|||
*
|
||||
* @return OK if successful, or ERROR: followed by message
|
||||
*/
|
||||
static closePopup(): string {
|
||||
closePopup(): string {
|
||||
this.log('Action - Close popup');
|
||||
|
||||
let backdrops = Array.from(document.querySelectorAll('ion-backdrop'));
|
||||
|
@ -206,7 +189,7 @@ export class TestsBehatRuntime {
|
|||
backdrop.click();
|
||||
|
||||
// Mark busy until the click finishes processing.
|
||||
TestsBehatBlocking.delay();
|
||||
TestingBehatBlocking.delay();
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
@ -215,20 +198,24 @@ export class TestsBehatRuntime {
|
|||
* 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.
|
||||
* @param options Search options.
|
||||
* @return OK if successful, or ERROR: followed by message
|
||||
*/
|
||||
static find(locator: TestBehatElementLocator, containerName: string): string {
|
||||
this.log('Action - Find', { locator, containerName });
|
||||
find(locator: TestingBehatElementLocator, options: Partial<TestingBehatFindOptions> = {}): string {
|
||||
this.log('Action - Find', { locator, ...options });
|
||||
|
||||
try {
|
||||
const element = TestsBehatDomUtils.findElementBasedOnText(locator, containerName);
|
||||
const element = TestingBehatDomUtils.findElementBasedOnText(locator, {
|
||||
onlyClickable: false,
|
||||
containerName: '',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!element) {
|
||||
return 'ERROR: No element matches locator to find.';
|
||||
}
|
||||
|
||||
this.log('Action - Found', { locator, containerName, element });
|
||||
this.log('Action - Found', { locator, element, ...options });
|
||||
|
||||
return 'OK';
|
||||
} catch (error) {
|
||||
|
@ -242,11 +229,11 @@ export class TestsBehatRuntime {
|
|||
* @param locator Element locator.
|
||||
* @return OK if successful, or ERROR: followed by message
|
||||
*/
|
||||
static scrollTo(locator: TestBehatElementLocator): string {
|
||||
scrollTo(locator: TestingBehatElementLocator): string {
|
||||
this.log('Action - scrollTo', { locator });
|
||||
|
||||
try {
|
||||
let element = TestsBehatDomUtils.findElementBasedOnText(locator);
|
||||
let element = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: false, containerName: '' });
|
||||
|
||||
if (!element) {
|
||||
return 'ERROR: No element matches element to scroll to.';
|
||||
|
@ -269,7 +256,7 @@ export class TestsBehatRuntime {
|
|||
*
|
||||
* @return OK if successful, or ERROR: followed by message
|
||||
*/
|
||||
static async loadMoreItems(): Promise<string> {
|
||||
async loadMoreItems(): Promise<string> {
|
||||
this.log('Action - loadMoreItems');
|
||||
|
||||
try {
|
||||
|
@ -316,13 +303,13 @@ export class TestsBehatRuntime {
|
|||
* @param locator Element locator.
|
||||
* @return YES or NO if successful, or ERROR: followed by message
|
||||
*/
|
||||
static isSelected(locator: TestBehatElementLocator): string {
|
||||
isSelected(locator: TestingBehatElementLocator): string {
|
||||
this.log('Action - Is Selected', locator);
|
||||
|
||||
try {
|
||||
const element = TestsBehatDomUtils.findElementBasedOnText(locator);
|
||||
const element = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: false, containerName: '' });
|
||||
|
||||
return TestsBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO';
|
||||
return TestingBehatDomUtils.isElementSelected(element, document.body) ? 'YES' : 'NO';
|
||||
} catch (error) {
|
||||
return 'ERROR: ' + error.message;
|
||||
}
|
||||
|
@ -334,17 +321,17 @@ export class TestsBehatRuntime {
|
|||
* @param locator Element locator.
|
||||
* @return OK if successful, or ERROR: followed by message
|
||||
*/
|
||||
static async press(locator: TestBehatElementLocator): Promise<string> {
|
||||
async press(locator: TestingBehatElementLocator): Promise<string> {
|
||||
this.log('Action - Press', locator);
|
||||
|
||||
try {
|
||||
const found = TestsBehatDomUtils.findElementBasedOnText(locator);
|
||||
const found = TestingBehatDomUtils.findElementBasedOnText(locator, { onlyClickable: true, containerName: '' });
|
||||
|
||||
if (!found) {
|
||||
return 'ERROR: No element matches locator to press.';
|
||||
}
|
||||
|
||||
await TestsBehatDomUtils.pressElement(found);
|
||||
await TestingBehatDomUtils.pressElement(found);
|
||||
|
||||
return 'OK';
|
||||
} catch (error) {
|
||||
|
@ -357,7 +344,7 @@ export class TestsBehatRuntime {
|
|||
*
|
||||
* @return OK if successful, or ERROR: followed by message
|
||||
*/
|
||||
static async pullToRefresh(): Promise<string> {
|
||||
async pullToRefresh(): Promise<string> {
|
||||
this.log('Action - pullToRefresh');
|
||||
|
||||
try {
|
||||
|
@ -390,11 +377,11 @@ export class TestsBehatRuntime {
|
|||
*
|
||||
* @return OK: followed by header text if successful, or ERROR: followed by message.
|
||||
*/
|
||||
static getHeader(): string {
|
||||
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));
|
||||
titles = titles.filter((title) => TestingBehatDomUtils.isElementVisible(title, document.body));
|
||||
|
||||
if (titles.length > 1) {
|
||||
return 'ERROR: Too many possible titles ('+titles.length+').';
|
||||
|
@ -416,18 +403,19 @@ export class TestsBehatRuntime {
|
|||
* @param value New value
|
||||
* @return OK or ERROR: followed by message
|
||||
*/
|
||||
static async setField(field: string, value: string): Promise<string> {
|
||||
async setField(field: string, value: string): Promise<string> {
|
||||
this.log('Action - Set field ' + field + ' to: ' + value);
|
||||
|
||||
const found: HTMLElement | HTMLInputElement = TestsBehatDomUtils.findElementBasedOnText(
|
||||
const found: HTMLElement | HTMLInputElement = TestingBehatDomUtils.findElementBasedOnText(
|
||||
{ text: field, selector: 'input, textarea, [contenteditable="true"], ion-select' },
|
||||
{ onlyClickable: false, containerName: '' },
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
return 'ERROR: No element matches field to set.';
|
||||
}
|
||||
|
||||
await TestsBehatDomUtils.setElementValue(found, value);
|
||||
await TestingBehatDomUtils.setElementValue(found, value);
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
|
@ -439,7 +427,7 @@ export class TestsBehatRuntime {
|
|||
* @param className Constructor class name
|
||||
* @return Component instance
|
||||
*/
|
||||
static getAngularInstance<T = unknown>(selector: string, className: string): T | null {
|
||||
getAngularInstance<T = unknown>(selector: string, className: string): T | null {
|
||||
this.log('Action - Get Angular instance ' + selector + ', ' + className);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -456,7 +444,7 @@ export class TestsBehatRuntime {
|
|||
* 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 {
|
||||
log(...args: unknown[]): void {
|
||||
const now = new Date();
|
||||
const nowFormatted = String(now.getHours()).padStart(2, '0') + ':' +
|
||||
String(now.getMinutes()).padStart(2, '0') + ':' +
|
||||
|
@ -468,24 +456,29 @@ export class TestsBehatRuntime {
|
|||
|
||||
}
|
||||
|
||||
export const TestingBehatRuntime = makeSingleton(TestingBehatRuntimeService);
|
||||
|
||||
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 = {
|
||||
export type TestingBehatFindOptions = {
|
||||
containerName: string;
|
||||
onlyClickable: boolean;
|
||||
};
|
||||
|
||||
export type TestingBehatElementLocator = {
|
||||
text: string;
|
||||
within?: TestBehatElementLocator;
|
||||
near?: TestBehatElementLocator;
|
||||
within?: TestingBehatElementLocator;
|
||||
near?: TestingBehatElementLocator;
|
||||
selector?: string;
|
||||
};
|
||||
|
||||
export type TestsBehatInitOptions = {
|
||||
export type TestingBehatInitOptions = {
|
||||
skipOnBoarding?: boolean;
|
||||
configOverrides?: Partial<EnvironmentConfig>;
|
||||
};
|
||||
|
|
|
@ -18,4 +18,4 @@ import { NgModule } from '@angular/core';
|
|||
* Stub used in production to avoid including testing code in production bundles.
|
||||
*/
|
||||
@NgModule({})
|
||||
export class BehatTestingModule {}
|
||||
export class TestingModule {}
|
|
@ -12,21 +12,25 @@
|
|||
// 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 { CoreDB, CoreDbProvider } from '@services/db';
|
||||
import { TestingBehatRuntime, TestingBehatRuntimeService } from './services/behat-runtime';
|
||||
|
||||
type AutomatedTestsWindow = Window & {
|
||||
dbProvider?: CoreDbProvider;
|
||||
behat?: TestingBehatRuntimeService;
|
||||
};
|
||||
|
||||
function initializeAutomatedTestsWindow(window: AutomatedTestsWindow) {
|
||||
window.dbProvider = CoreDB.instance;
|
||||
}
|
||||
|
||||
export default function(): void {
|
||||
if (!CoreAppProvider.isAutomated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeAutomatedTestsWindow(window);
|
||||
window.behat = TestingBehatRuntime.instance;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
{ provide: APP_INITIALIZER, multi: true, useValue: () => initializeAutomatedTestsWindow(window) },
|
||||
],
|
||||
})
|
||||
export class TestingModule {}
|
|
@ -0,0 +1,16 @@
|
|||
@app @javascript
|
||||
Feature: It has a Behat runtime with testing helpers.
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username |
|
||||
| student1 |
|
||||
|
||||
Scenario: Finds and presses elements
|
||||
Given I entered the app as "student1"
|
||||
When I set the following fields to these values in the app:
|
||||
| Search by activity type or name | Foo bar |
|
||||
Then I should find "Search" "button" in the app
|
||||
And I should find "Clear search" in the app
|
||||
And I should be able to press "Search" "button" in the app
|
||||
But I should not be able to press "Clear search" in the app
|
Loading…
Reference in New Issue