Merge pull request #3914 from NoelDeMartin/MOBILE-4496
MOBILE-4496: Improve Behat CI
This commit is contained in:
		
						commit
						c60f792927
					
				
							
								
								
									
										275
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										275
									
								
								.github/workflows/acceptance.yml
									
									
									
									
										vendored
									
									
								
							@ -5,8 +5,6 @@ on:
 | 
			
		||||
    inputs:
 | 
			
		||||
      behat_tags:
 | 
			
		||||
        description: 'Behat tags to execute'
 | 
			
		||||
        required: true
 | 
			
		||||
        default: '~@performance'
 | 
			
		||||
      moodle_branch:
 | 
			
		||||
        description: 'Moodle branch'
 | 
			
		||||
        required: true
 | 
			
		||||
@ -14,76 +12,217 @@ on:
 | 
			
		||||
      moodle_repository:
 | 
			
		||||
        description: 'Moodle repository'
 | 
			
		||||
        required: true
 | 
			
		||||
        default: 'https://github.com/moodle/moodle'
 | 
			
		||||
        default: 'https://github.com/moodle/moodle.git'
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: [ main, v*.x ]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  behat:
 | 
			
		||||
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    env:
 | 
			
		||||
      MOODLE_DOCKER_DB: pgsql
 | 
			
		||||
      MOODLE_DOCKER_BROWSER: chrome
 | 
			
		||||
      MOODLE_DOCKER_PHP_VERSION: '8.1'
 | 
			
		||||
      MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'main' }}
 | 
			
		||||
      MOODLE_REPOSITORY: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle' }}
 | 
			
		||||
      BEHAT_TAGS: ${{ github.event.inputs.behat_tags || '~@performance' }}
 | 
			
		||||
    outputs:
 | 
			
		||||
      tags: ${{ steps.set-tags.outputs.tags }}
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v4
 | 
			
		||||
    - uses: actions/setup-node@v4
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.nvmrc'
 | 
			
		||||
    - name: Additional checkouts
 | 
			
		||||
      run: |
 | 
			
		||||
        git clone --branch $MOODLE_BRANCH --depth 1 $MOODLE_REPOSITORY $GITHUB_WORKSPACE/moodle
 | 
			
		||||
        git clone --branch main --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
 | 
			
		||||
    - name: Install npm packages
 | 
			
		||||
      run: npm ci --no-audit
 | 
			
		||||
    - name: Create Behat faildumps folder
 | 
			
		||||
      run: |
 | 
			
		||||
        mkdir moodle/behatfaildumps
 | 
			
		||||
        chmod 777 moodle/behatfaildumps
 | 
			
		||||
    - name: Install Behat Snapshots plugin
 | 
			
		||||
      run: git clone --branch main --depth 1 https://github.com/NoelDeMartin/moodle-local_behatsnapshots $GITHUB_WORKSPACE/moodle/local/behatsnapshots
 | 
			
		||||
    - name: Generate Behat tests plugin
 | 
			
		||||
      run: |
 | 
			
		||||
        export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
 | 
			
		||||
        npx gulp behat
 | 
			
		||||
    - name: Configure & launch Moodle with Docker
 | 
			
		||||
      run: |
 | 
			
		||||
        export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
 | 
			
		||||
        cp $GITHUB_WORKSPACE/moodle-docker/config.docker-template.php $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "69c\$CFG->behat_faildump_path = '/var/www/html/behatfaildumps';" $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "69i\$CFG->behat_increasetimeout = 2;" $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "69i\$CFG->behat_ionic_wwwroot = 'http://moodleapp';" $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        echo "define('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER', 'http://bbbmockserver/hash' . sha1(\$CFG->behat_wwwroot));" >> $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose pull
 | 
			
		||||
        $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose up -d
 | 
			
		||||
        $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-wait-for-db
 | 
			
		||||
    - name: Compile & launch app with Docker
 | 
			
		||||
      run: |
 | 
			
		||||
        docker build --build-arg build_command="npm run build:test" -t moodlehq/moodleapp:behat .
 | 
			
		||||
        docker run -d --rm --name moodleapp moodlehq/moodleapp:behat
 | 
			
		||||
        docker network connect moodle-docker_default moodleapp --alias moodleapp
 | 
			
		||||
        docker run --detach --name bbbmockserver --network moodle-docker_default moodlehq/bigbluebutton_mock:latest
 | 
			
		||||
    - name: Init Behat
 | 
			
		||||
      run: |
 | 
			
		||||
        export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
 | 
			
		||||
         $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/init.php --parallel=8 --optimize-runs='@app&&~@local&&$BEHAT_TAGS'"
 | 
			
		||||
    - name: Run Behat tests
 | 
			
		||||
      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&&~@local&&$BEHAT_TAGS' --auto-rerun=3"
 | 
			
		||||
    - name: Upload Snapshot failures
 | 
			
		||||
      uses: actions/upload-artifact@v3
 | 
			
		||||
      if: ${{ failure() }}
 | 
			
		||||
      with:
 | 
			
		||||
        name: snapshot_failures
 | 
			
		||||
        path: moodle/local/moodleappbehat/tests/behat/snapshots/failures/*
 | 
			
		||||
    - name: Upload Behat failures
 | 
			
		||||
      uses: actions/upload-artifact@v3
 | 
			
		||||
      if: ${{ failure() }}
 | 
			
		||||
      with:
 | 
			
		||||
        name: behat_failures
 | 
			
		||||
        path: moodle/behatfaildumps
 | 
			
		||||
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: app
 | 
			
		||||
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: 'app/.nvmrc'
 | 
			
		||||
 | 
			
		||||
      - name: Install npm dependencies
 | 
			
		||||
        working-directory: app
 | 
			
		||||
        run: npm ci --no-audit
 | 
			
		||||
 | 
			
		||||
      - name: Build app
 | 
			
		||||
        working-directory: app
 | 
			
		||||
        run: npm run build:test
 | 
			
		||||
 | 
			
		||||
      - name: Build Behat plugin
 | 
			
		||||
        working-directory: app
 | 
			
		||||
        run: ./scripts/build-behat-plugin.js ../plugin
 | 
			
		||||
 | 
			
		||||
      - name: Prepare Behat tags
 | 
			
		||||
        id: set-tags
 | 
			
		||||
        run: |
 | 
			
		||||
          if [ -z $BEHAT_TAGS ]; then
 | 
			
		||||
            tags=(
 | 
			
		||||
              "@addon_block_timeline"
 | 
			
		||||
              "@addon_calendar"
 | 
			
		||||
              "@addon_competency"
 | 
			
		||||
              "@addon_messages"
 | 
			
		||||
              "@addon_mod_assign"
 | 
			
		||||
              "@addon_mod_bigbluebuttonbn"
 | 
			
		||||
              "@addon_mod_book"
 | 
			
		||||
              "@addon_mod_chat"
 | 
			
		||||
              "@addon_mod_choice"
 | 
			
		||||
              "@addon_mod_data"
 | 
			
		||||
              "@addon_mod_feedback"
 | 
			
		||||
              "@addon_mod_forum"
 | 
			
		||||
              "@addon_mod_glossary"
 | 
			
		||||
              "@addon_mod_lesson"
 | 
			
		||||
              "@addon_mod_quiz"
 | 
			
		||||
              "@addon_mod_scorm"
 | 
			
		||||
              "@addon_mod_survey"
 | 
			
		||||
              "@addon_mod_workshop"
 | 
			
		||||
              "@addon_notifications"
 | 
			
		||||
              "@core"
 | 
			
		||||
              "@core_comments"
 | 
			
		||||
              "@core_course"
 | 
			
		||||
              "@core_courses"
 | 
			
		||||
              "@core_grades"
 | 
			
		||||
              "@core_login"
 | 
			
		||||
              "@core_mainmenu"
 | 
			
		||||
              "@core_reminders"
 | 
			
		||||
              "@core_reportbuilder"
 | 
			
		||||
              "@core_search"
 | 
			
		||||
              "@core_settings"
 | 
			
		||||
              "@core_siteplugins"
 | 
			
		||||
              "@core_user"
 | 
			
		||||
              "@core_usertour"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            tags_json="["
 | 
			
		||||
            for tag in "${tags[@]}"; do
 | 
			
		||||
              tags_json+="\"$tag\","
 | 
			
		||||
            done
 | 
			
		||||
            tags_json="${tags_json%?}"
 | 
			
		||||
            tags_json+="]"
 | 
			
		||||
            echo "tags=$tags_json" >> $GITHUB_OUTPUT;
 | 
			
		||||
          else
 | 
			
		||||
            echo "tags=[\"$BEHAT_TAGS\"]" >> $GITHUB_OUTPUT;
 | 
			
		||||
          fi
 | 
			
		||||
        env:
 | 
			
		||||
          BEHAT_TAGS: ${{ github.event.inputs.behat_tags }}
 | 
			
		||||
 | 
			
		||||
      # We need to upload an artifact so that the download-artifact action
 | 
			
		||||
      # in the "complete" job does not fail if no other artifacts were uploaded.
 | 
			
		||||
      - name: Create build logs
 | 
			
		||||
        run: touch logs.txt
 | 
			
		||||
 | 
			
		||||
      - name: Upload build logs
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: build
 | 
			
		||||
          path: logs.txt
 | 
			
		||||
 | 
			
		||||
      - uses: actions/cache/save@v4
 | 
			
		||||
        with:
 | 
			
		||||
            key: build-${{ github.sha }}
 | 
			
		||||
            path: |
 | 
			
		||||
              app/node_modules/**/*
 | 
			
		||||
              app/www/**/*
 | 
			
		||||
              plugin/**/*
 | 
			
		||||
 | 
			
		||||
  behat:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: build
 | 
			
		||||
    continue-on-error: true
 | 
			
		||||
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        tags: ${{ fromJSON(needs.build.outputs.tags) }}
 | 
			
		||||
 | 
			
		||||
    services:
 | 
			
		||||
 | 
			
		||||
      postgres:
 | 
			
		||||
        image: postgres:13
 | 
			
		||||
        env:
 | 
			
		||||
          POSTGRES_USER: 'postgres'
 | 
			
		||||
          POSTGRES_HOST_AUTH_METHOD: 'trust'
 | 
			
		||||
        ports:
 | 
			
		||||
          - 5432:5432
 | 
			
		||||
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: app
 | 
			
		||||
 | 
			
		||||
      - uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: 'app/.nvmrc'
 | 
			
		||||
 | 
			
		||||
      - uses: shivammathur/setup-php@v2
 | 
			
		||||
        with:
 | 
			
		||||
          php-version: 8.1
 | 
			
		||||
          ini-values: max_input_vars=5000
 | 
			
		||||
          coverage: none
 | 
			
		||||
 | 
			
		||||
      - uses: actions/cache/restore@v4
 | 
			
		||||
        with:
 | 
			
		||||
          key: build-${{ github.sha }}
 | 
			
		||||
          path: |
 | 
			
		||||
            app/node_modules/**/*
 | 
			
		||||
            app/www/**/*
 | 
			
		||||
            plugin/**/*
 | 
			
		||||
 | 
			
		||||
      - name: Launch Docker images
 | 
			
		||||
        working-directory: app
 | 
			
		||||
        run: |
 | 
			
		||||
          docker run -d --rm -p 8001:80 --name moodleapp -v ./www:/usr/share/nginx/html -v ./nginx.conf:/etc/nginx/conf.d/default.conf nginx:alpine
 | 
			
		||||
          docker run -d --rm -p 8002:80 --name bigbluebutton moodlehq/bigbluebutton_mock:latest
 | 
			
		||||
 | 
			
		||||
      - name: Initialise moodle-plugin-ci
 | 
			
		||||
        run: |
 | 
			
		||||
          composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4.3
 | 
			
		||||
          echo $(cd ci/bin; pwd) >> $GITHUB_PATH
 | 
			
		||||
          echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH
 | 
			
		||||
          sudo locale-gen en_AU.UTF-8
 | 
			
		||||
          echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      - name: Install Behat Snapshots plugin
 | 
			
		||||
        run: moodle-plugin-ci add-plugin NoelDeMartin/moodle-local_behatsnapshots
 | 
			
		||||
 | 
			
		||||
      - name: Install moodle-plugin-ci
 | 
			
		||||
        run: moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1
 | 
			
		||||
        env:
 | 
			
		||||
          DB: pgsql
 | 
			
		||||
          MOODLE_BRANCH: ${{ github.event.inputs.moodle_branch || 'main' }}
 | 
			
		||||
          MOODLE_REPO: ${{ github.event.inputs.moodle_repository || 'https://github.com/moodle/moodle.git' }}
 | 
			
		||||
          MOODLE_BEHAT_IONIC_WWWROOT: http://localhost:8001
 | 
			
		||||
          MOODLE_BEHAT_DEFAULT_BROWSER: chrome
 | 
			
		||||
 | 
			
		||||
      - name: Update config
 | 
			
		||||
        run: moodle-plugin-ci add-config 'define("TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER", "http://localhost:8002/hash" . sha1($CFG->wwwroot));'
 | 
			
		||||
 | 
			
		||||
      - name: Run Behat tests
 | 
			
		||||
        run: moodle-plugin-ci behat --auto-rerun 3 --profile chrome --tags="@app&&~@local&&$BEHAT_TAGS"
 | 
			
		||||
        env:
 | 
			
		||||
          BEHAT_TAGS: ${{ matrix.tags }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload Snapshot failures
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        if: ${{ failure() }}
 | 
			
		||||
        with:
 | 
			
		||||
          name: snapshot_failures-${{ matrix.tags }}
 | 
			
		||||
          path: moodle/local/moodleappbehat/tests/behat/snapshots/failures/*
 | 
			
		||||
 | 
			
		||||
      - name: Upload Behat failures
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        if: ${{ failure() }}
 | 
			
		||||
        with:
 | 
			
		||||
          name: behat_failures-${{ matrix.tags }}
 | 
			
		||||
          path: moodledata/behat_dump/*
 | 
			
		||||
 | 
			
		||||
  complete:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [behat]
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: artifacts
 | 
			
		||||
 | 
			
		||||
      - name: Check failure artifacts
 | 
			
		||||
        run: |
 | 
			
		||||
          rm ./artifacts/build -rf
 | 
			
		||||
          if [ -n "$(ls -A ./artifacts)" ]; then
 | 
			
		||||
            echo "There were some failures in the previous jobs"
 | 
			
		||||
            exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										58
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/performance.yml
									
									
									
									
										vendored
									
									
								
							@ -1,58 +0,0 @@
 | 
			
		||||
name: Performance
 | 
			
		||||
 | 
			
		||||
on: [ workflow_dispatch ]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  performance:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    env:
 | 
			
		||||
      MOODLE_DOCKER_DB: pgsql
 | 
			
		||||
      MOODLE_DOCKER_BROWSER: chrome
 | 
			
		||||
      MOODLE_DOCKER_PHP_VERSION: '8.0'
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v4
 | 
			
		||||
    - uses: actions/setup-node@v4
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.nvmrc'
 | 
			
		||||
    - name: Additional checkouts
 | 
			
		||||
      run: |
 | 
			
		||||
        git clone --branch main --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle
 | 
			
		||||
        git clone --branch main --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
 | 
			
		||||
    - name: Install npm packages
 | 
			
		||||
      run: npm ci --no-audit
 | 
			
		||||
    - name: Generate Behat tests plugin
 | 
			
		||||
      run: |
 | 
			
		||||
        export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
 | 
			
		||||
        npx gulp behat
 | 
			
		||||
    - name: Configure & launch Moodle with Docker
 | 
			
		||||
      run: |
 | 
			
		||||
        export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
 | 
			
		||||
        cp $GITHUB_WORKSPACE/moodle-docker/config.docker-template.php $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "67i\        'capabilities' => [" $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "68i\            'extra_capabilities' => [" $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "69i\                'goog:loggingPrefs' => ['performance' => 'ALL']," $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "70i\                'chromeOptions' => ['perfLoggingPrefs' => ['traceCategories' => 'devtools.timeline']]," $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "71i\            ]," $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "72i\        ]," $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        sed -i "84i\$CFG->behat_ionic_wwwroot = 'http://moodleapp';" $GITHUB_WORKSPACE/moodle/config.php
 | 
			
		||||
        $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose pull
 | 
			
		||||
        $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose up -d
 | 
			
		||||
        $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-wait-for-db
 | 
			
		||||
    - name: Compile & launch app with Docker
 | 
			
		||||
      run: |
 | 
			
		||||
        docker build --build-arg build_command="npm run build:test" -t moodlehq/moodleapp:performance .
 | 
			
		||||
        docker run -d --rm --name moodleapp moodlehq/moodleapp:performance
 | 
			
		||||
        docker network connect moodle-docker_default moodleapp --alias moodleapp
 | 
			
		||||
    - name: Init Behat
 | 
			
		||||
      run: |
 | 
			
		||||
        export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
 | 
			
		||||
        $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/init.php"
 | 
			
		||||
    - name: Run performance tests
 | 
			
		||||
      run: |
 | 
			
		||||
        export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
 | 
			
		||||
        for i in {0..2}
 | 
			
		||||
        do
 | 
			
		||||
          $GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags="@performance" --auto-rerun"
 | 
			
		||||
        done
 | 
			
		||||
    - name: Show performance results
 | 
			
		||||
      run: node ./scripts/print-performance-measures.js $GITHUB_WORKSPACE/moodle/behatperformancemeasures/
 | 
			
		||||
@ -57,7 +57,6 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->featurepath = dirname($feature->getFile());
 | 
			
		||||
        $this->configure_performance_logs();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -82,23 +81,6 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
        $this->enter_site();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Configure performance logs.
 | 
			
		||||
     */
 | 
			
		||||
    protected function configure_performance_logs() {
 | 
			
		||||
        global $CFG;
 | 
			
		||||
 | 
			
		||||
        $performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null;
 | 
			
		||||
 | 
			
		||||
        if ($performanceLogs !== 'ALL') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Enable DB Logging only for app tests with performance logs activated.
 | 
			
		||||
        $this->getSession()->visit($this->get_app_url() . '/assets/env.json');
 | 
			
		||||
        $this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether the current page is the login form.
 | 
			
		||||
     */
 | 
			
		||||
@ -1073,29 +1055,83 @@ class behat_app extends behat_app_helper {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send pending notifications.
 | 
			
		||||
     *
 | 
			
		||||
     * @Then /^I flush pending notifications in the app$/
 | 
			
		||||
     */
 | 
			
		||||
    public function i_flush_notifications() {
 | 
			
		||||
        $this->runtime_js("flushNotifications()");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a notification has been triggered and is present.
 | 
			
		||||
     *
 | 
			
		||||
     * @Then /^a notification with title (".+") is( not)? present in the app$/
 | 
			
		||||
     * @Then /^a notification with title (".+") should( not)? be present in the app$/
 | 
			
		||||
     * @param string $title Notification title
 | 
			
		||||
     * @param bool $not Whether assert that the notification was not found
 | 
			
		||||
     */
 | 
			
		||||
    public function notification_present_in_the_app(string $title, bool $not = false) {
 | 
			
		||||
        $result = $this->runtime_js("notificationIsPresentWithText($title)");
 | 
			
		||||
        $this->spin(function() use ($not, $title) {
 | 
			
		||||
            $result = $this->runtime_js("notificationIsPresentWithText($title)");
 | 
			
		||||
 | 
			
		||||
        if ($not && $result === 'YES') {
 | 
			
		||||
            throw new ExpectationException("Notification is present", $this->getSession()->getDriver());
 | 
			
		||||
            if ($not && $result === 'YES') {
 | 
			
		||||
                throw new ExpectationException("Notification is present", $this->getSession()->getDriver());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!$not && $result === 'NO') {
 | 
			
		||||
                throw new ExpectationException("Notification is not present", $this->getSession()->getDriver());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($result !== 'YES' && $result !== 'NO') {
 | 
			
		||||
                throw new DriverException('Error checking notification - ' . $result);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a notification has been scheduled.
 | 
			
		||||
     *
 | 
			
		||||
     * @Then /^a notification with title (".+") should( not)? be scheduled(?: (\d+) minutes before the "(.+)" assignment due date)? in the app$/
 | 
			
		||||
     * @param string $title Notification title
 | 
			
		||||
     * @param bool $not Whether assert that the notification was not scheduled
 | 
			
		||||
     * @param int $minutes Minutes before the assignment at which the notification was scheduled
 | 
			
		||||
     * @param string $assignment Assignment for which the notification was scheduled
 | 
			
		||||
     */
 | 
			
		||||
    public function notification_scheduled_in_the_app(string $title, bool $not = false, ?int $minutes = null, ?string $assignment = null) {
 | 
			
		||||
        if (!is_null($minutes)) {
 | 
			
		||||
            global $DB;
 | 
			
		||||
 | 
			
		||||
            $assign = $DB->get_record('assign', ['name' => $assignment]);
 | 
			
		||||
 | 
			
		||||
            if (!$assign) {
 | 
			
		||||
                throw new ExpectationException("Couldn't find '$assignment' assignment", $this->getSession()->getDriver());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $date = ($assign->duedate - $minutes * 60) * 1000;
 | 
			
		||||
        } else {
 | 
			
		||||
            $date = 'undefined';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$not && $result === 'NO') {
 | 
			
		||||
            throw new ExpectationException("Notification is not present", $this->getSession()->getDriver());
 | 
			
		||||
        }
 | 
			
		||||
        $this->spin(function() use ($not, $title, $date) {
 | 
			
		||||
            $result = $this->runtime_js("notificationIsScheduledWithText($title, $date)");
 | 
			
		||||
 | 
			
		||||
        if ($result !== 'YES' && $result !== 'NO') {
 | 
			
		||||
            throw new DriverException('Error checking notification - ' . $result);
 | 
			
		||||
        }
 | 
			
		||||
            if ($not && $result === 'YES') {
 | 
			
		||||
                throw new ExpectationException("Notification is scheduled", $this->getSession()->getDriver());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
            if (!$not && $result === 'NO') {
 | 
			
		||||
                throw new ExpectationException("Notification is not scheduled", $this->getSession()->getDriver());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($result !== 'YES' && $result !== 'NO') {
 | 
			
		||||
                throw new DriverException('Error checking scheduled notification - ' . $result);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -292,6 +292,8 @@ class behat_app_helper extends behat_base {
 | 
			
		||||
    /**
 | 
			
		||||
     * Replaces $WWWROOT for the url of the Moodle site.
 | 
			
		||||
     *
 | 
			
		||||
     * Using $WWWROOTPATTERN will replace it for a regex pattern.
 | 
			
		||||
     *
 | 
			
		||||
     * @Transform /^(.*\$WWWROOT.*)$/
 | 
			
		||||
     * @param string $text Text.
 | 
			
		||||
     * @return string
 | 
			
		||||
@ -299,7 +301,10 @@ class behat_app_helper extends behat_base {
 | 
			
		||||
    public function replace_wwwroot($text) {
 | 
			
		||||
        global $CFG;
 | 
			
		||||
 | 
			
		||||
        return str_replace('$WWWROOT', $CFG->behat_wwwroot, $text);
 | 
			
		||||
        $text = str_replace('$WWWROOTPATTERN', preg_quote($CFG->behat_wwwroot, '/'), $text);
 | 
			
		||||
        $text = str_replace('$WWWROOT', $CFG->behat_wwwroot, $text);
 | 
			
		||||
 | 
			
		||||
        return $text;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -1,128 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
// This file is part of Moodle - http://moodle.org/
 | 
			
		||||
//
 | 
			
		||||
// Moodle is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// Moodle is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
use Behat\Mink\Exception\DriverException;
 | 
			
		||||
use Behat\Mink\Exception\ExpectationException;
 | 
			
		||||
 | 
			
		||||
require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
 | 
			
		||||
require_once(__DIR__ . '/classes/performance_measure.php');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Behat step definitions to measure performance.
 | 
			
		||||
 */
 | 
			
		||||
class behat_performance extends behat_base {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    private $measures = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Start measuring performance.
 | 
			
		||||
     *
 | 
			
		||||
     * @When /^I start measuring "([^"]+)"$/
 | 
			
		||||
     */
 | 
			
		||||
    public function i_start_measuring(string $name) {
 | 
			
		||||
        $this->measures[$name] = new performance_measure($name, $this->getSession()->getDriver());
 | 
			
		||||
        $this->measures[$name]->start();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Stop measuring performance.
 | 
			
		||||
     *
 | 
			
		||||
     * @When /^I stop measuring "([^"]+)"$/
 | 
			
		||||
     */
 | 
			
		||||
    public function i_stop_measuring(string $name) {
 | 
			
		||||
        $this->get_performance_measure($name)->end();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Assert how long a performance measure took.
 | 
			
		||||
     *
 | 
			
		||||
     * @Then /^"([^"]+)" should have taken (less than|more than|exactly) (\d+(?:\.\d+)? (?:seconds|milliseconds))$/
 | 
			
		||||
     */
 | 
			
		||||
    public function timing_should_have_taken(string $measure, string $comparison, string $expectedtime) {
 | 
			
		||||
        $measuretiming = $this->get_performance_measure($measure);
 | 
			
		||||
        $comparison = $this->parse_comparison($comparison);
 | 
			
		||||
        $expectedtime = $this->parse_time($expectedtime);
 | 
			
		||||
 | 
			
		||||
        if (!call_user_func($comparison, $measuretiming->duration, $expectedtime)) {
 | 
			
		||||
            throw new ExpectationException(
 | 
			
		||||
                "Expected duration for '$measure' failed! (took {$measuretiming->duration}ms)",
 | 
			
		||||
                $this->getSession()->getDriver()
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $measuretiming->store();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse time.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $text Time string.
 | 
			
		||||
     * @return float
 | 
			
		||||
     */
 | 
			
		||||
    private function parse_time(string $text): float {
 | 
			
		||||
        $spaceindex = strpos($text, ' ');
 | 
			
		||||
        $value = floatval(substr($text, 0, $spaceindex));
 | 
			
		||||
 | 
			
		||||
        if (substr($text, $spaceindex + 1) == 'seconds') {
 | 
			
		||||
            $value *= 1000;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse a comparison function.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $text Comparison string.
 | 
			
		||||
     * @return Closure
 | 
			
		||||
     */
 | 
			
		||||
    private function parse_comparison(string $text): Closure {
 | 
			
		||||
        switch ($text) {
 | 
			
		||||
            case 'less than':
 | 
			
		||||
                return function ($a, $b) {
 | 
			
		||||
                    return $a < $b;
 | 
			
		||||
                };
 | 
			
		||||
            case 'more than':
 | 
			
		||||
                return function ($a, $b) {
 | 
			
		||||
                    return $a > $b;
 | 
			
		||||
                };
 | 
			
		||||
            case 'exactly':
 | 
			
		||||
                return function ($a, $b) {
 | 
			
		||||
                    return $a === $b;
 | 
			
		||||
                };
 | 
			
		||||
            default:
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get performance measure by name.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $name Performance measure name.
 | 
			
		||||
     * @return performance_measure Performance measure.
 | 
			
		||||
     */
 | 
			
		||||
    private function get_performance_measure(string $name): performance_measure {
 | 
			
		||||
        if (!isset($this->measures[$name])) {
 | 
			
		||||
            throw new DriverException("'$name' performance measure does not exist.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->measures[$name];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,346 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
// This file is part of Moodle - http://moodle.org/
 | 
			
		||||
//
 | 
			
		||||
// Moodle is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// Moodle is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You should have received a copy of the GNU General Public License
 | 
			
		||||
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
use Behat\Mink\Exception\DriverException;
 | 
			
		||||
use Facebook\WebDriver\Exception\InvalidArgumentException;
 | 
			
		||||
use Moodle\BehatExtension\Driver\WebDriver;
 | 
			
		||||
 | 
			
		||||
require_once(__DIR__ . '/../behat_app.php');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Performance measures for one particular metric.
 | 
			
		||||
 */
 | 
			
		||||
class performance_measure implements behat_app_listener {
 | 
			
		||||
 | 
			
		||||
    const STORAGE_FOLDER = '/behatperformancemeasures/';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    public $name;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var int
 | 
			
		||||
     */
 | 
			
		||||
    public $start;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var int
 | 
			
		||||
     */
 | 
			
		||||
    public $end;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var int
 | 
			
		||||
     */
 | 
			
		||||
    public $duration;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var int
 | 
			
		||||
     */
 | 
			
		||||
    public $scripting;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var int
 | 
			
		||||
     */
 | 
			
		||||
    public $styling;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var int
 | 
			
		||||
     */
 | 
			
		||||
     public $blocking;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var int
 | 
			
		||||
     */
 | 
			
		||||
    public $databaseStart;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var int
 | 
			
		||||
     */
 | 
			
		||||
    public $database;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var int
 | 
			
		||||
     */
 | 
			
		||||
    public $networking;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array
 | 
			
		||||
     */
 | 
			
		||||
    private $longTasks = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Closure
 | 
			
		||||
     */
 | 
			
		||||
    private $behatAppUnsubscribe;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Moodle\BehatExtension\Driver\WebDriver
 | 
			
		||||
     */
 | 
			
		||||
    private $driver;
 | 
			
		||||
 | 
			
		||||
    public function __construct(string $name, WebDriver $driver) {
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
        $this->driver = $driver;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Start timing.
 | 
			
		||||
     */
 | 
			
		||||
    public function start(): void {
 | 
			
		||||
        $this->start = $this->now();
 | 
			
		||||
 | 
			
		||||
        $this->observeLongTasks();
 | 
			
		||||
        $this->startDatabaseCount();
 | 
			
		||||
 | 
			
		||||
        $this->behatAppUnsubscribe = behat_app::listen($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Stop timing.
 | 
			
		||||
     */
 | 
			
		||||
    public function end(): void {
 | 
			
		||||
        $this->end = $this->now();
 | 
			
		||||
 | 
			
		||||
        $this->stopLongTasksObserver();
 | 
			
		||||
 | 
			
		||||
        call_user_func($this->behatAppUnsubscribe);
 | 
			
		||||
        $this->behatAppUnsubscribe = null;
 | 
			
		||||
 | 
			
		||||
        $this->analyseDuration();
 | 
			
		||||
        $this->analyseLongTasks();
 | 
			
		||||
        $this->analyseDatabaseUsage();
 | 
			
		||||
        $this->analysePerformanceLogs();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Persist measure logs in storage.
 | 
			
		||||
     */
 | 
			
		||||
    public function store(): void {
 | 
			
		||||
        global $CFG;
 | 
			
		||||
 | 
			
		||||
        $storagefolderpath = $CFG->dirroot . static::STORAGE_FOLDER;
 | 
			
		||||
 | 
			
		||||
        if (!file_exists($storagefolderpath)) {
 | 
			
		||||
            mkdir($storagefolderpath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $data = [
 | 
			
		||||
            'name' => $this->name,
 | 
			
		||||
            'start' => $this->start,
 | 
			
		||||
            'end' => $this->end,
 | 
			
		||||
            'duration' => $this->duration,
 | 
			
		||||
            'scripting' => $this->scripting,
 | 
			
		||||
            'styling' => $this->styling,
 | 
			
		||||
            'blocking' => $this->blocking,
 | 
			
		||||
            'longTasks' => count($this->longTasks),
 | 
			
		||||
            'database' => $this->database,
 | 
			
		||||
            'networking' => $this->networking,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        file_put_contents($storagefolderpath . time() . '.json', json_encode($data));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    public function on_app_load(): void {
 | 
			
		||||
        if (is_null($this->start) || !is_null($this->end)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->observeLongTasks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    public function on_app_unload(): void {
 | 
			
		||||
        $this->stopLongTasksObserver();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get current time.
 | 
			
		||||
     *
 | 
			
		||||
     * @return int Current time in milliseconds.
 | 
			
		||||
     */
 | 
			
		||||
    private function now(): int {
 | 
			
		||||
        return $this->driver->evaluateScript('Date.now()');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Start observing long tasks.
 | 
			
		||||
     */
 | 
			
		||||
    private function observeLongTasks(): void {
 | 
			
		||||
        $this->driver->executeScript("
 | 
			
		||||
            if (window.MA_PERFORMANCE_OBSERVER) return;
 | 
			
		||||
 | 
			
		||||
            window.MA_LONG_TASKS = [];
 | 
			
		||||
            window.MA_PERFORMANCE_OBSERVER = new PerformanceObserver(list => {
 | 
			
		||||
                for (const entry of list.getEntries()) {
 | 
			
		||||
                    window.MA_LONG_TASKS.push(entry);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            window.MA_PERFORMANCE_OBSERVER.observe({ entryTypes: ['longtask'] });
 | 
			
		||||
        ");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Record how many database queries have been logged so far.
 | 
			
		||||
     */
 | 
			
		||||
    private function startDatabaseCount(): void {
 | 
			
		||||
        try {
 | 
			
		||||
            $this->databaseStart = $this->driver->evaluateScript('dbProvider.queryLogs.length') ?? 0;
 | 
			
		||||
        } catch (Exception $e) {
 | 
			
		||||
            $this->databaseStart = 0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Flush Performance observer.
 | 
			
		||||
     */
 | 
			
		||||
    private function stopLongTasksObserver(): void {
 | 
			
		||||
        $newLongTasks = $this->driver->evaluateScript("
 | 
			
		||||
            return (function() {
 | 
			
		||||
                if (!window.MA_PERFORMANCE_OBSERVER) {
 | 
			
		||||
                    return [];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                window.MA_PERFORMANCE_OBSERVER.disconnect();
 | 
			
		||||
 | 
			
		||||
                const observer = window.MA_PERFORMANCE_OBSERVER;
 | 
			
		||||
                const longTasks = window.MA_LONG_TASKS;
 | 
			
		||||
 | 
			
		||||
                delete window.MA_PERFORMANCE_OBSERVER;
 | 
			
		||||
                delete window.MA_LONG_TASKS;
 | 
			
		||||
 | 
			
		||||
                return [...longTasks, ...observer.takeRecords()];
 | 
			
		||||
            })();
 | 
			
		||||
        ");
 | 
			
		||||
 | 
			
		||||
        if ($newLongTasks) {
 | 
			
		||||
            $this->longTasks = array_merge($this->longTasks, $newLongTasks);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Analyse duration.
 | 
			
		||||
     */
 | 
			
		||||
    private function analyseDuration(): void {
 | 
			
		||||
        $this->duration = $this->end - $this->start;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Analyse long tasks.
 | 
			
		||||
     */
 | 
			
		||||
    private function analyseLongTasks(): void {
 | 
			
		||||
        $blockingDuration = 0;
 | 
			
		||||
 | 
			
		||||
        foreach ($this->longTasks as $longTask) {
 | 
			
		||||
            $blockingDuration += $longTask['duration'] - 50;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->blocking = $blockingDuration;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Analyse database usage.
 | 
			
		||||
     */
 | 
			
		||||
    private function analyseDatabaseUsage(): void {
 | 
			
		||||
        $this->database = $this->driver->evaluateScript('dbProvider.queryLogs.length') - $this->databaseStart;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Analyse performance logs.
 | 
			
		||||
     */
 | 
			
		||||
    private function analysePerformanceLogs(): void {
 | 
			
		||||
        global $CFG;
 | 
			
		||||
 | 
			
		||||
        $scriptingDuration = 0;
 | 
			
		||||
        $stylingDuration = 0;
 | 
			
		||||
        $networkingCount = 0;
 | 
			
		||||
        $logs = $this->getPerformanceLogs();
 | 
			
		||||
 | 
			
		||||
        foreach ($logs as $log) {
 | 
			
		||||
            // TODO this should filter by end time as well, but it seems like the timestamps are not
 | 
			
		||||
            // working as expected.
 | 
			
		||||
            if ($log['timestamp'] < $this->start) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $message = json_decode($log['message'])->message;
 | 
			
		||||
            $messagename = $message->params->name ?? '';
 | 
			
		||||
 | 
			
		||||
            if (in_array($messagename, ['FunctionCall', 'GCEvent', 'MajorGC', 'MinorGC', 'EvaluateScript'])) {
 | 
			
		||||
                $scriptingDuration += $message->params->dur;
 | 
			
		||||
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (in_array($messagename, ['UpdateLayoutTree', 'RecalculateStyles', 'ParseAuthorStyleSheet'])) {
 | 
			
		||||
                $stylingDuration += $message->params->dur;
 | 
			
		||||
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (in_array($messagename, ['XHRLoad']) && !str_starts_with($message->params->args->data->url, $CFG->behat_ionic_wwwroot)) {
 | 
			
		||||
                $networkingCount++;
 | 
			
		||||
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->scripting = round($scriptingDuration / 1000);
 | 
			
		||||
        $this->styling = round($stylingDuration / 1000);
 | 
			
		||||
        $this->networking = $networkingCount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get performance logs.
 | 
			
		||||
     *
 | 
			
		||||
     * @return array Performance logs.
 | 
			
		||||
     */
 | 
			
		||||
    private function getPerformanceLogs(): array {
 | 
			
		||||
        try {
 | 
			
		||||
            return $this->driver->getWebDriver()->manage()->getLog('performance');
 | 
			
		||||
        } catch (InvalidArgumentException $e) {
 | 
			
		||||
            throw new DriverException(
 | 
			
		||||
                implode("\n", [
 | 
			
		||||
                    "It wasn't possible to get performance logs, make sure that you have configured the following capabilities:",
 | 
			
		||||
                    "",
 | 
			
		||||
                    "\$CFG->behat_profiles = [",
 | 
			
		||||
                    "    'default' => [",
 | 
			
		||||
                    "        'browser' => 'chrome',",
 | 
			
		||||
                    "        'wd_host' => 'http://selenium:4444/wd/hub',",
 | 
			
		||||
                    "        'capabilities' => [",
 | 
			
		||||
                    "            'extra_capabilities' => [",
 | 
			
		||||
                    "                'goog:loggingPrefs' => ['performance' => 'ALL'],",
 | 
			
		||||
                    "                'chromeOptions' => [",
 | 
			
		||||
                    "                    'perfLoggingPrefs' => [",
 | 
			
		||||
                    "                        'traceCategories' => 'devtools.timeline',",
 | 
			
		||||
                    "                    ],",
 | 
			
		||||
                    "                ],",
 | 
			
		||||
                    "            ],",
 | 
			
		||||
                    "        ],",
 | 
			
		||||
                    "    ],",
 | 
			
		||||
                    "];",
 | 
			
		||||
                    "",
 | 
			
		||||
                ])
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,87 +0,0 @@
 | 
			
		||||
#!/usr/bin/env node
 | 
			
		||||
 | 
			
		||||
// (C) Copyright 2015 Moodle Pty Ltd.
 | 
			
		||||
//
 | 
			
		||||
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
// you may not use this file except in compliance with the License.
 | 
			
		||||
// You may obtain a copy of the License at
 | 
			
		||||
//
 | 
			
		||||
//     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
//
 | 
			
		||||
// Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
// See the License for the specific language governing permissions and
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
const { readdirSync, readFileSync } = require('fs');
 | 
			
		||||
 | 
			
		||||
if (process.argv.length < 3) {
 | 
			
		||||
    console.error('Missing measure timings storage path argument');
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const performanceMeasuresStoragePath = process.argv[2].trimRight('/') + '/';
 | 
			
		||||
const files = readdirSync(performanceMeasuresStoragePath);
 | 
			
		||||
const performanceMeasures = {};
 | 
			
		||||
 | 
			
		||||
if (files.length === 0) {
 | 
			
		||||
    console.log('No logs found!');
 | 
			
		||||
    process.exit(0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Aggregate data
 | 
			
		||||
for (const file of files) {
 | 
			
		||||
    const performanceMeasure = JSON.parse(readFileSync(performanceMeasuresStoragePath + file));
 | 
			
		||||
 | 
			
		||||
    performanceMeasures[performanceMeasure.name] = performanceMeasures[performanceMeasure.name] ?? {
 | 
			
		||||
        duration: [],
 | 
			
		||||
        scripting: [],
 | 
			
		||||
        styling: [],
 | 
			
		||||
        blocking: [],
 | 
			
		||||
        longTasks: [],
 | 
			
		||||
        database: [],
 | 
			
		||||
        networking: [],
 | 
			
		||||
    };
 | 
			
		||||
    performanceMeasures[performanceMeasure.name].duration.push(performanceMeasure.duration);
 | 
			
		||||
    performanceMeasures[performanceMeasure.name].scripting.push(performanceMeasure.scripting);
 | 
			
		||||
    performanceMeasures[performanceMeasure.name].styling.push(performanceMeasure.styling);
 | 
			
		||||
    performanceMeasures[performanceMeasure.name].blocking.push(performanceMeasure.blocking);
 | 
			
		||||
    performanceMeasures[performanceMeasure.name].longTasks.push(performanceMeasure.longTasks);
 | 
			
		||||
    performanceMeasures[performanceMeasure.name].database.push(performanceMeasure.database);
 | 
			
		||||
    performanceMeasures[performanceMeasure.name].networking.push(performanceMeasure.networking);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Calculate averages
 | 
			
		||||
for (const [name, { duration, scripting, styling, blocking, longTasks, database, networking }] of Object.entries(performanceMeasures)) {
 | 
			
		||||
    const totalRuns = duration.length;
 | 
			
		||||
    const averageDuration = Math.round(duration.reduce((total, duration) => total + duration) / totalRuns);
 | 
			
		||||
    const averageScripting = Math.round(scripting.reduce((total, scripting) => total + scripting) / totalRuns);
 | 
			
		||||
    const averageStyling = Math.round(styling.reduce((total, styling) => total + styling) / totalRuns);
 | 
			
		||||
    const averageBlocking = Math.round(blocking.reduce((total, blocking) => total + blocking) / totalRuns);
 | 
			
		||||
    const averageLongTasks = Math.round(longTasks.reduce((total, longTasks) => total + longTasks) / totalRuns);
 | 
			
		||||
    const averageDatabase = Math.round(database.reduce((total, database) => total + database) / totalRuns);
 | 
			
		||||
    const averageNetworking = Math.round(networking.reduce((total, networking) => total + networking) / totalRuns);
 | 
			
		||||
 | 
			
		||||
    performanceMeasures[name] = {
 | 
			
		||||
        'Total duration': `${averageDuration}ms`,
 | 
			
		||||
        'Scripting': `${averageScripting}ms`,
 | 
			
		||||
        'Styling': `${averageStyling}ms`,
 | 
			
		||||
        'Blocking': `${averageBlocking}ms`,
 | 
			
		||||
        '# Network requests': averageNetworking,
 | 
			
		||||
        '# DB Queries': averageDatabase,
 | 
			
		||||
        '# Long Tasks': averageLongTasks,
 | 
			
		||||
        '# runs': totalRuns,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sort tests
 | 
			
		||||
const tests = Object.keys(performanceMeasures).sort();
 | 
			
		||||
const sortedPerformanceMeasures = {};
 | 
			
		||||
 | 
			
		||||
for (const test of tests) {
 | 
			
		||||
    sortedPerformanceMeasures[test] = performanceMeasures[test];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Display data
 | 
			
		||||
console.table(sortedPerformanceMeasures);
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@block @block_timeline @app @javascript @lms_upto3.11
 | 
			
		||||
@addon_block_timeline @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: Timeline block.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@block @block_timeline @app @javascript
 | 
			
		||||
@addon_block_timeline @app @javascript
 | 
			
		||||
Feature: Timeline block.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_calendar @app @javascript
 | 
			
		||||
@addon_calendar @app @javascript
 | 
			
		||||
Feature: Test creation of calendar events in app
 | 
			
		||||
  In order to take advantage of all the calendar features while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @competency @app @javascript
 | 
			
		||||
@addon_competency @app @javascript
 | 
			
		||||
Feature: Test competency navigation
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_message @app @javascript
 | 
			
		||||
@addon_messages @app @javascript
 | 
			
		||||
Feature: Test basic usage of messages in app
 | 
			
		||||
  In order to participate with messages while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_message @app @javascript
 | 
			
		||||
@addon_messages @app @javascript
 | 
			
		||||
Feature: Test messages navigation in the app
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_message @app @javascript
 | 
			
		||||
@addon_messages @app @javascript
 | 
			
		||||
Feature: Test messages settings
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_assign @app @javascript @lms_upto3.10
 | 
			
		||||
@addon_mod_assign @app @javascript @lms_upto3.10
 | 
			
		||||
Feature: Test basic usage of assignment activity in app
 | 
			
		||||
  In order to participate in the assignment while using the mobile app
 | 
			
		||||
  I need basic assignment functionality to work
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_assign @app @javascript
 | 
			
		||||
@addon_mod_assign @app @javascript
 | 
			
		||||
Feature: Test basic usage of assignment activity in app
 | 
			
		||||
  In order to participate in the assignment while using the mobile app
 | 
			
		||||
  I need basic assignment functionality to work
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_assign @app @javascript @lms_from4.0
 | 
			
		||||
@addon_mod_assign @app @javascript @lms_from4.0
 | 
			
		||||
Feature: Test marking workflow in assignment activity in app
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_assign @app @javascript
 | 
			
		||||
@addon_mod_assign @app @javascript
 | 
			
		||||
Feature: Test assignments navigation
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_bigbluebuttonbn @app @javascript @lms_from4.0
 | 
			
		||||
@addon_mod_bigbluebuttonbn @app @javascript @lms_from4.0
 | 
			
		||||
Feature: Test basic usage of BBB activity in app
 | 
			
		||||
  In order to join a BBB meeting while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_bigbluebuttonbn @app @javascript @lms_from4.0
 | 
			
		||||
@addon_mod_bigbluebuttonbn @app @javascript @lms_from4.0
 | 
			
		||||
Feature: Test usage of BBB activity with groups in app
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_book @app @javascript
 | 
			
		||||
@addon_mod_book @app @javascript
 | 
			
		||||
Feature: Test basic usage of book activity in app
 | 
			
		||||
  In order to view a book while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @mod @mod_book
 | 
			
		||||
@addon_mod_book @app @javascript
 | 
			
		||||
Feature: Test single activity of book type in app
 | 
			
		||||
  In order to view a book while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_chat @app @javascript
 | 
			
		||||
@addon_mod_chat @app @javascript
 | 
			
		||||
Feature: Test basic usage of chat in app
 | 
			
		||||
  As a student
 | 
			
		||||
  I need basic chat functionality to work
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_chat @app @javascript
 | 
			
		||||
@addon_mod_chat @app @javascript
 | 
			
		||||
Feature: Test chat navigation
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_choice @app @javascript @lms_upto3.11
 | 
			
		||||
@addon_mod_choice @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: Test basic usage of choice activity in app
 | 
			
		||||
  In order to participate in the choice while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_choice @app @javascript
 | 
			
		||||
@addon_mod_choice @app @javascript
 | 
			
		||||
Feature: Test basic usage of choice activity in app
 | 
			
		||||
  In order to participate in the choice while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_data @app @javascript
 | 
			
		||||
@addon_mod_data @app @javascript
 | 
			
		||||
Feature: Users can manage entries in database activities
 | 
			
		||||
  In order to populate databases
 | 
			
		||||
  As a user
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_data @app @javascript
 | 
			
		||||
@addon_mod_data @app @javascript
 | 
			
		||||
Feature: Users can store entries in database activities when offline and sync when online
 | 
			
		||||
  In order to populate databases while offline
 | 
			
		||||
  As a user
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_feedback @app @javascript
 | 
			
		||||
@addon_mod_feedback @app @javascript
 | 
			
		||||
Feature: Test feedback navigation
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_forum @app @javascript
 | 
			
		||||
@addon_mod_forum @app @javascript
 | 
			
		||||
Feature: Test basic usage of forum activity in app
 | 
			
		||||
  In order to participate in the forum while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_forum @app @javascript
 | 
			
		||||
@addon_mod_forum @app @javascript
 | 
			
		||||
Feature: Test usage of forum activity with groups in app
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_forum @app @javascript
 | 
			
		||||
@addon_mod_forum @app @javascript
 | 
			
		||||
Feature: Test forum navigation
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_forum @app @javascript @lms_from4.3
 | 
			
		||||
@addon_mod_forum @app @javascript @lms_from4.3
 | 
			
		||||
Feature: Test Forum Search
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_glossary @app @javascript
 | 
			
		||||
@addon_mod_glossary @app @javascript
 | 
			
		||||
Feature: Test basic usage of glossary in app
 | 
			
		||||
  In order to participate in the glossaries while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_glossary @app @javascript
 | 
			
		||||
@addon_mod_glossary @app @javascript
 | 
			
		||||
Feature: Test glossary navigation
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_lesson @app @javascript @lms_upto3.11
 | 
			
		||||
@addon_mod_lesson @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: Test decimal separators in lesson
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_lesson @app @javascript
 | 
			
		||||
@addon_mod_lesson @app @javascript
 | 
			
		||||
Feature: Test decimal separators in lesson
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_quiz @app @javascript @lms_from3.10 @lms_upto3.11
 | 
			
		||||
@addon_mod_quiz @app @javascript @lms_from3.10 @lms_upto3.11
 | 
			
		||||
Feature: Attempt a quiz in app
 | 
			
		||||
  As a student
 | 
			
		||||
  In order to demonstrate what I know
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_quiz @app @javascript @lms_upto3.9
 | 
			
		||||
@addon_mod_quiz @app @javascript @lms_upto3.9
 | 
			
		||||
Feature: Attempt a quiz in app
 | 
			
		||||
  As a student
 | 
			
		||||
  In order to demonstrate what I know
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_quiz @app @javascript @lms_from4.0
 | 
			
		||||
@addon_mod_quiz @app @javascript @lms_from4.0
 | 
			
		||||
Feature: Attempt a quiz in app
 | 
			
		||||
  As a student
 | 
			
		||||
  In order to demonstrate what I know
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_quiz @app @javascript
 | 
			
		||||
@addon_mod_quiz @app @javascript
 | 
			
		||||
Feature: Use quizzes with different behaviours in the app
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_quiz @app @javascript
 | 
			
		||||
@addon_mod_quiz @app @javascript
 | 
			
		||||
Feature: Navigate through a quiz in the app
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_scorm @app @javascript
 | 
			
		||||
@addon_mod_scorm @app @javascript
 | 
			
		||||
Feature: Test appearance options of SCORM activity in app
 | 
			
		||||
  In order to play a SCORM while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_scorm @app @javascript @_switch_iframe
 | 
			
		||||
@addon_mod_scorm @app @javascript @_switch_iframe
 | 
			
		||||
Feature: Test attempts and grading settings of SCORM activity in app
 | 
			
		||||
  In order to play a SCORM while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_scorm @app @javascript
 | 
			
		||||
@addon_mod_scorm @app @javascript
 | 
			
		||||
Feature: Test availability options of SCORM activity in app
 | 
			
		||||
  Only open SCORMs should be allowed to be played
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_scorm @app @javascript @_switch_iframe
 | 
			
		||||
@addon_mod_scorm @app @javascript @_switch_iframe
 | 
			
		||||
Feature: Test basic usage of SCORM activity in app
 | 
			
		||||
  In order to play a SCORM while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_survey @app @javascript
 | 
			
		||||
@addon_mod_survey @app @javascript
 | 
			
		||||
Feature: Test basic usage of survey activity in app
 | 
			
		||||
  In order to participate in surveys while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@mod @mod_workshop @app @javascript
 | 
			
		||||
@addon_mod_workshop @app @javascript
 | 
			
		||||
Feature: Test basic usage of workshop activity in app
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@addon_notifications @app @javascript
 | 
			
		||||
Feature: Notifications
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ import { CoreUpdateManagerProvider } from '@services/update-manager';
 | 
			
		||||
import { CoreUrlUtilsProvider } from '@services/utils/url';
 | 
			
		||||
import { CoreUtilsProvider } from '@services/utils/utils';
 | 
			
		||||
import { CoreWSProvider } from '@services/ws';
 | 
			
		||||
import { CorePlatformService } from '@services/platform';
 | 
			
		||||
 | 
			
		||||
export const CORE_SERVICES: Type<unknown>[] = [
 | 
			
		||||
    CoreAppProvider,
 | 
			
		||||
@ -68,6 +69,7 @@ export const CORE_SERVICES: Type<unknown>[] = [
 | 
			
		||||
    CoreMimetypeUtilsProvider,
 | 
			
		||||
    CoreNavigatorService,
 | 
			
		||||
    CorePluginFileDelegateService,
 | 
			
		||||
    CorePlatformService,
 | 
			
		||||
    CoreScreenService,
 | 
			
		||||
    CoreSitesProvider,
 | 
			
		||||
    CoreSyncProvider,
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_comments @app @javascript
 | 
			
		||||
@core_comments @app @javascript
 | 
			
		||||
Feature: Test basic usage of comments in app
 | 
			
		||||
  In order to participate in the comments while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript @lms_upto3.11
 | 
			
		||||
@core_course @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: Test basic usage of one course in app
 | 
			
		||||
  In order to participate in one course while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript
 | 
			
		||||
@core_course @app @javascript
 | 
			
		||||
Feature: Test basic usage of one course in app
 | 
			
		||||
  In order to participate in one course while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript @lms_upto3.10
 | 
			
		||||
@core_course @app @javascript @lms_upto3.10
 | 
			
		||||
Feature: Check course completion feature.
 | 
			
		||||
  In order to track the progress of the course on mobile device
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript
 | 
			
		||||
@core_course @app @javascript
 | 
			
		||||
Feature: Check course completion feature.
 | 
			
		||||
  In order to track the progress of the course on mobile device
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript @lms_upto3.11
 | 
			
		||||
@core_course @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: Test course list shown on app start tab
 | 
			
		||||
  In order to select a course
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript
 | 
			
		||||
@core_course @app @javascript
 | 
			
		||||
Feature: Test course list shown on app start tab
 | 
			
		||||
  In order to select a course
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript @enrol @enrol_guest
 | 
			
		||||
@core_course @app @javascript @enrol @enrol_guest
 | 
			
		||||
Feature: Test basic usage of guest access course in app
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript @lms_from4.0
 | 
			
		||||
@core_course @app @javascript @lms_from4.0
 | 
			
		||||
Feature: Check relative dates feature.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript @lms_upto3.10
 | 
			
		||||
@core_courses @app @javascript @lms_upto3.10
 | 
			
		||||
Feature: Test basic usage of courses in app
 | 
			
		||||
  In order to participate in the courses while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript @lms_upto3.11
 | 
			
		||||
@core_courses @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: Test basic usage of courses in app
 | 
			
		||||
  In order to participate in the courses while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_course @app @javascript
 | 
			
		||||
@core_courses @app @javascript
 | 
			
		||||
Feature: Test basic usage of courses in app
 | 
			
		||||
  In order to participate in the courses while using the mobile app
 | 
			
		||||
  As a student
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,8 @@
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { ILocalNotification, ILocalNotificationAction, LocalNotifications } from '@awesome-cordova-plugins/local-notifications/ngx';
 | 
			
		||||
import { Observable, Subject } from 'rxjs';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { CorePlatform } from '@services/platform';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Mock LocalNotifications service.
 | 
			
		||||
@ -67,6 +69,17 @@ export class LocalNotificationsMock extends LocalNotifications {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Flush pending notifications.
 | 
			
		||||
     */
 | 
			
		||||
    flush(): void {
 | 
			
		||||
        for (const notification of this.scheduledNotifications) {
 | 
			
		||||
            this.sendNotification(notification);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.scheduledNotifications = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets timeout for next nofitication.
 | 
			
		||||
     */
 | 
			
		||||
@ -104,36 +117,7 @@ export class LocalNotificationsMock extends LocalNotifications {
 | 
			
		||||
 | 
			
		||||
        const notificationTime = nextNotification.trigger?.at?.getTime() || 0;
 | 
			
		||||
        if (notificationTime === 0 || notificationTime <= dateNow) {
 | 
			
		||||
            const body = Array.isArray(nextNotification.text) ? nextNotification.text.join() : nextNotification.text;
 | 
			
		||||
            const notification = new Notification(nextNotification.title || '', {
 | 
			
		||||
                body,
 | 
			
		||||
                data: nextNotification.data,
 | 
			
		||||
                icon: nextNotification.icon,
 | 
			
		||||
                requireInteraction: true,
 | 
			
		||||
                tag: nextNotification.data?.component,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.triggeredNotifications.push(nextNotification);
 | 
			
		||||
 | 
			
		||||
            this.observables.trigger.next(nextNotification);
 | 
			
		||||
 | 
			
		||||
            notification.addEventListener('click', () => {
 | 
			
		||||
                this.observables.click.next(nextNotification);
 | 
			
		||||
 | 
			
		||||
                notification.close();
 | 
			
		||||
                if (nextNotification.id) {
 | 
			
		||||
                    delete(this.presentNotifications[nextNotification.id]);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (nextNotification.id) {
 | 
			
		||||
                this.presentNotifications[nextNotification.id] = notification;
 | 
			
		||||
 | 
			
		||||
                notification.addEventListener('close', () => {
 | 
			
		||||
                    delete(this.presentNotifications[nextNotification.id ?? 0]);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.sendNotification(nextNotification);
 | 
			
		||||
            this.scheduledNotifications.shift();
 | 
			
		||||
            this.triggerNextNotification();
 | 
			
		||||
        } else {
 | 
			
		||||
@ -141,6 +125,43 @@ export class LocalNotificationsMock extends LocalNotifications {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send notification.
 | 
			
		||||
     *
 | 
			
		||||
     * @param localNotification Notification.
 | 
			
		||||
     */
 | 
			
		||||
    protected sendNotification(localNotification: ILocalNotification): void {
 | 
			
		||||
        const body = Array.isArray(localNotification.text) ? localNotification.text.join() : localNotification.text;
 | 
			
		||||
        const notification = new Notification(localNotification.title || '', {
 | 
			
		||||
            body,
 | 
			
		||||
            data: localNotification.data,
 | 
			
		||||
            icon: localNotification.icon,
 | 
			
		||||
            requireInteraction: true,
 | 
			
		||||
            tag: localNotification.data?.component,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.triggeredNotifications.push(localNotification);
 | 
			
		||||
 | 
			
		||||
        this.observables.trigger.next(localNotification);
 | 
			
		||||
 | 
			
		||||
        notification.addEventListener('click', () => {
 | 
			
		||||
            this.observables.click.next(localNotification);
 | 
			
		||||
 | 
			
		||||
            notification.close();
 | 
			
		||||
            if (localNotification.id) {
 | 
			
		||||
                delete(this.presentNotifications[localNotification.id]);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (localNotification.id) {
 | 
			
		||||
            this.presentNotifications[localNotification.id] = notification;
 | 
			
		||||
 | 
			
		||||
            notification.addEventListener('close', () => {
 | 
			
		||||
                delete(this.presentNotifications[localNotification.id ?? 0]);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
@ -313,7 +334,16 @@ export class LocalNotificationsMock extends LocalNotifications {
 | 
			
		||||
     */
 | 
			
		||||
    async registerPermission(): Promise<boolean> {
 | 
			
		||||
        // We need to ask the user for permission
 | 
			
		||||
        const permission = await Notification.requestPermission();
 | 
			
		||||
        const permissionRequests = [Notification.requestPermission()];
 | 
			
		||||
 | 
			
		||||
        if (CorePlatform.isAutomated()) {
 | 
			
		||||
            // In some testing environments, Notification.requestPermission gets stuck and never returns.
 | 
			
		||||
            // Given that we don't actually need browser notifications to work in Behat tests, we can just
 | 
			
		||||
            // continue if the permissions haven't been granted after 1 second.
 | 
			
		||||
            permissionRequests.push(CoreUtils.wait(1000).then(() => 'granted'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const permission = await Promise.race(permissionRequests);
 | 
			
		||||
 | 
			
		||||
        this.hasGranted = permission === 'granted';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -41,9 +41,9 @@ import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
 | 
			
		||||
import { CoreAppProvider } from '@services/app';
 | 
			
		||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
			
		||||
import { CoreCourseAccess } from '@features/course/services/course-options-delegate';
 | 
			
		||||
import { CorePlatform } from '@services/platform';
 | 
			
		||||
 | 
			
		||||
export const GRADES_PAGE_NAME = 'grades';
 | 
			
		||||
export const GRADES_PARTICIPANTS_PAGE_NAME = 'participant-grades';
 | 
			
		||||
@ -105,7 +105,7 @@ export class CoreGradesHelperProvider {
 | 
			
		||||
                row.rowclass += itemNameColumn.class.indexOf('hidden') >= 0 ? ' hidden' : '';
 | 
			
		||||
                row.rowclass += itemNameColumn.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
 | 
			
		||||
 | 
			
		||||
                if (!useLegacyLayout && !CoreAppProvider.isAutomated()) {
 | 
			
		||||
                if (!useLegacyLayout && !CorePlatform.isAutomated()) {
 | 
			
		||||
                    // Activity name is only included in the webservice response from the latest version when behat is not running.
 | 
			
		||||
                    content = content.replace(/<span[^>]+>.+?<\/span>/i, '');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @lms_upto4.1
 | 
			
		||||
@core_grades @app @javascript @lms_upto4.1
 | 
			
		||||
Feature: Grades navigation
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core_grades @app @javascript
 | 
			
		||||
Feature: Grades navigation
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @lms_upto3.11
 | 
			
		||||
@core_grades @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: View grades
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @lms_from4.0
 | 
			
		||||
@core_grades @app @javascript @lms_from4.0
 | 
			
		||||
Feature: View grades
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@auth @core_auth @app @javascript @lms_from4.0 @lms_upto4.0
 | 
			
		||||
@core_login @app @javascript @lms_from4.0 @lms_upto4.0
 | 
			
		||||
Feature: Test basic usage of login in app
 | 
			
		||||
  I need basic login functionality to work
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@auth @core_auth @app @javascript @lms_upto3.11
 | 
			
		||||
@core_login @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: Test basic usage of login in app
 | 
			
		||||
  I need basic login functionality to work
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@auth @core_auth @app @javascript
 | 
			
		||||
@core_login @app @javascript
 | 
			
		||||
Feature: Test basic usage of login in app
 | 
			
		||||
  I need basic login functionality to work
 | 
			
		||||
 | 
			
		||||
@ -96,7 +96,7 @@ Feature: Test basic usage of login in app
 | 
			
		||||
    And I should find "You must change your password to proceed." in the app
 | 
			
		||||
 | 
			
		||||
    When I press "Change password" "ion-button" in the app
 | 
			
		||||
    Then the app should have opened a browser tab with url "webserver"
 | 
			
		||||
    Then the app should have opened a browser tab with url "$WWWROOTPATTERN"
 | 
			
		||||
 | 
			
		||||
    When I close the browser tab opened by the app
 | 
			
		||||
    Then I should find "If you didn't change your password correctly, you'll be asked to do it again." in the app
 | 
			
		||||
@ -115,7 +115,7 @@ Feature: Test basic usage of login in app
 | 
			
		||||
    But I should not find "Reconnect" in the app
 | 
			
		||||
 | 
			
		||||
    When I press "Change password" "ion-button" in the app
 | 
			
		||||
    Then the app should have opened a browser tab with url "webserver"
 | 
			
		||||
    Then the app should have opened a browser tab with url "$WWWROOTPATTERN"
 | 
			
		||||
 | 
			
		||||
    When I switch to the browser tab opened by the app
 | 
			
		||||
    And I set the field "username" to "student1"
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@auth @core_auth @app @javascript @lms_upto3.9
 | 
			
		||||
@core_login @app @javascript @lms_upto3.9
 | 
			
		||||
Feature: Test signup in app
 | 
			
		||||
  I need basic signup functionality to work
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@auth @core_auth @app @javascript
 | 
			
		||||
@core_login @app @javascript
 | 
			
		||||
Feature: Test signup in app
 | 
			
		||||
  I need basic signup functionality to work
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @lms_upto3.11
 | 
			
		||||
@core_mainmenu @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: Main Menu opens the right page
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core_mainmenu @app @javascript
 | 
			
		||||
Feature: Main Menu opens the right page
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @core_reminders @lms_from4.0
 | 
			
		||||
@core_reminders @app @javascript @lms_from4.0
 | 
			
		||||
Feature: Set a new reminder on activity
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -62,20 +62,21 @@ Feature: Set a new reminder on activity
 | 
			
		||||
    And I press "Custom..." in the app
 | 
			
		||||
    Then I should find "Custom reminder" in the app
 | 
			
		||||
    When I set the following fields to these values in the app:
 | 
			
		||||
       | Value | 69 |
 | 
			
		||||
       | Value | 40 |
 | 
			
		||||
       | Units | minutes |
 | 
			
		||||
    And I press "Set reminder" in the app
 | 
			
		||||
    Then I should find "Reminder set for" in the app
 | 
			
		||||
    When I wait "50" seconds
 | 
			
		||||
    Then a notification with title "Due: Assignment 01" is present in the app
 | 
			
		||||
    And I close a notification with title "Due: Assignment 01" in the app
 | 
			
		||||
    And a notification with title "Due: Assignment 01" should be scheduled 40 minutes before the "Assignment 01" assignment due date in the app
 | 
			
		||||
    When I flush pending notifications in the app
 | 
			
		||||
    Then a notification with title "Due: Assignment 01" should be present in the app
 | 
			
		||||
 | 
			
		||||
    # Set and check reminder is cancelled
 | 
			
		||||
    When I press "Set a reminder for \"Assignment 01\" (Due)" in the app
 | 
			
		||||
    When I close a notification with title "Due: Assignment 01" in the app
 | 
			
		||||
    And I press "Set a reminder for \"Assignment 01\" (Due)" in the app
 | 
			
		||||
    And I press "Custom..." in the app
 | 
			
		||||
    Then I should find "Custom reminder" in the app
 | 
			
		||||
    When I set the following fields to these values in the app:
 | 
			
		||||
       | Value | 68 |
 | 
			
		||||
       | Value | 20 |
 | 
			
		||||
       | Units | minutes |
 | 
			
		||||
    And I press "Set reminder" in the app
 | 
			
		||||
    Then I should find "Reminder set for" in the app
 | 
			
		||||
@ -83,8 +84,8 @@ Feature: Set a new reminder on activity
 | 
			
		||||
    Then I should find "Reminder set for" in the app
 | 
			
		||||
    When I press "Delete reminder" in the app
 | 
			
		||||
    Then I should find "Reminder deleted" in the app
 | 
			
		||||
    When I wait "50" seconds
 | 
			
		||||
    Then a notification with title "Due: Assignment 01" is not present in the app
 | 
			
		||||
    But a notification with title "Due: Assignment 01" should not be scheduled in the app
 | 
			
		||||
    And a notification with title "Due: Assignment 01" should not be present in the app
 | 
			
		||||
 | 
			
		||||
  Scenario: Check toast is correct
 | 
			
		||||
    Given I entered the assign activity "Assignment 02" on course "Course 1" as "student1" in the app
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @core_reminders
 | 
			
		||||
@core_reminders @app @javascript
 | 
			
		||||
Feature: Set a new reminder on course
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @core_reportbuilder @lms_from4.1
 | 
			
		||||
@core_reportbuilder @app @javascript @lms_from4.1
 | 
			
		||||
Feature: Report builder
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_search @app @javascript @lms_from4.3
 | 
			
		||||
@core_search @app @javascript @lms_from4.3
 | 
			
		||||
Feature: Test Global Search
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @core_settings
 | 
			
		||||
@core_settings @app @javascript
 | 
			
		||||
Feature: It navigates properly within settings.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @core_settings
 | 
			
		||||
@core_settings @app @javascript
 | 
			
		||||
Feature: It synchronise sites properly
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core_siteplugins @app @javascript
 | 
			
		||||
Feature: Plugins work properly.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_user @app @javascript
 | 
			
		||||
@core_user @app @javascript
 | 
			
		||||
Feature: Test basic usage of user features
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -17,7 +17,7 @@ Feature: Test basic usage of user features
 | 
			
		||||
    And I should find "Before you continue, please fill in the required fields in your user profile." in the app
 | 
			
		||||
 | 
			
		||||
    When I press "Complete profile" in the app
 | 
			
		||||
    Then the app should have opened a browser tab with url "webserver"
 | 
			
		||||
    Then the app should have opened a browser tab with url "$WWWROOTPATTERN"
 | 
			
		||||
 | 
			
		||||
    When I close the browser tab opened by the app
 | 
			
		||||
    Then I should find "If you didn't complete your profile correctly, you'll be asked to do it again." in the app
 | 
			
		||||
@ -36,7 +36,7 @@ Feature: Test basic usage of user features
 | 
			
		||||
    But I should not find "Reconnect" in the app
 | 
			
		||||
 | 
			
		||||
    When I press "Complete profile" in the app
 | 
			
		||||
    Then the app should have opened a browser tab with url "webserver"
 | 
			
		||||
    Then the app should have opened a browser tab with url "$WWWROOTPATTERN"
 | 
			
		||||
 | 
			
		||||
    When I switch to the browser tab opened by the app
 | 
			
		||||
    And I set the field "username" to "student1"
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_user @app @javascript @lms_upto3.11
 | 
			
		||||
@core_user @app @javascript @lms_upto3.11
 | 
			
		||||
Feature: Site support
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@core @core_user @app @javascript @lms_from4.0
 | 
			
		||||
@core_user @app @javascript @lms_from4.0
 | 
			
		||||
Feature: Site support
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core_usertours @app @javascript
 | 
			
		||||
Feature: User Tours work properly.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
 | 
			
		||||
@ -70,10 +70,11 @@ export class CoreAppProvider {
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns whether the user agent is controlled by automation. I.e. Behat testing.
 | 
			
		||||
     *
 | 
			
		||||
     * @deprecated since 4.4. Use CorePlatform.isAutomated() instead.
 | 
			
		||||
     * @returns True if the user agent is controlled by automation, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    static isAutomated(): boolean {
 | 
			
		||||
        return !!navigator.webdriver;
 | 
			
		||||
        return CorePlatform.isAutomated();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -82,7 +83,7 @@ export class CoreAppProvider {
 | 
			
		||||
     * @returns Timezone. Undefined to use the user's timezone.
 | 
			
		||||
     */
 | 
			
		||||
    static getForcedTimezone(): string | undefined {
 | 
			
		||||
        if (CoreAppProvider.isAutomated()) {
 | 
			
		||||
        if (CorePlatform.isAutomated()) {
 | 
			
		||||
            // Use the same timezone forced for LMS in tests.
 | 
			
		||||
            return 'Australia/Perth';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import { Injectable } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { LangChangeEvent } from '@ngx-translate/core';
 | 
			
		||||
import { CoreAppProvider } from '@services/app';
 | 
			
		||||
import { CoreConfig } from '@services/config';
 | 
			
		||||
import { CoreSubscriptions } from '@singletons/subscriptions';
 | 
			
		||||
import { makeSingleton, Translate, Http } from '@singletons';
 | 
			
		||||
@ -72,7 +71,7 @@ export class CoreLangProvider {
 | 
			
		||||
 | 
			
		||||
        let language: string;
 | 
			
		||||
 | 
			
		||||
        if (CoreAppProvider.isAutomated()) {
 | 
			
		||||
        if (CorePlatform.isAutomated()) {
 | 
			
		||||
            // Force current language to English when Behat is running.
 | 
			
		||||
            language = 'en';
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,15 @@ export class CorePlatformService extends Platform {
 | 
			
		||||
        return this.isMobile() && this.is('android');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns whether the user agent is controlled by automation. I.e. Behat testing.
 | 
			
		||||
     *
 | 
			
		||||
     * @returns True if the user agent is controlled by automation, false otherwise.
 | 
			
		||||
     */
 | 
			
		||||
    isAutomated(): boolean {
 | 
			
		||||
        return !!navigator.webdriver;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if the app is running in an iOS mobile or tablet device.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @lms_from4.0
 | 
			
		||||
@core @app @javascript @lms_from4.0
 | 
			
		||||
Feature: Custom lang strings
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core @app @javascript
 | 
			
		||||
Feature: It navigates properly within activities.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core @app @javascript
 | 
			
		||||
Feature: It navigates properly using deep links.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core @app @javascript
 | 
			
		||||
Feature: It opens external links properly.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core @app @javascript
 | 
			
		||||
Feature: It navigates using gestures.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core @app @javascript
 | 
			
		||||
Feature: It navigates properly in pages with a split-view component.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript @lms_upto3.9
 | 
			
		||||
@core @app @javascript @lms_upto3.9
 | 
			
		||||
Feature: It opens files properly.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core @app @javascript
 | 
			
		||||
Feature: It opens files properly.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
@app @javascript
 | 
			
		||||
@core @app @javascript
 | 
			
		||||
Feature: It has a Behat runtime with testing helpers.
 | 
			
		||||
 | 
			
		||||
  Background:
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB  | 
@ -30,6 +30,7 @@ import { CoreSites, CoreSitesProvider } from '@services/sites';
 | 
			
		||||
import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
 | 
			
		||||
import { CoreSwipeNavigationDirective } from '@directives/swipe-navigation';
 | 
			
		||||
import { Swiper } from 'swiper';
 | 
			
		||||
import { LocalNotificationsMock } from '@features/emulator/services/local-notifications';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Behat runtime servive with public API.
 | 
			
		||||
@ -585,6 +586,13 @@ export class TestingBehatRuntimeService {
 | 
			
		||||
        console.log('BEHAT: ' + nowFormatted, ...args); // eslint-disable-line no-console
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Flush pending notifications.
 | 
			
		||||
     */
 | 
			
		||||
    flushNotifications(): void {
 | 
			
		||||
        (LocalNotifications as unknown as LocalNotificationsMock).flush();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check a notification is present.
 | 
			
		||||
     *
 | 
			
		||||
@ -608,6 +616,23 @@ export class TestingBehatRuntimeService {
 | 
			
		||||
        return (await LocalNotifications.isPresent(notification.id)) ? 'YES' : 'NO';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check a notification is scheduled.
 | 
			
		||||
     *
 | 
			
		||||
     * @param title Title of the notification
 | 
			
		||||
     * @param date Scheduled notification date.
 | 
			
		||||
     * @returns YES or NO: depending on the result.
 | 
			
		||||
     */
 | 
			
		||||
    async notificationIsScheduledWithText(title: string, date?: number): Promise<string> {
 | 
			
		||||
        const notifications = await LocalNotifications.getAllScheduled();
 | 
			
		||||
 | 
			
		||||
        const notification = notifications.find(
 | 
			
		||||
            (notification) => notification.title?.includes(title) && (!date || notification.trigger?.at?.getTime() === date),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return notification ? 'YES' : 'NO';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close notification.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user