<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://docs.moodle.org/dev/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Quen</id>
	<title>MoodleDocs - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://docs.moodle.org/dev/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Quen"/>
	<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/Special:Contributions/Quen"/>
	<updated>2026-08-09T22:07:19Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.43.5</generator>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=61655</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=61655"/>
		<updated>2022-01-26T14:27:45Z</updated>

		<summary type="html">&lt;p&gt;Quen: moved the text when I realised there was an existing entry for it, oops&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle App (Ionic 5)}}&lt;br /&gt;
The structure of this page is the following:&lt;br /&gt;
* The first part, up to the point where you get the &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; command to work, includes the basics of what you need to work on the app.&lt;br /&gt;
* The second part indicates how to build the app to run it on a native device (or emulator).&lt;br /&gt;
* The third part includes a list of troubleshooting advice. If you encounter a problem that is not already listed, please consider adding it.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of your development can happen on a browser, you only need to use an emulator if you have to work on native functionality.&lt;br /&gt;
&lt;br /&gt;
If you are just [[Moodle App Plugins Development Guide|adding mobile support to plugins]], you probably don&#039;t need to build the app yourself and you can skip reading this page.&lt;br /&gt;
== Requirements ==&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
Most of the time we recommend that you use a browser for development; the app will work in any Chromium-based browser. We recommend using the Chromium browser (an open source alternative to Google Chrome). You can get it from the [https://www.chromium.org/getting-involved/download-chromium official download page].&lt;br /&gt;
&lt;br /&gt;
To learn more about using a browser for development, and why it needs to be Chromium-based, read the [[Using the Moodle App in a browser]] page.&lt;br /&gt;
===Install git===&lt;br /&gt;
You will need to install Git in order to get the source code and upload your changes. If you are not familiar with it, we recommend that you get started reading the following guide: [https://git-scm.com/book/en/v2/Getting-Started-Installing-Git Installing Git].&lt;br /&gt;
===Install Node.js and Npm===&lt;br /&gt;
We recommend using a version manager like [https://github.com/nvm-sh/nvm nvm] to make this easier, you can prepare the correct environment running &amp;lt;code&amp;gt;nvm install&amp;lt;/code&amp;gt; in the project root. Remember to run this every time you work with the app, or if you’re not working on any other node projects in your computer you can run &amp;lt;code&amp;gt;nvm alias default `node -v`&amp;lt;/code&amp;gt; to make it the default. &lt;br /&gt;
&lt;br /&gt;
It may seem simpler and easier to install directly from [http://nodejs.org nodejs.org], but actually it is more tricky to get that to work. If you have previously installed Node directly, and want to switch to nvm, you need to uninstall node completely before installing nvm - or search for trouble-shooting instructions online.&lt;br /&gt;
===Install native SDKs===&lt;br /&gt;
If you intend to run the application in a native device, you will need to install that platform&#039;s native SDKs. You can learn how to set up your environment by reading Ionic’s documentation for [https://ionicframework.com/docs/developing/android Android] and [https://ionicframework.com/docs/developing/ios iOS].&lt;br /&gt;
===Windows only: Native build dependencies===&lt;br /&gt;
&amp;lt;code&amp;gt;node-gyp&amp;lt;/code&amp;gt; requires native build tools for your platform. If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if you don&#039;t]). On Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm install --global --production windows-build-tools&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Warning! This installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
This is only be necessary if you intend to compile the native iOS application. The push notifications plugin requires CocoaPods to work on a Mac; you can find the complete installation instructions in [https://cocoapods.org/ the official documentation], but it should work by running the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo gem install cocoapods&lt;br /&gt;
pod setup&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the &#039;&#039;&#039;Moodle.xcworkspace&#039;&#039;&#039; file, more information here: MOBILE-1970.&lt;br /&gt;
===Linux only: libsecret===&lt;br /&gt;
If you are using [[Moodle App scripts: gulp push|the gulp push script]], you need to have &amp;lt;code&amp;gt;libsecret&amp;lt;/code&amp;gt; installed before running &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt;. Depending on your distribution, you will need to run one of the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Debian/Ubuntu&lt;br /&gt;
sudo apt-get install libsecret-1-dev&lt;br /&gt;
&lt;br /&gt;
# Red Hat&lt;br /&gt;
sudo yum install libsecret-devel&lt;br /&gt;
&lt;br /&gt;
# Arch Linux&lt;br /&gt;
sudo pacman -S libsecret&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
== Running the app in a browser ==&lt;br /&gt;
You can obtain a copy of the source code by cloning the public repository. If you want to work on the latest development version, you should check out the &amp;lt;code&amp;gt;integration&amp;lt;/code&amp;gt; branch:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
git clone git@github.com:moodlehq/moodleapp.git&lt;br /&gt;
cd moodleapp&lt;br /&gt;
git checkout integration&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Once you have the correct environment set up, you can run the application with the following two commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will launch the application in a browser and you should be ready to start coding (you may also want to [[Using the Moodle App in a browser#Configuring_the_default_browser|configure the default browser]] for future runs). This compiles the entire application and can take a while, so don&#039;t worry if it doesn&#039;t open the browser instantly. Keep in mind that this command may open the browser before the dev server is ready, and you could get a network error. If that happens, just wait until the dev server is ready and launch the application again. It should be ready when you see a &amp;quot;Compiled successfully&amp;quot; message in the console. &lt;br /&gt;
&lt;br /&gt;
Congratulations, you have just completed the basics to become a Moodle App developer!&lt;br /&gt;
&lt;br /&gt;
If you need to work with native features or build packaged versions of the app, you can read the rest of this page.&lt;br /&gt;
&lt;br /&gt;
== Running the app in Android and iOS ==&lt;br /&gt;
The first time you want to run the application in a native device, this process will take a bit longer than usual because it needs to create the native projects and install native plugins. These will be created under the &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; folders respectively. If you ever run into issues during this process, try deleting both of these folders to get a clean start.&lt;br /&gt;
&lt;br /&gt;
In order to run the application on a native device, you can use one of the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm run dev:android  # Uses Live Reload, read below&lt;br /&gt;
npm run dev:ios      # Does NOT use Live Reload, read below&lt;br /&gt;
npm run prod:android # Uses AOT compilation, read below&lt;br /&gt;
npm run prod:ios     # Uses AOT compilation, read below&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If you get any errors while building, please see the [[#Troubleshooting|Troubleshooting]] section below.&lt;br /&gt;
=== Developing using Live Reload ===&lt;br /&gt;
Most of the time, it is recommended to develop using the &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; command. Working in a browser is faster, easier, and doesn&#039;t have as much overhead. However, in some situations you may want to run the application in a native device. If you want a similar development experience, you&#039;ll want to enable [https://ionicframework.com/docs/cli/livereload Live Reload].&lt;br /&gt;
&lt;br /&gt;
In Android, you can use the &amp;lt;code&amp;gt;npm run dev:android&amp;lt;/code&amp;gt; command and the application will launch on a device or emulator. Whenever you make any changes to your code, the application will reload automatically. Keep in mind that this command may launch the application before the dev server is ready, and you could get a network error. If that happens, just wait until the dev server is ready and launch the application again (you should see &amp;quot;Compiled successfully&amp;quot; in the console).&lt;br /&gt;
&lt;br /&gt;
In iOS, there are some limitations using live reload and that&#039;s why the &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; command doesn&#039;t enable it by default. You can still use it by running &amp;lt;code&amp;gt;npx ionic cordova run ios --livereload --external&amp;lt;/code&amp;gt;, but keep in mind that this will serve your application on an IP and will expose it to anyone connected to the same Wi-Fi network. Other than privacy concerns, this may cause some problems if you are working with iframes or local files. If you have any problems with that, you&#039;ll have to use the &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; command to recompile the application every time you change the code.&lt;br /&gt;
=== Compiling using AOT ===&lt;br /&gt;
Angular has 2 ways of compiling: [https://angular.io/guide/glossary#jit JIT] and [https://angular.io/guide/glossary#aot AOT].&lt;br /&gt;
&lt;br /&gt;
Running &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;npm run dev:android&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; compiles using JIT, which is faster to compile but the app takes longer to start. This is acceptable during development because it allows you to use Live Reload.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;npm run prod:android&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;npm run prod:ios&amp;lt;/code&amp;gt; commands use AOT compilation because they generate production bundles.&lt;br /&gt;
=== Using Android emulators ===&lt;br /&gt;
Most of the time, you should be using an emulator running recent versions of Android, and it should work fine. But sometimes, you may want to use an older version to test a specific behaviour.&lt;br /&gt;
&lt;br /&gt;
If you want to run the application in an Android 5 emulator, you’ll need to upgrade the system webview because emulators come with version 37 preinstalled. Your first idea may be to upgrade the webview using the Google Play store, but it will not work because the webview served by Google Play is &amp;lt;code&amp;gt;com.google.android.webview&amp;lt;/code&amp;gt; whilst the system webview used in emulators is &amp;lt;code&amp;gt;com.android.webview&amp;lt;/code&amp;gt;. You can do the following instead.&lt;br /&gt;
&lt;br /&gt;
Once you have [https://developer.android.com/studio/run/managing-avds created your Android 5 virtual device], you’ll need to do download [https://android.googlesource.com/platform/external/chromium-webview/+/refs/heads/oreo-m3-release/prebuilt/x86_64/ the apk for Webview 61] and run the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Open the folder where the “emulator” script is installed&lt;br /&gt;
cd $(dirname `which emulator`)&lt;br /&gt;
&lt;br /&gt;
# Boot the emulator in write mode&lt;br /&gt;
# (you can get a list of device names running “emulator -list-avds”)&lt;br /&gt;
emulator @DeviceName -writable-system&lt;br /&gt;
&lt;br /&gt;
# In a different shell, make /system writable&lt;br /&gt;
adb remount&lt;br /&gt;
&lt;br /&gt;
# Uninstall the webview app manually and reboot the device&lt;br /&gt;
adb shell&lt;br /&gt;
rm -rf /data/data/com.android.webview&lt;br /&gt;
rm -rf /system/app/webview&lt;br /&gt;
reboot&lt;br /&gt;
&lt;br /&gt;
# Install the new version&lt;br /&gt;
adb install webview.apk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
After doing this, remember to run the emulator in write mode for subsequent sessions, but you don’t need to call the &amp;lt;code&amp;gt;remount&amp;lt;/code&amp;gt; command every time.&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
This section contains a list of common errors that have been found and how to solve them. However, keep in mind that these may be platform-dependent and could not fix the problem in your machine, even if you are seeing the same error message.&lt;br /&gt;
=== General advice ===&lt;br /&gt;
If you are stuck with an error and you can&#039;t find a way to continue, here&#039;s a list of things you can do:&lt;br /&gt;
* Using git, look at the changes you have in your working directory and make sure that they aren&#039;t causing the problem. Be specially careful with changes in &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt;. You can see a list of the files you have modified running &amp;lt;code&amp;gt;git status&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Make sure that you are using the proper node and npm versions. You can see it looking at the &amp;lt;code&amp;gt;engines&amp;lt;/code&amp;gt; key in &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt;. If you are using [https://github.com/nvm-sh/nvm nvm], just run &amp;lt;code&amp;gt;nvm install&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Make sure that all dependencies have been installed properly. To be extra sure, run &amp;lt;code&amp;gt;npm ci&amp;lt;/code&amp;gt;; this will remove the &amp;lt;code&amp;gt;node_modules/&amp;lt;/code&amp;gt; folder and install all dependencies again exactly as described in your &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If you are having issues trying to build for Android or iOS, try removing the &amp;lt;code&amp;gt;www/&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; folders and try again.&lt;br /&gt;
* If you are using a development version, maybe the repository is broken and it&#039;s not your fault. Try checking out the &amp;lt;code&amp;gt;master&amp;lt;/code&amp;gt; branch and see if you&#039;re getting the same error.&lt;br /&gt;
* Try cloning the repository in a new folder and run through the instructions in this page again. If you can, try doing it on a different computer to make sure that you&#039;re doing everything properly and it&#039;s not a problem in your machine.&lt;br /&gt;
* Try creating [https://ionicframework.com/docs/cli/commands/start a blank Ionic application] and see if you&#039;re having the same problems. Make sure that you are using the same version of the main dependencies (Angular, Cordova, Ionic CLI, etc.).&lt;br /&gt;
* If you are searching for help online, maybe your problem has nothing to do with the Moodle App in particular an it&#039;s related with Ionic, Cordova, Angular, etc. Searching using the proper context will give you better solutions.&lt;br /&gt;
=== I get a blank page when launching the app ===&lt;br /&gt;
This error can happen when the application is launched properly but there is a runtime error. If you get a network connection error, this may be the expected behaviour. Wait until you see &amp;quot;Compiled successfully&amp;quot; in the console and reload.&lt;br /&gt;
&lt;br /&gt;
If that&#039;s not the case and you really are getting a blank page, look at the console and you should see some error indicating why the application is not rendering properly. Keep in mind that at the moment the application is already logging some errors and warnings, so make sure that you&#039;re not stuck trying to fix something that isn&#039;t causing the problem. You can follow any updates about this on MOBILE-3854.&lt;br /&gt;
&lt;br /&gt;
If you are not seeing any relevant logs, check out what to do on the [[#General_advice|General advice]] section.&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
To get more debug output from npm commands, see [https://docs.npmjs.com/cli/v7/using-npm/config the available configuration flags]. In particular try adding &amp;lt;code&amp;gt;--loglevel verbose&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;--loglevel info&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;--loglevel silly&amp;lt;/code&amp;gt; to the command-line.&lt;br /&gt;
=== I can&#039;t change the language ===&lt;br /&gt;
If you&#039;re getting a network error for a url like &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;http://localhost:8100/assets/lang/es.json&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;, this probably means that you haven&#039;t installed the language packs.&lt;br /&gt;
&lt;br /&gt;
Currently, you can install them in your machine running the &amp;lt;code&amp;gt;scripts/update_lang.sh&amp;lt;/code&amp;gt; script, but it may not work in your system if you don&#039;t have php installed. For future improvements, you can subscribe to MOBILE-3864.&lt;br /&gt;
=== Error: libsass bindings not found. Try reinstalling node-sass? ===&lt;br /&gt;
Most of the time, running the following command will fix the problem:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm rebuild node-sass&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== com.android.dex.DexException: Multiple dex files define XXX ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
configurations {&lt;br /&gt;
    all*.exclude group: &#039;com.android.support&#039;, module: &#039;support-v4&#039;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Could not resolve all dependencies for configuration &#039;:_debugCompile&#039;. ===&lt;br /&gt;
Open the Android SDK Manager and make sure you have installed: Android Support Repository, Android Support Library, Google Play Services and Google Repository.&lt;br /&gt;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
configurations.all {&lt;br /&gt;
    resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
android {&lt;br /&gt;
   compileSdkVersion 26&lt;br /&gt;
   buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK. ===&lt;br /&gt;
Download [https://developer.android.com/studio/ Android Studio] and copy the folder &amp;lt;code&amp;gt;android-studio/plugins/android/lib/templates&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;android-sdk-folder/Sdk/tools&amp;lt;/code&amp;gt;.&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and configure like this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
allprojects {&lt;br /&gt;
    repositories {&lt;br /&gt;
        jcenter()&lt;br /&gt;
        maven {&lt;br /&gt;
            url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;code&amp;gt;sudo apt-get install build-essential&amp;lt;/code&amp;gt; and retry.&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
If you see this error during &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt;, run &amp;lt;code&amp;gt;mkdir www&amp;lt;/code&amp;gt; and retry.&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &amp;lt;code&amp;gt;natives&amp;lt;/code&amp;gt; prior to 1.1.6. It can be fixed by running &amp;lt;code&amp;gt;npm install natives@1.1.6&amp;lt;/code&amp;gt;.&lt;br /&gt;
=== npm update check failed ===&lt;br /&gt;
You may get the following error on Windows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The suggested command does not work on Windows, so the solution is to manually check the ownership of all the files in &amp;lt;code&amp;gt;C:\Users\{username}\.config\configstore&amp;lt;/code&amp;gt;. In some cases, it can be &amp;lt;code&amp;gt;update-notifier-npm.json&amp;lt;/code&amp;gt; that got changed to be owned by Administrator.&lt;br /&gt;
=== Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ... ===&lt;br /&gt;
This is a common issue for Cygwin user running Node. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle App development.&lt;br /&gt;
&lt;br /&gt;
You don&#039;t need your Cygwin bin folder on the Windows path because it automatically gets added to the path when you lauch Cygwin bash.&lt;br /&gt;
=== The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically ===&lt;br /&gt;
This happens when you create the iOS platform with a certain &amp;lt;name&amp;gt; and then you change that name in config.xml. The solution seems to be removing and adding the iOS platform again:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx ionic platform remove ios&lt;br /&gt;
npx ionic platform add ios&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
===Failed to install &#039;cordova-plugin-x&#039; ===&lt;br /&gt;
Sometimes, you may see an error message similar to this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-x@x.x.x&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-x@&amp;gt;=x.x.x&amp;quot;.&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This can happen when a cordova plugin (let&#039;s call it &amp;quot;X&amp;quot;) is installed with an incorrect version. You can find this by removing the plugin and adding it again with the correct version:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx cordova plugin remove cordova-plugin-x&lt;br /&gt;
npx cordova plugin add cordova-plugin-x@x.x.x # Make sure to use the proper version here&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Please notice that if there is any plugin installed that depends on &amp;lt;code&amp;gt;cordova-plugin-x&amp;lt;/code&amp;gt; you&#039;ll have to remove and re-add them too.&lt;br /&gt;
=== doc.find is not a function ===&lt;br /&gt;
This happens in some environments, the solution is to run &amp;lt;code&amp;gt;npx cordova platform add ios&amp;lt;/code&amp;gt; before running the failing command. You should do this in a clean environment, once you&#039;ve seen the error running the command may not work. Try deleting &amp;lt;code&amp;gt;www/&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; before trying again.&lt;br /&gt;
&lt;br /&gt;
[https://stackoverflow.com/questions/47404622/edit-config-for-ios-usage-descriptions-doc-find-is-not-a-function Find more about this in StackOverflow]&lt;br /&gt;
===Mac: linker code failed with exit code 1===&lt;br /&gt;
If you get this error when trying to build the Moodle app with XCode, some dependencies might not have installed correctly.&lt;br /&gt;
&lt;br /&gt;
Ensure you have followed the [#Mac_only:_Push_notifications Mac only: Push notifications] steps above (particularly opening the .xcworkspace file rather than the .xcodeproj file). Then run the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd platforms/ios&lt;br /&gt;
pod install&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Now try running the build again in XCode.&lt;br /&gt;
===Windows: &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; hangs after &amp;quot;Starting &#039;watch&#039;&amp;quot;===&lt;br /&gt;
&lt;br /&gt;
If you follow the above procedure to run the app on a Windows system and get repeated &#039;Waiting for connectivity with NPM&#039; like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$ npm start&lt;br /&gt;
&lt;br /&gt;
&amp;gt; moodlemobile@3.9.5 start C:\Users\xxx\workspace\moodlemobile2&lt;br /&gt;
&amp;gt; ionic serve&lt;br /&gt;
&lt;br /&gt;
&amp;gt; npm.cmd run ionic:serve:before&lt;br /&gt;
&lt;br /&gt;
&amp;gt; moodlemobile@3.9.5 ionic:serve:before C:\Users\xxx\workspace\moodlemobile2&lt;br /&gt;
&amp;gt; gulp&lt;br /&gt;
&lt;br /&gt;
[13:59:24] Using gulpfile ~\workspace\moodlemobile2\gulpfile.js&lt;br /&gt;
[13:59:24] Starting &#039;default&#039;...&lt;br /&gt;
[13:59:24] Starting &#039;lang&#039;...&lt;br /&gt;
[13:59:24] Starting &#039;env&#039;...&lt;br /&gt;
[13:59:25] Finished &#039;env&#039; after 802 ms&lt;br /&gt;
[13:59:27] Finished &#039;lang&#039; after 3.4 s&lt;br /&gt;
[13:59:27] Finished &#039;default&#039; after 3.4 s&lt;br /&gt;
&amp;gt; npm.cmd run ionic:serve -- --host=localhost --port=8100 --project=app&lt;br /&gt;
[npm] &amp;gt; moodlemobile@3.9.5 ionic:serve C:\Users\xxx\workspace\moodlemobile2&lt;br /&gt;
[npm] &amp;gt; gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 ng serve &amp;quot;--host=localhost&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[npm] [13:59:46] Using gulpfile ~\workspace\moodlemobile2\gulpfile.js&lt;br /&gt;
[npm] [13:59:46] Starting &#039;watch&#039;...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can resolve the problem (sort of) by pressing ctrl-c to get out of it, then rerun the last command displayed, but this time with &#039;npx&#039; before each of the 2 commands. (This assumes you are using a bash shell.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 npx ng serve &amp;quot;--host=localhost&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There will be a pause (a few minutes) while building everything. It should finish with the line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;&lt;br /&gt;
: Compiled successfully.&lt;br /&gt;
&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you can access it by running Chrome and connecting to localhost:8100.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Moodle App Coding Style]]&lt;br /&gt;
* [[Debugging network requests in the Moodle_App]]&lt;br /&gt;
* [https://github.com/moodlehq/moodle-docker Moodle Docker]&lt;br /&gt;
* [http://ionicframework.com/docs/cli/ Ionic CLI docs]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=61654</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=61654"/>
		<updated>2022-01-26T14:23:11Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Windows &amp;#039;Waiting for connectivity with NPM&amp;#039; problem */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle App (Ionic 5)}}&lt;br /&gt;
The structure of this page is the following:&lt;br /&gt;
* The first part, up to the point where you get the &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; command to work, includes the basics of what you need to work on the app.&lt;br /&gt;
* The second part indicates how to build the app to run it on a native device (or emulator).&lt;br /&gt;
* The third part includes a list of troubleshooting advice. If you encounter a problem that is not already listed, please consider adding it.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of your development can happen on a browser, you only need to use an emulator if you have to work on native functionality.&lt;br /&gt;
&lt;br /&gt;
If you are just [[Moodle App Plugins Development Guide|adding mobile support to plugins]], you probably don&#039;t need to build the app yourself and you can skip reading this page.&lt;br /&gt;
== Requirements ==&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
Most of the time we recommend that you use a browser for development; the app will work in any Chromium-based browser. We recommend using the Chromium browser (an open source alternative to Google Chrome). You can get it from the [https://www.chromium.org/getting-involved/download-chromium official download page].&lt;br /&gt;
&lt;br /&gt;
To learn more about using a browser for development, and why it needs to be Chromium-based, read the [[Using the Moodle App in a browser]] page.&lt;br /&gt;
===Install git===&lt;br /&gt;
You will need to install Git in order to get the source code and upload your changes. If you are not familiar with it, we recommend that you get started reading the following guide: [https://git-scm.com/book/en/v2/Getting-Started-Installing-Git Installing Git].&lt;br /&gt;
===Install Node.js and Npm===&lt;br /&gt;
We recommend using a version manager like [https://github.com/nvm-sh/nvm nvm] to make this easier, you can prepare the correct environment running &amp;lt;code&amp;gt;nvm install&amp;lt;/code&amp;gt; in the project root. Remember to run this every time you work with the app, or if you’re not working on any other node projects in your computer you can run &amp;lt;code&amp;gt;nvm alias default `node -v`&amp;lt;/code&amp;gt; to make it the default. &lt;br /&gt;
&lt;br /&gt;
It may seem simpler and easier to install directly from [http://nodejs.org nodejs.org], but actually it is more tricky to get that to work. If you have previously installed Node directly, and want to switch to nvm, you need to uninstall node completely before installing nvm - or search for trouble-shooting instructions online.&lt;br /&gt;
===Install native SDKs===&lt;br /&gt;
If you intend to run the application in a native device, you will need to install that platform&#039;s native SDKs. You can learn how to set up your environment by reading Ionic’s documentation for [https://ionicframework.com/docs/developing/android Android] and [https://ionicframework.com/docs/developing/ios iOS].&lt;br /&gt;
===Windows only: Native build dependencies===&lt;br /&gt;
&amp;lt;code&amp;gt;node-gyp&amp;lt;/code&amp;gt; requires native build tools for your platform. If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if you don&#039;t]). On Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm install --global --production windows-build-tools&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Warning! This installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
This is only be necessary if you intend to compile the native iOS application. The push notifications plugin requires CocoaPods to work on a Mac; you can find the complete installation instructions in [https://cocoapods.org/ the official documentation], but it should work by running the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo gem install cocoapods&lt;br /&gt;
pod setup&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the &#039;&#039;&#039;Moodle.xcworkspace&#039;&#039;&#039; file, more information here: MOBILE-1970.&lt;br /&gt;
===Linux only: libsecret===&lt;br /&gt;
If you are using [[Moodle App scripts: gulp push|the gulp push script]], you need to have &amp;lt;code&amp;gt;libsecret&amp;lt;/code&amp;gt; installed before running &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt;. Depending on your distribution, you will need to run one of the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Debian/Ubuntu&lt;br /&gt;
sudo apt-get install libsecret-1-dev&lt;br /&gt;
&lt;br /&gt;
# Red Hat&lt;br /&gt;
sudo yum install libsecret-devel&lt;br /&gt;
&lt;br /&gt;
# Arch Linux&lt;br /&gt;
sudo pacman -S libsecret&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
== Running the app in a browser ==&lt;br /&gt;
You can obtain a copy of the source code by cloning the public repository. If you want to work on the latest development version, you should check out the &amp;lt;code&amp;gt;integration&amp;lt;/code&amp;gt; branch:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
git clone git@github.com:moodlehq/moodleapp.git&lt;br /&gt;
cd moodleapp&lt;br /&gt;
git checkout integration&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Once you have the correct environment set up, you can run the application with the following two commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will launch the application in a browser and you should be ready to start coding (you may also want to [[Using the Moodle App in a browser#Configuring_the_default_browser|configure the default browser]] for future runs). This compiles the entire application and can take a while, so don&#039;t worry if it doesn&#039;t open the browser instantly. Keep in mind that this command may open the browser before the dev server is ready, and you could get a network error. If that happens, just wait until the dev server is ready and launch the application again. It should be ready when you see a &amp;quot;Compiled successfully&amp;quot; message in the console. &lt;br /&gt;
&lt;br /&gt;
Congratulations, you have just completed the basics to become a Moodle App developer!&lt;br /&gt;
&lt;br /&gt;
If you need to work with native features or build packaged versions of the app, you can read the rest of this page.&lt;br /&gt;
&lt;br /&gt;
=== Windows &#039;Waiting for connectivity with NPM&#039; problem ===&lt;br /&gt;
&lt;br /&gt;
If you follow the above procedure on a Windows system (may depend on installed versions etc.) and get repeated &#039;Waiting for connectivity with NPM&#039; like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$ npm start&lt;br /&gt;
&lt;br /&gt;
&amp;gt; moodlemobile@3.9.5 start C:\Users\xxx\workspace\moodlemobile2&lt;br /&gt;
&amp;gt; ionic serve&lt;br /&gt;
&lt;br /&gt;
&amp;gt; npm.cmd run ionic:serve:before&lt;br /&gt;
&lt;br /&gt;
&amp;gt; moodlemobile@3.9.5 ionic:serve:before C:\Users\xxx\workspace\moodlemobile2&lt;br /&gt;
&amp;gt; gulp&lt;br /&gt;
&lt;br /&gt;
[13:59:24] Using gulpfile ~\workspace\moodlemobile2\gulpfile.js&lt;br /&gt;
[13:59:24] Starting &#039;default&#039;...&lt;br /&gt;
[13:59:24] Starting &#039;lang&#039;...&lt;br /&gt;
[13:59:24] Starting &#039;env&#039;...&lt;br /&gt;
[13:59:25] Finished &#039;env&#039; after 802 ms&lt;br /&gt;
[13:59:27] Finished &#039;lang&#039; after 3.4 s&lt;br /&gt;
[13:59:27] Finished &#039;default&#039; after 3.4 s&lt;br /&gt;
&amp;gt; npm.cmd run ionic:serve -- --host=localhost --port=8100 --project=app&lt;br /&gt;
[npm] &amp;gt; moodlemobile@3.9.5 ionic:serve C:\Users\xxx\workspace\moodlemobile2&lt;br /&gt;
[npm] &amp;gt; gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 ng serve &amp;quot;--host=localhost&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[npm] [13:59:46] Using gulpfile ~\workspace\moodlemobile2\gulpfile.js&lt;br /&gt;
[npm] [13:59:46] Starting &#039;watch&#039;...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can resolve the problem (sort of) by pressing ctrl-c to get out of it, then rerun the last command displayed, but this time with &#039;npx&#039; before each of the 2 commands. (This assumes you are using a bash shell.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 npx ng serve &amp;quot;--host=localhost&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There will be a pause (a few minutes) while building everything. It should finish with the line:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;&lt;br /&gt;
: Compiled successfully.&lt;br /&gt;
&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you can access it by running Chrome and connecting to localhost:8100.&lt;br /&gt;
&lt;br /&gt;
== Running the app in Android and iOS ==&lt;br /&gt;
The first time you want to run the application in a native device, this process will take a bit longer than usual because it needs to create the native projects and install native plugins. These will be created under the &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; folders respectively. If you ever run into issues during this process, try deleting both of these folders to get a clean start.&lt;br /&gt;
&lt;br /&gt;
In order to run the application on a native device, you can use one of the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm run dev:android  # Uses Live Reload, read below&lt;br /&gt;
npm run dev:ios      # Does NOT use Live Reload, read below&lt;br /&gt;
npm run prod:android # Uses AOT compilation, read below&lt;br /&gt;
npm run prod:ios     # Uses AOT compilation, read below&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If you get any errors while building, please see the [[#Troubleshooting|Troubleshooting]] section below.&lt;br /&gt;
=== Developing using Live Reload ===&lt;br /&gt;
Most of the time, it is recommended to develop using the &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; command. Working in a browser is faster, easier, and doesn&#039;t have as much overhead. However, in some situations you may want to run the application in a native device. If you want a similar development experience, you&#039;ll want to enable [https://ionicframework.com/docs/cli/livereload Live Reload].&lt;br /&gt;
&lt;br /&gt;
In Android, you can use the &amp;lt;code&amp;gt;npm run dev:android&amp;lt;/code&amp;gt; command and the application will launch on a device or emulator. Whenever you make any changes to your code, the application will reload automatically. Keep in mind that this command may launch the application before the dev server is ready, and you could get a network error. If that happens, just wait until the dev server is ready and launch the application again (you should see &amp;quot;Compiled successfully&amp;quot; in the console).&lt;br /&gt;
&lt;br /&gt;
In iOS, there are some limitations using live reload and that&#039;s why the &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; command doesn&#039;t enable it by default. You can still use it by running &amp;lt;code&amp;gt;npx ionic cordova run ios --livereload --external&amp;lt;/code&amp;gt;, but keep in mind that this will serve your application on an IP and will expose it to anyone connected to the same Wi-Fi network. Other than privacy concerns, this may cause some problems if you are working with iframes or local files. If you have any problems with that, you&#039;ll have to use the &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; command to recompile the application every time you change the code.&lt;br /&gt;
=== Compiling using AOT ===&lt;br /&gt;
Angular has 2 ways of compiling: [https://angular.io/guide/glossary#jit JIT] and [https://angular.io/guide/glossary#aot AOT].&lt;br /&gt;
&lt;br /&gt;
Running &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;npm run dev:android&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; compiles using JIT, which is faster to compile but the app takes longer to start. This is acceptable during development because it allows you to use Live Reload.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;npm run prod:android&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;npm run prod:ios&amp;lt;/code&amp;gt; commands use AOT compilation because they generate production bundles.&lt;br /&gt;
=== Using Android emulators ===&lt;br /&gt;
Most of the time, you should be using an emulator running recent versions of Android, and it should work fine. But sometimes, you may want to use an older version to test a specific behaviour.&lt;br /&gt;
&lt;br /&gt;
If you want to run the application in an Android 5 emulator, you’ll need to upgrade the system webview because emulators come with version 37 preinstalled. Your first idea may be to upgrade the webview using the Google Play store, but it will not work because the webview served by Google Play is &amp;lt;code&amp;gt;com.google.android.webview&amp;lt;/code&amp;gt; whilst the system webview used in emulators is &amp;lt;code&amp;gt;com.android.webview&amp;lt;/code&amp;gt;. You can do the following instead.&lt;br /&gt;
&lt;br /&gt;
Once you have [https://developer.android.com/studio/run/managing-avds created your Android 5 virtual device], you’ll need to do download [https://android.googlesource.com/platform/external/chromium-webview/+/refs/heads/oreo-m3-release/prebuilt/x86_64/ the apk for Webview 61] and run the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Open the folder where the “emulator” script is installed&lt;br /&gt;
cd $(dirname `which emulator`)&lt;br /&gt;
&lt;br /&gt;
# Boot the emulator in write mode&lt;br /&gt;
# (you can get a list of device names running “emulator -list-avds”)&lt;br /&gt;
emulator @DeviceName -writable-system&lt;br /&gt;
&lt;br /&gt;
# In a different shell, make /system writable&lt;br /&gt;
adb remount&lt;br /&gt;
&lt;br /&gt;
# Uninstall the webview app manually and reboot the device&lt;br /&gt;
adb shell&lt;br /&gt;
rm -rf /data/data/com.android.webview&lt;br /&gt;
rm -rf /system/app/webview&lt;br /&gt;
reboot&lt;br /&gt;
&lt;br /&gt;
# Install the new version&lt;br /&gt;
adb install webview.apk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
After doing this, remember to run the emulator in write mode for subsequent sessions, but you don’t need to call the &amp;lt;code&amp;gt;remount&amp;lt;/code&amp;gt; command every time.&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
This section contains a list of common errors that have been found and how to solve them. However, keep in mind that these may be platform-dependent and could not fix the problem in your machine, even if you are seeing the same error message.&lt;br /&gt;
=== General advice ===&lt;br /&gt;
If you are stuck with an error and you can&#039;t find a way to continue, here&#039;s a list of things you can do:&lt;br /&gt;
* Using git, look at the changes you have in your working directory and make sure that they aren&#039;t causing the problem. Be specially careful with changes in &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt;. You can see a list of the files you have modified running &amp;lt;code&amp;gt;git status&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Make sure that you are using the proper node and npm versions. You can see it looking at the &amp;lt;code&amp;gt;engines&amp;lt;/code&amp;gt; key in &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt;. If you are using [https://github.com/nvm-sh/nvm nvm], just run &amp;lt;code&amp;gt;nvm install&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Make sure that all dependencies have been installed properly. To be extra sure, run &amp;lt;code&amp;gt;npm ci&amp;lt;/code&amp;gt;; this will remove the &amp;lt;code&amp;gt;node_modules/&amp;lt;/code&amp;gt; folder and install all dependencies again exactly as described in your &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If you are having issues trying to build for Android or iOS, try removing the &amp;lt;code&amp;gt;www/&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; folders and try again.&lt;br /&gt;
* If you are using a development version, maybe the repository is broken and it&#039;s not your fault. Try checking out the &amp;lt;code&amp;gt;master&amp;lt;/code&amp;gt; branch and see if you&#039;re getting the same error.&lt;br /&gt;
* Try cloning the repository in a new folder and run through the instructions in this page again. If you can, try doing it on a different computer to make sure that you&#039;re doing everything properly and it&#039;s not a problem in your machine.&lt;br /&gt;
* Try creating [https://ionicframework.com/docs/cli/commands/start a blank Ionic application] and see if you&#039;re having the same problems. Make sure that you are using the same version of the main dependencies (Angular, Cordova, Ionic CLI, etc.).&lt;br /&gt;
* If you are searching for help online, maybe your problem has nothing to do with the Moodle App in particular an it&#039;s related with Ionic, Cordova, Angular, etc. Searching using the proper context will give you better solutions.&lt;br /&gt;
=== I get a blank page when launching the app ===&lt;br /&gt;
This error can happen when the application is launched properly but there is a runtime error. If you get a network connection error, this may be the expected behaviour. Wait until you see &amp;quot;Compiled successfully&amp;quot; in the console and reload.&lt;br /&gt;
&lt;br /&gt;
If that&#039;s not the case and you really are getting a blank page, look at the console and you should see some error indicating why the application is not rendering properly. Keep in mind that at the moment the application is already logging some errors and warnings, so make sure that you&#039;re not stuck trying to fix something that isn&#039;t causing the problem. You can follow any updates about this on MOBILE-3854.&lt;br /&gt;
&lt;br /&gt;
If you are not seeing any relevant logs, check out what to do on the [[#General_advice|General advice]] section.&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
To get more debug output from npm commands, see [https://docs.npmjs.com/cli/v7/using-npm/config the available configuration flags]. In particular try adding &amp;lt;code&amp;gt;--loglevel verbose&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;--loglevel info&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;--loglevel silly&amp;lt;/code&amp;gt; to the command-line.&lt;br /&gt;
=== I can&#039;t change the language ===&lt;br /&gt;
If you&#039;re getting a network error for a url like &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;http://localhost:8100/assets/lang/es.json&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;, this probably means that you haven&#039;t installed the language packs.&lt;br /&gt;
&lt;br /&gt;
Currently, you can install them in your machine running the &amp;lt;code&amp;gt;scripts/update_lang.sh&amp;lt;/code&amp;gt; script, but it may not work in your system if you don&#039;t have php installed. For future improvements, you can subscribe to MOBILE-3864.&lt;br /&gt;
=== Error: libsass bindings not found. Try reinstalling node-sass? ===&lt;br /&gt;
Most of the time, running the following command will fix the problem:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm rebuild node-sass&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== com.android.dex.DexException: Multiple dex files define XXX ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
configurations {&lt;br /&gt;
    all*.exclude group: &#039;com.android.support&#039;, module: &#039;support-v4&#039;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Could not resolve all dependencies for configuration &#039;:_debugCompile&#039;. ===&lt;br /&gt;
Open the Android SDK Manager and make sure you have installed: Android Support Repository, Android Support Library, Google Play Services and Google Repository.&lt;br /&gt;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
configurations.all {&lt;br /&gt;
    resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
android {&lt;br /&gt;
   compileSdkVersion 26&lt;br /&gt;
   buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK. ===&lt;br /&gt;
Download [https://developer.android.com/studio/ Android Studio] and copy the folder &amp;lt;code&amp;gt;android-studio/plugins/android/lib/templates&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;android-sdk-folder/Sdk/tools&amp;lt;/code&amp;gt;.&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and configure like this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
allprojects {&lt;br /&gt;
    repositories {&lt;br /&gt;
        jcenter()&lt;br /&gt;
        maven {&lt;br /&gt;
            url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;code&amp;gt;sudo apt-get install build-essential&amp;lt;/code&amp;gt; and retry.&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
If you see this error during &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt;, run &amp;lt;code&amp;gt;mkdir www&amp;lt;/code&amp;gt; and retry.&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &amp;lt;code&amp;gt;natives&amp;lt;/code&amp;gt; prior to 1.1.6. It can be fixed by running &amp;lt;code&amp;gt;npm install natives@1.1.6&amp;lt;/code&amp;gt;.&lt;br /&gt;
=== npm update check failed ===&lt;br /&gt;
You may get the following error on Windows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The suggested command does not work on Windows, so the solution is to manually check the ownership of all the files in &amp;lt;code&amp;gt;C:\Users\{username}\.config\configstore&amp;lt;/code&amp;gt;. In some cases, it can be &amp;lt;code&amp;gt;update-notifier-npm.json&amp;lt;/code&amp;gt; that got changed to be owned by Administrator.&lt;br /&gt;
=== Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ... ===&lt;br /&gt;
This is a common issue for Cygwin user running Node. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle App development.&lt;br /&gt;
&lt;br /&gt;
You don&#039;t need your Cygwin bin folder on the Windows path because it automatically gets added to the path when you lauch Cygwin bash.&lt;br /&gt;
=== The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically ===&lt;br /&gt;
This happens when you create the iOS platform with a certain &amp;lt;name&amp;gt; and then you change that name in config.xml. The solution seems to be removing and adding the iOS platform again:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx ionic platform remove ios&lt;br /&gt;
npx ionic platform add ios&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
===Failed to install &#039;cordova-plugin-x&#039; ===&lt;br /&gt;
Sometimes, you may see an error message similar to this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-x@x.x.x&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-x@&amp;gt;=x.x.x&amp;quot;.&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This can happen when a cordova plugin (let&#039;s call it &amp;quot;X&amp;quot;) is installed with an incorrect version. You can find this by removing the plugin and adding it again with the correct version:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx cordova plugin remove cordova-plugin-x&lt;br /&gt;
npx cordova plugin add cordova-plugin-x@x.x.x # Make sure to use the proper version here&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Please notice that if there is any plugin installed that depends on &amp;lt;code&amp;gt;cordova-plugin-x&amp;lt;/code&amp;gt; you&#039;ll have to remove and re-add them too.&lt;br /&gt;
=== doc.find is not a function ===&lt;br /&gt;
This happens in some environments, the solution is to run &amp;lt;code&amp;gt;npx cordova platform add ios&amp;lt;/code&amp;gt; before running the failing command. You should do this in a clean environment, once you&#039;ve seen the error running the command may not work. Try deleting &amp;lt;code&amp;gt;www/&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; before trying again.&lt;br /&gt;
&lt;br /&gt;
[https://stackoverflow.com/questions/47404622/edit-config-for-ios-usage-descriptions-doc-find-is-not-a-function Find more about this in StackOverflow]&lt;br /&gt;
===Mac: linker code failed with exit code 1===&lt;br /&gt;
If you get this error when trying to build the Moodle app with XCode, some dependencies might not have installed correctly.&lt;br /&gt;
&lt;br /&gt;
Ensure you have followed the [#Mac_only:_Push_notifications Mac only: Push notifications] steps above (particularly opening the .xcworkspace file rather than the .xcodeproj file). Then run the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd platforms/ios&lt;br /&gt;
pod install&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Now try running the build again in XCode.&lt;br /&gt;
===Windows: &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; hangs after &amp;quot;Starting &#039;watch&#039;&amp;quot;===&lt;br /&gt;
Try running the commands generated by &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; directly in bash:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 npx ng serve &amp;quot;--host=0.0.0.0&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Moodle App Coding Style]]&lt;br /&gt;
* [[Debugging network requests in the Moodle_App]]&lt;br /&gt;
* [https://github.com/moodlehq/moodle-docker Moodle Docker]&lt;br /&gt;
* [http://ionicframework.com/docs/cli/ Ionic CLI docs]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=61653</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=61653"/>
		<updated>2022-01-26T14:22:23Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Windows &amp;#039;Waiting for connectivity with NPM&amp;#039; problem */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle App (Ionic 5)}}&lt;br /&gt;
The structure of this page is the following:&lt;br /&gt;
* The first part, up to the point where you get the &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; command to work, includes the basics of what you need to work on the app.&lt;br /&gt;
* The second part indicates how to build the app to run it on a native device (or emulator).&lt;br /&gt;
* The third part includes a list of troubleshooting advice. If you encounter a problem that is not already listed, please consider adding it.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of your development can happen on a browser, you only need to use an emulator if you have to work on native functionality.&lt;br /&gt;
&lt;br /&gt;
If you are just [[Moodle App Plugins Development Guide|adding mobile support to plugins]], you probably don&#039;t need to build the app yourself and you can skip reading this page.&lt;br /&gt;
== Requirements ==&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
Most of the time we recommend that you use a browser for development; the app will work in any Chromium-based browser. We recommend using the Chromium browser (an open source alternative to Google Chrome). You can get it from the [https://www.chromium.org/getting-involved/download-chromium official download page].&lt;br /&gt;
&lt;br /&gt;
To learn more about using a browser for development, and why it needs to be Chromium-based, read the [[Using the Moodle App in a browser]] page.&lt;br /&gt;
===Install git===&lt;br /&gt;
You will need to install Git in order to get the source code and upload your changes. If you are not familiar with it, we recommend that you get started reading the following guide: [https://git-scm.com/book/en/v2/Getting-Started-Installing-Git Installing Git].&lt;br /&gt;
===Install Node.js and Npm===&lt;br /&gt;
We recommend using a version manager like [https://github.com/nvm-sh/nvm nvm] to make this easier, you can prepare the correct environment running &amp;lt;code&amp;gt;nvm install&amp;lt;/code&amp;gt; in the project root. Remember to run this every time you work with the app, or if you’re not working on any other node projects in your computer you can run &amp;lt;code&amp;gt;nvm alias default `node -v`&amp;lt;/code&amp;gt; to make it the default. &lt;br /&gt;
&lt;br /&gt;
It may seem simpler and easier to install directly from [http://nodejs.org nodejs.org], but actually it is more tricky to get that to work. If you have previously installed Node directly, and want to switch to nvm, you need to uninstall node completely before installing nvm - or search for trouble-shooting instructions online.&lt;br /&gt;
===Install native SDKs===&lt;br /&gt;
If you intend to run the application in a native device, you will need to install that platform&#039;s native SDKs. You can learn how to set up your environment by reading Ionic’s documentation for [https://ionicframework.com/docs/developing/android Android] and [https://ionicframework.com/docs/developing/ios iOS].&lt;br /&gt;
===Windows only: Native build dependencies===&lt;br /&gt;
&amp;lt;code&amp;gt;node-gyp&amp;lt;/code&amp;gt; requires native build tools for your platform. If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if you don&#039;t]). On Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm install --global --production windows-build-tools&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Warning! This installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
This is only be necessary if you intend to compile the native iOS application. The push notifications plugin requires CocoaPods to work on a Mac; you can find the complete installation instructions in [https://cocoapods.org/ the official documentation], but it should work by running the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo gem install cocoapods&lt;br /&gt;
pod setup&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the &#039;&#039;&#039;Moodle.xcworkspace&#039;&#039;&#039; file, more information here: MOBILE-1970.&lt;br /&gt;
===Linux only: libsecret===&lt;br /&gt;
If you are using [[Moodle App scripts: gulp push|the gulp push script]], you need to have &amp;lt;code&amp;gt;libsecret&amp;lt;/code&amp;gt; installed before running &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt;. Depending on your distribution, you will need to run one of the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Debian/Ubuntu&lt;br /&gt;
sudo apt-get install libsecret-1-dev&lt;br /&gt;
&lt;br /&gt;
# Red Hat&lt;br /&gt;
sudo yum install libsecret-devel&lt;br /&gt;
&lt;br /&gt;
# Arch Linux&lt;br /&gt;
sudo pacman -S libsecret&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
== Running the app in a browser ==&lt;br /&gt;
You can obtain a copy of the source code by cloning the public repository. If you want to work on the latest development version, you should check out the &amp;lt;code&amp;gt;integration&amp;lt;/code&amp;gt; branch:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
git clone git@github.com:moodlehq/moodleapp.git&lt;br /&gt;
cd moodleapp&lt;br /&gt;
git checkout integration&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Once you have the correct environment set up, you can run the application with the following two commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will launch the application in a browser and you should be ready to start coding (you may also want to [[Using the Moodle App in a browser#Configuring_the_default_browser|configure the default browser]] for future runs). This compiles the entire application and can take a while, so don&#039;t worry if it doesn&#039;t open the browser instantly. Keep in mind that this command may open the browser before the dev server is ready, and you could get a network error. If that happens, just wait until the dev server is ready and launch the application again. It should be ready when you see a &amp;quot;Compiled successfully&amp;quot; message in the console. &lt;br /&gt;
&lt;br /&gt;
Congratulations, you have just completed the basics to become a Moodle App developer!&lt;br /&gt;
&lt;br /&gt;
If you need to work with native features or build packaged versions of the app, you can read the rest of this page.&lt;br /&gt;
&lt;br /&gt;
=== Windows &#039;Waiting for connectivity with NPM&#039; problem ===&lt;br /&gt;
&lt;br /&gt;
If you follow the above procedure on a Windows system (may depend on installed versions etc.) and get repeated &#039;Waiting for connectivity with NPM&#039; like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$ npm start&lt;br /&gt;
&lt;br /&gt;
&amp;gt; moodlemobile@3.9.5 start C:\Users\xxx\workspace\moodlemobile2&lt;br /&gt;
&amp;gt; ionic serve&lt;br /&gt;
&lt;br /&gt;
&amp;gt; npm.cmd run ionic:serve:before&lt;br /&gt;
&lt;br /&gt;
&amp;gt; moodlemobile@3.9.5 ionic:serve:before C:\Users\xxx\workspace\moodlemobile2&lt;br /&gt;
&amp;gt; gulp&lt;br /&gt;
&lt;br /&gt;
[13:59:24] Using gulpfile ~\workspace\moodlemobile2\gulpfile.js&lt;br /&gt;
[13:59:24] Starting &#039;default&#039;...&lt;br /&gt;
[13:59:24] Starting &#039;lang&#039;...&lt;br /&gt;
[13:59:24] Starting &#039;env&#039;...&lt;br /&gt;
[13:59:25] Finished &#039;env&#039; after 802 ms&lt;br /&gt;
[13:59:27] Finished &#039;lang&#039; after 3.4 s&lt;br /&gt;
[13:59:27] Finished &#039;default&#039; after 3.4 s&lt;br /&gt;
&amp;gt; npm.cmd run ionic:serve -- --host=localhost --port=8100 --project=app&lt;br /&gt;
[npm] &amp;gt; moodlemobile@3.9.5 ionic:serve C:\Users\xxx\workspace\moodlemobile2&lt;br /&gt;
[npm] &amp;gt; gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 ng serve &amp;quot;--host=localhost&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[npm] [13:59:46] Using gulpfile ~\workspace\moodlemobile2\gulpfile.js&lt;br /&gt;
[npm] [13:59:46] Starting &#039;watch&#039;...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can resolve the problem (sort of) by pressing ctrl-c to get out of it, then rerun the last command displayed, but this time with &#039;npx&#039; before each of the 2 commands. (This assumes you are using a bash shell.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 npx ng serve &amp;quot;--host=localhost&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There will be a pause (a few minutes) while building everything. It should finish with the lines:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;&lt;br /&gt;
** Angular Live Development Server is listening on localhost:8100, open your browser on http://localhost:8100/ **&lt;br /&gt;
: Compiled successfully.&lt;br /&gt;
&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you can access it by running Chrome and connecting to localhost:8100.&lt;br /&gt;
&lt;br /&gt;
== Running the app in Android and iOS ==&lt;br /&gt;
The first time you want to run the application in a native device, this process will take a bit longer than usual because it needs to create the native projects and install native plugins. These will be created under the &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; folders respectively. If you ever run into issues during this process, try deleting both of these folders to get a clean start.&lt;br /&gt;
&lt;br /&gt;
In order to run the application on a native device, you can use one of the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm run dev:android  # Uses Live Reload, read below&lt;br /&gt;
npm run dev:ios      # Does NOT use Live Reload, read below&lt;br /&gt;
npm run prod:android # Uses AOT compilation, read below&lt;br /&gt;
npm run prod:ios     # Uses AOT compilation, read below&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If you get any errors while building, please see the [[#Troubleshooting|Troubleshooting]] section below.&lt;br /&gt;
=== Developing using Live Reload ===&lt;br /&gt;
Most of the time, it is recommended to develop using the &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; command. Working in a browser is faster, easier, and doesn&#039;t have as much overhead. However, in some situations you may want to run the application in a native device. If you want a similar development experience, you&#039;ll want to enable [https://ionicframework.com/docs/cli/livereload Live Reload].&lt;br /&gt;
&lt;br /&gt;
In Android, you can use the &amp;lt;code&amp;gt;npm run dev:android&amp;lt;/code&amp;gt; command and the application will launch on a device or emulator. Whenever you make any changes to your code, the application will reload automatically. Keep in mind that this command may launch the application before the dev server is ready, and you could get a network error. If that happens, just wait until the dev server is ready and launch the application again (you should see &amp;quot;Compiled successfully&amp;quot; in the console).&lt;br /&gt;
&lt;br /&gt;
In iOS, there are some limitations using live reload and that&#039;s why the &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; command doesn&#039;t enable it by default. You can still use it by running &amp;lt;code&amp;gt;npx ionic cordova run ios --livereload --external&amp;lt;/code&amp;gt;, but keep in mind that this will serve your application on an IP and will expose it to anyone connected to the same Wi-Fi network. Other than privacy concerns, this may cause some problems if you are working with iframes or local files. If you have any problems with that, you&#039;ll have to use the &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; command to recompile the application every time you change the code.&lt;br /&gt;
=== Compiling using AOT ===&lt;br /&gt;
Angular has 2 ways of compiling: [https://angular.io/guide/glossary#jit JIT] and [https://angular.io/guide/glossary#aot AOT].&lt;br /&gt;
&lt;br /&gt;
Running &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;npm run dev:android&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; compiles using JIT, which is faster to compile but the app takes longer to start. This is acceptable during development because it allows you to use Live Reload.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;npm run prod:android&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;npm run prod:ios&amp;lt;/code&amp;gt; commands use AOT compilation because they generate production bundles.&lt;br /&gt;
=== Using Android emulators ===&lt;br /&gt;
Most of the time, you should be using an emulator running recent versions of Android, and it should work fine. But sometimes, you may want to use an older version to test a specific behaviour.&lt;br /&gt;
&lt;br /&gt;
If you want to run the application in an Android 5 emulator, you’ll need to upgrade the system webview because emulators come with version 37 preinstalled. Your first idea may be to upgrade the webview using the Google Play store, but it will not work because the webview served by Google Play is &amp;lt;code&amp;gt;com.google.android.webview&amp;lt;/code&amp;gt; whilst the system webview used in emulators is &amp;lt;code&amp;gt;com.android.webview&amp;lt;/code&amp;gt;. You can do the following instead.&lt;br /&gt;
&lt;br /&gt;
Once you have [https://developer.android.com/studio/run/managing-avds created your Android 5 virtual device], you’ll need to do download [https://android.googlesource.com/platform/external/chromium-webview/+/refs/heads/oreo-m3-release/prebuilt/x86_64/ the apk for Webview 61] and run the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Open the folder where the “emulator” script is installed&lt;br /&gt;
cd $(dirname `which emulator`)&lt;br /&gt;
&lt;br /&gt;
# Boot the emulator in write mode&lt;br /&gt;
# (you can get a list of device names running “emulator -list-avds”)&lt;br /&gt;
emulator @DeviceName -writable-system&lt;br /&gt;
&lt;br /&gt;
# In a different shell, make /system writable&lt;br /&gt;
adb remount&lt;br /&gt;
&lt;br /&gt;
# Uninstall the webview app manually and reboot the device&lt;br /&gt;
adb shell&lt;br /&gt;
rm -rf /data/data/com.android.webview&lt;br /&gt;
rm -rf /system/app/webview&lt;br /&gt;
reboot&lt;br /&gt;
&lt;br /&gt;
# Install the new version&lt;br /&gt;
adb install webview.apk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
After doing this, remember to run the emulator in write mode for subsequent sessions, but you don’t need to call the &amp;lt;code&amp;gt;remount&amp;lt;/code&amp;gt; command every time.&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
This section contains a list of common errors that have been found and how to solve them. However, keep in mind that these may be platform-dependent and could not fix the problem in your machine, even if you are seeing the same error message.&lt;br /&gt;
=== General advice ===&lt;br /&gt;
If you are stuck with an error and you can&#039;t find a way to continue, here&#039;s a list of things you can do:&lt;br /&gt;
* Using git, look at the changes you have in your working directory and make sure that they aren&#039;t causing the problem. Be specially careful with changes in &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt;. You can see a list of the files you have modified running &amp;lt;code&amp;gt;git status&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Make sure that you are using the proper node and npm versions. You can see it looking at the &amp;lt;code&amp;gt;engines&amp;lt;/code&amp;gt; key in &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt;. If you are using [https://github.com/nvm-sh/nvm nvm], just run &amp;lt;code&amp;gt;nvm install&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Make sure that all dependencies have been installed properly. To be extra sure, run &amp;lt;code&amp;gt;npm ci&amp;lt;/code&amp;gt;; this will remove the &amp;lt;code&amp;gt;node_modules/&amp;lt;/code&amp;gt; folder and install all dependencies again exactly as described in your &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If you are having issues trying to build for Android or iOS, try removing the &amp;lt;code&amp;gt;www/&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; folders and try again.&lt;br /&gt;
* If you are using a development version, maybe the repository is broken and it&#039;s not your fault. Try checking out the &amp;lt;code&amp;gt;master&amp;lt;/code&amp;gt; branch and see if you&#039;re getting the same error.&lt;br /&gt;
* Try cloning the repository in a new folder and run through the instructions in this page again. If you can, try doing it on a different computer to make sure that you&#039;re doing everything properly and it&#039;s not a problem in your machine.&lt;br /&gt;
* Try creating [https://ionicframework.com/docs/cli/commands/start a blank Ionic application] and see if you&#039;re having the same problems. Make sure that you are using the same version of the main dependencies (Angular, Cordova, Ionic CLI, etc.).&lt;br /&gt;
* If you are searching for help online, maybe your problem has nothing to do with the Moodle App in particular an it&#039;s related with Ionic, Cordova, Angular, etc. Searching using the proper context will give you better solutions.&lt;br /&gt;
=== I get a blank page when launching the app ===&lt;br /&gt;
This error can happen when the application is launched properly but there is a runtime error. If you get a network connection error, this may be the expected behaviour. Wait until you see &amp;quot;Compiled successfully&amp;quot; in the console and reload.&lt;br /&gt;
&lt;br /&gt;
If that&#039;s not the case and you really are getting a blank page, look at the console and you should see some error indicating why the application is not rendering properly. Keep in mind that at the moment the application is already logging some errors and warnings, so make sure that you&#039;re not stuck trying to fix something that isn&#039;t causing the problem. You can follow any updates about this on MOBILE-3854.&lt;br /&gt;
&lt;br /&gt;
If you are not seeing any relevant logs, check out what to do on the [[#General_advice|General advice]] section.&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
To get more debug output from npm commands, see [https://docs.npmjs.com/cli/v7/using-npm/config the available configuration flags]. In particular try adding &amp;lt;code&amp;gt;--loglevel verbose&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;--loglevel info&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;--loglevel silly&amp;lt;/code&amp;gt; to the command-line.&lt;br /&gt;
=== I can&#039;t change the language ===&lt;br /&gt;
If you&#039;re getting a network error for a url like &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;http://localhost:8100/assets/lang/es.json&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;, this probably means that you haven&#039;t installed the language packs.&lt;br /&gt;
&lt;br /&gt;
Currently, you can install them in your machine running the &amp;lt;code&amp;gt;scripts/update_lang.sh&amp;lt;/code&amp;gt; script, but it may not work in your system if you don&#039;t have php installed. For future improvements, you can subscribe to MOBILE-3864.&lt;br /&gt;
=== Error: libsass bindings not found. Try reinstalling node-sass? ===&lt;br /&gt;
Most of the time, running the following command will fix the problem:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm rebuild node-sass&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== com.android.dex.DexException: Multiple dex files define XXX ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
configurations {&lt;br /&gt;
    all*.exclude group: &#039;com.android.support&#039;, module: &#039;support-v4&#039;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Could not resolve all dependencies for configuration &#039;:_debugCompile&#039;. ===&lt;br /&gt;
Open the Android SDK Manager and make sure you have installed: Android Support Repository, Android Support Library, Google Play Services and Google Repository.&lt;br /&gt;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
configurations.all {&lt;br /&gt;
    resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
android {&lt;br /&gt;
   compileSdkVersion 26&lt;br /&gt;
   buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK. ===&lt;br /&gt;
Download [https://developer.android.com/studio/ Android Studio] and copy the folder &amp;lt;code&amp;gt;android-studio/plugins/android/lib/templates&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;android-sdk-folder/Sdk/tools&amp;lt;/code&amp;gt;.&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and configure like this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
allprojects {&lt;br /&gt;
    repositories {&lt;br /&gt;
        jcenter()&lt;br /&gt;
        maven {&lt;br /&gt;
            url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;code&amp;gt;sudo apt-get install build-essential&amp;lt;/code&amp;gt; and retry.&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
If you see this error during &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt;, run &amp;lt;code&amp;gt;mkdir www&amp;lt;/code&amp;gt; and retry.&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &amp;lt;code&amp;gt;natives&amp;lt;/code&amp;gt; prior to 1.1.6. It can be fixed by running &amp;lt;code&amp;gt;npm install natives@1.1.6&amp;lt;/code&amp;gt;.&lt;br /&gt;
=== npm update check failed ===&lt;br /&gt;
You may get the following error on Windows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The suggested command does not work on Windows, so the solution is to manually check the ownership of all the files in &amp;lt;code&amp;gt;C:\Users\{username}\.config\configstore&amp;lt;/code&amp;gt;. In some cases, it can be &amp;lt;code&amp;gt;update-notifier-npm.json&amp;lt;/code&amp;gt; that got changed to be owned by Administrator.&lt;br /&gt;
=== Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ... ===&lt;br /&gt;
This is a common issue for Cygwin user running Node. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle App development.&lt;br /&gt;
&lt;br /&gt;
You don&#039;t need your Cygwin bin folder on the Windows path because it automatically gets added to the path when you lauch Cygwin bash.&lt;br /&gt;
=== The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically ===&lt;br /&gt;
This happens when you create the iOS platform with a certain &amp;lt;name&amp;gt; and then you change that name in config.xml. The solution seems to be removing and adding the iOS platform again:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx ionic platform remove ios&lt;br /&gt;
npx ionic platform add ios&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
===Failed to install &#039;cordova-plugin-x&#039; ===&lt;br /&gt;
Sometimes, you may see an error message similar to this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-x@x.x.x&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-x@&amp;gt;=x.x.x&amp;quot;.&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This can happen when a cordova plugin (let&#039;s call it &amp;quot;X&amp;quot;) is installed with an incorrect version. You can find this by removing the plugin and adding it again with the correct version:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx cordova plugin remove cordova-plugin-x&lt;br /&gt;
npx cordova plugin add cordova-plugin-x@x.x.x # Make sure to use the proper version here&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Please notice that if there is any plugin installed that depends on &amp;lt;code&amp;gt;cordova-plugin-x&amp;lt;/code&amp;gt; you&#039;ll have to remove and re-add them too.&lt;br /&gt;
=== doc.find is not a function ===&lt;br /&gt;
This happens in some environments, the solution is to run &amp;lt;code&amp;gt;npx cordova platform add ios&amp;lt;/code&amp;gt; before running the failing command. You should do this in a clean environment, once you&#039;ve seen the error running the command may not work. Try deleting &amp;lt;code&amp;gt;www/&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; before trying again.&lt;br /&gt;
&lt;br /&gt;
[https://stackoverflow.com/questions/47404622/edit-config-for-ios-usage-descriptions-doc-find-is-not-a-function Find more about this in StackOverflow]&lt;br /&gt;
===Mac: linker code failed with exit code 1===&lt;br /&gt;
If you get this error when trying to build the Moodle app with XCode, some dependencies might not have installed correctly.&lt;br /&gt;
&lt;br /&gt;
Ensure you have followed the [#Mac_only:_Push_notifications Mac only: Push notifications] steps above (particularly opening the .xcworkspace file rather than the .xcodeproj file). Then run the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd platforms/ios&lt;br /&gt;
pod install&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Now try running the build again in XCode.&lt;br /&gt;
===Windows: &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; hangs after &amp;quot;Starting &#039;watch&#039;&amp;quot;===&lt;br /&gt;
Try running the commands generated by &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; directly in bash:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 npx ng serve &amp;quot;--host=0.0.0.0&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Moodle App Coding Style]]&lt;br /&gt;
* [[Debugging network requests in the Moodle_App]]&lt;br /&gt;
* [https://github.com/moodlehq/moodle-docker Moodle Docker]&lt;br /&gt;
* [http://ionicframework.com/docs/cli/ Ionic CLI docs]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=61652</id>
		<title>Setting up your development environment for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Setting_up_your_development_environment_for_the_Moodle_App&amp;diff=61652"/>
		<updated>2022-01-26T14:21:49Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Running the app in a browser */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle App (Ionic 5)}}&lt;br /&gt;
The structure of this page is the following:&lt;br /&gt;
* The first part, up to the point where you get the &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; command to work, includes the basics of what you need to work on the app.&lt;br /&gt;
* The second part indicates how to build the app to run it on a native device (or emulator).&lt;br /&gt;
* The third part includes a list of troubleshooting advice. If you encounter a problem that is not already listed, please consider adding it.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Most of your development can happen on a browser, you only need to use an emulator if you have to work on native functionality.&lt;br /&gt;
&lt;br /&gt;
If you are just [[Moodle App Plugins Development Guide|adding mobile support to plugins]], you probably don&#039;t need to build the app yourself and you can skip reading this page.&lt;br /&gt;
== Requirements ==&lt;br /&gt;
===Install a browser for development===&lt;br /&gt;
Most of the time we recommend that you use a browser for development; the app will work in any Chromium-based browser. We recommend using the Chromium browser (an open source alternative to Google Chrome). You can get it from the [https://www.chromium.org/getting-involved/download-chromium official download page].&lt;br /&gt;
&lt;br /&gt;
To learn more about using a browser for development, and why it needs to be Chromium-based, read the [[Using the Moodle App in a browser]] page.&lt;br /&gt;
===Install git===&lt;br /&gt;
You will need to install Git in order to get the source code and upload your changes. If you are not familiar with it, we recommend that you get started reading the following guide: [https://git-scm.com/book/en/v2/Getting-Started-Installing-Git Installing Git].&lt;br /&gt;
===Install Node.js and Npm===&lt;br /&gt;
We recommend using a version manager like [https://github.com/nvm-sh/nvm nvm] to make this easier, you can prepare the correct environment running &amp;lt;code&amp;gt;nvm install&amp;lt;/code&amp;gt; in the project root. Remember to run this every time you work with the app, or if you’re not working on any other node projects in your computer you can run &amp;lt;code&amp;gt;nvm alias default `node -v`&amp;lt;/code&amp;gt; to make it the default. &lt;br /&gt;
&lt;br /&gt;
It may seem simpler and easier to install directly from [http://nodejs.org nodejs.org], but actually it is more tricky to get that to work. If you have previously installed Node directly, and want to switch to nvm, you need to uninstall node completely before installing nvm - or search for trouble-shooting instructions online.&lt;br /&gt;
===Install native SDKs===&lt;br /&gt;
If you intend to run the application in a native device, you will need to install that platform&#039;s native SDKs. You can learn how to set up your environment by reading Ionic’s documentation for [https://ionicframework.com/docs/developing/android Android] and [https://ionicframework.com/docs/developing/ios iOS].&lt;br /&gt;
===Windows only: Native build dependencies===&lt;br /&gt;
&amp;lt;code&amp;gt;node-gyp&amp;lt;/code&amp;gt; requires native build tools for your platform. If you&#039;re developing on Mac or Linux, you&#039;ll probably have these already ([https://github.com/nodejs/node-gyp/blob/master/README.md refer to the docs if you don&#039;t]). On Windows, run the following command as administrator (in cmd or Powershell):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm install --global --production windows-build-tools&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Warning! This installer can take a very, very long time to run. We were seeing it take hours. Literally. Be prepared to be very patient. Don&#039;t just make the natural assumption that it has crashed.&lt;br /&gt;
===Mac only: Push notifications===&lt;br /&gt;
This is only be necessary if you intend to compile the native iOS application. The push notifications plugin requires CocoaPods to work on a Mac; you can find the complete installation instructions in [https://cocoapods.org/ the official documentation], but it should work by running the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo gem install cocoapods&lt;br /&gt;
pod setup&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Please note that for compiling the app in Mac you need to open the &#039;&#039;&#039;Moodle.xcworkspace&#039;&#039;&#039; file, more information here: MOBILE-1970.&lt;br /&gt;
===Linux only: libsecret===&lt;br /&gt;
If you are using [[Moodle App scripts: gulp push|the gulp push script]], you need to have &amp;lt;code&amp;gt;libsecret&amp;lt;/code&amp;gt; installed before running &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt;. Depending on your distribution, you will need to run one of the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Debian/Ubuntu&lt;br /&gt;
sudo apt-get install libsecret-1-dev&lt;br /&gt;
&lt;br /&gt;
# Red Hat&lt;br /&gt;
sudo yum install libsecret-devel&lt;br /&gt;
&lt;br /&gt;
# Arch Linux&lt;br /&gt;
sudo pacman -S libsecret&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
== Running the app in a browser ==&lt;br /&gt;
You can obtain a copy of the source code by cloning the public repository. If you want to work on the latest development version, you should check out the &amp;lt;code&amp;gt;integration&amp;lt;/code&amp;gt; branch:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
git clone git@github.com:moodlehq/moodleapp.git&lt;br /&gt;
cd moodleapp&lt;br /&gt;
git checkout integration&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Once you have the correct environment set up, you can run the application with the following two commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will launch the application in a browser and you should be ready to start coding (you may also want to [[Using the Moodle App in a browser#Configuring_the_default_browser|configure the default browser]] for future runs). This compiles the entire application and can take a while, so don&#039;t worry if it doesn&#039;t open the browser instantly. Keep in mind that this command may open the browser before the dev server is ready, and you could get a network error. If that happens, just wait until the dev server is ready and launch the application again. It should be ready when you see a &amp;quot;Compiled successfully&amp;quot; message in the console. &lt;br /&gt;
&lt;br /&gt;
Congratulations, you have just completed the basics to become a Moodle App developer!&lt;br /&gt;
&lt;br /&gt;
If you need to work with native features or build packaged versions of the app, you can read the rest of this page.&lt;br /&gt;
&lt;br /&gt;
=== Windows &#039;Waiting for connectivity with NPM&#039; problem ===&lt;br /&gt;
&lt;br /&gt;
If you follow the above procedure on a Windows system (may depend on installed versions etc.) and get repeated &#039;Waiting for connectivity with NPM&#039; like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$ npm start&lt;br /&gt;
&lt;br /&gt;
&amp;gt; moodlemobile@3.9.5 start C:\Users\sm449\workspace\moodlemobile2&lt;br /&gt;
&amp;gt; ionic serve&lt;br /&gt;
&lt;br /&gt;
&amp;gt; npm.cmd run ionic:serve:before&lt;br /&gt;
&lt;br /&gt;
&amp;gt; moodlemobile@3.9.5 ionic:serve:before C:\Users\sm449\workspace\moodlemobile2&lt;br /&gt;
&amp;gt; gulp&lt;br /&gt;
&lt;br /&gt;
[13:59:24] Using gulpfile ~\workspace\moodlemobile2\gulpfile.js&lt;br /&gt;
[13:59:24] Starting &#039;default&#039;...&lt;br /&gt;
[13:59:24] Starting &#039;lang&#039;...&lt;br /&gt;
[13:59:24] Starting &#039;env&#039;...&lt;br /&gt;
[13:59:25] Finished &#039;env&#039; after 802 ms&lt;br /&gt;
[13:59:27] Finished &#039;lang&#039; after 3.4 s&lt;br /&gt;
[13:59:27] Finished &#039;default&#039; after 3.4 s&lt;br /&gt;
&amp;gt; npm.cmd run ionic:serve -- --host=localhost --port=8100 --project=app&lt;br /&gt;
[npm] &amp;gt; moodlemobile@3.9.5 ionic:serve C:\Users\sm449\workspace\moodlemobile2&lt;br /&gt;
[npm] &amp;gt; gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 ng serve &amp;quot;--host=localhost&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[npm] [13:59:46] Using gulpfile ~\workspace\moodlemobile2\gulpfile.js&lt;br /&gt;
[npm] [13:59:46] Starting &#039;watch&#039;...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
[INFO] Waiting for connectivity with npm...&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can resolve the problem (sort of) by pressing ctrl-c to get out of it, then rerun the last command displayed, but this time with &#039;npx&#039; before each of the 2 commands. (This assumes you are using a bash shell.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 npx ng serve &amp;quot;--host=localhost&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There will be a pause (a few minutes) while building everything. It should finish with the lines:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;&lt;br /&gt;
** Angular Live Development Server is listening on localhost:8100, open your browser on http://localhost:8100/ **&lt;br /&gt;
: Compiled successfully.&lt;br /&gt;
&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you can access it by running Chrome and connecting to localhost:8100.&lt;br /&gt;
&lt;br /&gt;
== Running the app in Android and iOS ==&lt;br /&gt;
The first time you want to run the application in a native device, this process will take a bit longer than usual because it needs to create the native projects and install native plugins. These will be created under the &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; folders respectively. If you ever run into issues during this process, try deleting both of these folders to get a clean start.&lt;br /&gt;
&lt;br /&gt;
In order to run the application on a native device, you can use one of the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm run dev:android  # Uses Live Reload, read below&lt;br /&gt;
npm run dev:ios      # Does NOT use Live Reload, read below&lt;br /&gt;
npm run prod:android # Uses AOT compilation, read below&lt;br /&gt;
npm run prod:ios     # Uses AOT compilation, read below&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If you get any errors while building, please see the [[#Troubleshooting|Troubleshooting]] section below.&lt;br /&gt;
=== Developing using Live Reload ===&lt;br /&gt;
Most of the time, it is recommended to develop using the &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; command. Working in a browser is faster, easier, and doesn&#039;t have as much overhead. However, in some situations you may want to run the application in a native device. If you want a similar development experience, you&#039;ll want to enable [https://ionicframework.com/docs/cli/livereload Live Reload].&lt;br /&gt;
&lt;br /&gt;
In Android, you can use the &amp;lt;code&amp;gt;npm run dev:android&amp;lt;/code&amp;gt; command and the application will launch on a device or emulator. Whenever you make any changes to your code, the application will reload automatically. Keep in mind that this command may launch the application before the dev server is ready, and you could get a network error. If that happens, just wait until the dev server is ready and launch the application again (you should see &amp;quot;Compiled successfully&amp;quot; in the console).&lt;br /&gt;
&lt;br /&gt;
In iOS, there are some limitations using live reload and that&#039;s why the &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; command doesn&#039;t enable it by default. You can still use it by running &amp;lt;code&amp;gt;npx ionic cordova run ios --livereload --external&amp;lt;/code&amp;gt;, but keep in mind that this will serve your application on an IP and will expose it to anyone connected to the same Wi-Fi network. Other than privacy concerns, this may cause some problems if you are working with iframes or local files. If you have any problems with that, you&#039;ll have to use the &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; command to recompile the application every time you change the code.&lt;br /&gt;
=== Compiling using AOT ===&lt;br /&gt;
Angular has 2 ways of compiling: [https://angular.io/guide/glossary#jit JIT] and [https://angular.io/guide/glossary#aot AOT].&lt;br /&gt;
&lt;br /&gt;
Running &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;npm run dev:android&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;npm run dev:ios&amp;lt;/code&amp;gt; compiles using JIT, which is faster to compile but the app takes longer to start. This is acceptable during development because it allows you to use Live Reload.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;npm run prod:android&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;npm run prod:ios&amp;lt;/code&amp;gt; commands use AOT compilation because they generate production bundles.&lt;br /&gt;
=== Using Android emulators ===&lt;br /&gt;
Most of the time, you should be using an emulator running recent versions of Android, and it should work fine. But sometimes, you may want to use an older version to test a specific behaviour.&lt;br /&gt;
&lt;br /&gt;
If you want to run the application in an Android 5 emulator, you’ll need to upgrade the system webview because emulators come with version 37 preinstalled. Your first idea may be to upgrade the webview using the Google Play store, but it will not work because the webview served by Google Play is &amp;lt;code&amp;gt;com.google.android.webview&amp;lt;/code&amp;gt; whilst the system webview used in emulators is &amp;lt;code&amp;gt;com.android.webview&amp;lt;/code&amp;gt;. You can do the following instead.&lt;br /&gt;
&lt;br /&gt;
Once you have [https://developer.android.com/studio/run/managing-avds created your Android 5 virtual device], you’ll need to do download [https://android.googlesource.com/platform/external/chromium-webview/+/refs/heads/oreo-m3-release/prebuilt/x86_64/ the apk for Webview 61] and run the following commands:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Open the folder where the “emulator” script is installed&lt;br /&gt;
cd $(dirname `which emulator`)&lt;br /&gt;
&lt;br /&gt;
# Boot the emulator in write mode&lt;br /&gt;
# (you can get a list of device names running “emulator -list-avds”)&lt;br /&gt;
emulator @DeviceName -writable-system&lt;br /&gt;
&lt;br /&gt;
# In a different shell, make /system writable&lt;br /&gt;
adb remount&lt;br /&gt;
&lt;br /&gt;
# Uninstall the webview app manually and reboot the device&lt;br /&gt;
adb shell&lt;br /&gt;
rm -rf /data/data/com.android.webview&lt;br /&gt;
rm -rf /system/app/webview&lt;br /&gt;
reboot&lt;br /&gt;
&lt;br /&gt;
# Install the new version&lt;br /&gt;
adb install webview.apk&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
After doing this, remember to run the emulator in write mode for subsequent sessions, but you don’t need to call the &amp;lt;code&amp;gt;remount&amp;lt;/code&amp;gt; command every time.&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
This section contains a list of common errors that have been found and how to solve them. However, keep in mind that these may be platform-dependent and could not fix the problem in your machine, even if you are seeing the same error message.&lt;br /&gt;
=== General advice ===&lt;br /&gt;
If you are stuck with an error and you can&#039;t find a way to continue, here&#039;s a list of things you can do:&lt;br /&gt;
* Using git, look at the changes you have in your working directory and make sure that they aren&#039;t causing the problem. Be specially careful with changes in &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt;. You can see a list of the files you have modified running &amp;lt;code&amp;gt;git status&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Make sure that you are using the proper node and npm versions. You can see it looking at the &amp;lt;code&amp;gt;engines&amp;lt;/code&amp;gt; key in &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt;. If you are using [https://github.com/nvm-sh/nvm nvm], just run &amp;lt;code&amp;gt;nvm install&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Make sure that all dependencies have been installed properly. To be extra sure, run &amp;lt;code&amp;gt;npm ci&amp;lt;/code&amp;gt;; this will remove the &amp;lt;code&amp;gt;node_modules/&amp;lt;/code&amp;gt; folder and install all dependencies again exactly as described in your &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If you are having issues trying to build for Android or iOS, try removing the &amp;lt;code&amp;gt;www/&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; folders and try again.&lt;br /&gt;
* If you are using a development version, maybe the repository is broken and it&#039;s not your fault. Try checking out the &amp;lt;code&amp;gt;master&amp;lt;/code&amp;gt; branch and see if you&#039;re getting the same error.&lt;br /&gt;
* Try cloning the repository in a new folder and run through the instructions in this page again. If you can, try doing it on a different computer to make sure that you&#039;re doing everything properly and it&#039;s not a problem in your machine.&lt;br /&gt;
* Try creating [https://ionicframework.com/docs/cli/commands/start a blank Ionic application] and see if you&#039;re having the same problems. Make sure that you are using the same version of the main dependencies (Angular, Cordova, Ionic CLI, etc.).&lt;br /&gt;
* If you are searching for help online, maybe your problem has nothing to do with the Moodle App in particular an it&#039;s related with Ionic, Cordova, Angular, etc. Searching using the proper context will give you better solutions.&lt;br /&gt;
=== I get a blank page when launching the app ===&lt;br /&gt;
This error can happen when the application is launched properly but there is a runtime error. If you get a network connection error, this may be the expected behaviour. Wait until you see &amp;quot;Compiled successfully&amp;quot; in the console and reload.&lt;br /&gt;
&lt;br /&gt;
If that&#039;s not the case and you really are getting a blank page, look at the console and you should see some error indicating why the application is not rendering properly. Keep in mind that at the moment the application is already logging some errors and warnings, so make sure that you&#039;re not stuck trying to fix something that isn&#039;t causing the problem. You can follow any updates about this on MOBILE-3854.&lt;br /&gt;
&lt;br /&gt;
If you are not seeing any relevant logs, check out what to do on the [[#General_advice|General advice]] section.&lt;br /&gt;
=== Strange NPM errors ===&lt;br /&gt;
To get more debug output from npm commands, see [https://docs.npmjs.com/cli/v7/using-npm/config the available configuration flags]. In particular try adding &amp;lt;code&amp;gt;--loglevel verbose&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;--loglevel info&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;--loglevel silly&amp;lt;/code&amp;gt; to the command-line.&lt;br /&gt;
=== I can&#039;t change the language ===&lt;br /&gt;
If you&#039;re getting a network error for a url like &amp;lt;code&amp;gt;&amp;lt;nowiki&amp;gt;http://localhost:8100/assets/lang/es.json&amp;lt;/nowiki&amp;gt;&amp;lt;/code&amp;gt;, this probably means that you haven&#039;t installed the language packs.&lt;br /&gt;
&lt;br /&gt;
Currently, you can install them in your machine running the &amp;lt;code&amp;gt;scripts/update_lang.sh&amp;lt;/code&amp;gt; script, but it may not work in your system if you don&#039;t have php installed. For future improvements, you can subscribe to MOBILE-3864.&lt;br /&gt;
=== Error: libsass bindings not found. Try reinstalling node-sass? ===&lt;br /&gt;
Most of the time, running the following command will fix the problem:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npm rebuild node-sass&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== com.android.dex.DexException: Multiple dex files define XXX ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
configurations {&lt;br /&gt;
    all*.exclude group: &#039;com.android.support&#039;, module: &#039;support-v4&#039;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Could not resolve all dependencies for configuration &#039;:_debugCompile&#039;. ===&lt;br /&gt;
Open the Android SDK Manager and make sure you have installed: Android Support Repository, Android Support Library, Google Play Services and Google Repository.&lt;br /&gt;
=== Could not find com.android.support:support-v4:XXX ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
configurations.all {&lt;br /&gt;
    resolutionStrategy.force &#039;com.android.support:support-v4:24.0.0&#039;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== ERROR: In &amp;lt;declare-styleable&amp;gt; FontFamilyFont, unable to find attribute android:font ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and add this code at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
android {&lt;br /&gt;
   compileSdkVersion 26&lt;br /&gt;
   buildToolsVersion &amp;quot;26.0.1&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Error: Could not find gradle wrapper within Android SDK. Might need to update your Android SDK. ===&lt;br /&gt;
Download [https://developer.android.com/studio/ Android Studio] and copy the folder &amp;lt;code&amp;gt;android-studio/plugins/android/lib/templates&amp;lt;/code&amp;gt; into &amp;lt;code&amp;gt;android-sdk-folder/Sdk/tools&amp;lt;/code&amp;gt;.&lt;br /&gt;
=== Could not find com.android.support:support-v4:27.1.0 ===&lt;br /&gt;
Open the file &amp;lt;code&amp;gt;platforms/android/build.gradle&amp;lt;/code&amp;gt; and configure like this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;groovy&amp;quot;&amp;gt;&lt;br /&gt;
allprojects {&lt;br /&gt;
    repositories {&lt;br /&gt;
        jcenter()&lt;br /&gt;
        maven {&lt;br /&gt;
            url &amp;quot;https://maven.google.com&amp;quot;&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
=== Error: not found: make ===&lt;br /&gt;
If you see this error in Ubuntu, run &amp;lt;code&amp;gt;sudo apt-get install build-essential&amp;lt;/code&amp;gt; and retry.&lt;br /&gt;
=== Current working directory is not a Cordova-based project. ===&lt;br /&gt;
If you see this error during &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt;, run &amp;lt;code&amp;gt;mkdir www&amp;lt;/code&amp;gt; and retry.&lt;br /&gt;
=== ReferenceError: internalBinding is not defined ===&lt;br /&gt;
This [https://stackoverflow.com/questions/53146394/node-app-fails-to-run-on-mojave-referenceerror-internalbinding-is-not-defined seems to be] an error with &amp;lt;code&amp;gt;natives&amp;lt;/code&amp;gt; prior to 1.1.6. It can be fixed by running &amp;lt;code&amp;gt;npm install natives@1.1.6&amp;lt;/code&amp;gt;.&lt;br /&gt;
=== npm update check failed ===&lt;br /&gt;
You may get the following error on Windows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
 │                   npm update check failed                   │&lt;br /&gt;
 │             Try running with sudo or get access             │ &lt;br /&gt;
 │            to the local update config store via             │&lt;br /&gt;
 │ sudo chown -R $USER:$(id -gn $USER) C:\Users\username\.config │&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The suggested command does not work on Windows, so the solution is to manually check the ownership of all the files in &amp;lt;code&amp;gt;C:\Users\{username}\.config\configstore&amp;lt;/code&amp;gt;. In some cases, it can be &amp;lt;code&amp;gt;update-notifier-npm.json&amp;lt;/code&amp;gt; that got changed to be owned by Administrator.&lt;br /&gt;
=== Unhandled rejection Error: Command failed: C:\cygwin64\bin\git.EXE ... ===&lt;br /&gt;
This is a common issue for Cygwin user running Node. However, you just need to ensure that Msysgit is on your windows path and that the cygwin bin folder is not. Then always use another shell like Powershell for your Moodle App development.&lt;br /&gt;
&lt;br /&gt;
You don&#039;t need your Cygwin bin folder on the Windows path because it automatically gets added to the path when you lauch Cygwin bash.&lt;br /&gt;
=== The product name change (&amp;lt;name&amp;gt; tag) in config.xml is not supported dynamically ===&lt;br /&gt;
This happens when you create the iOS platform with a certain &amp;lt;name&amp;gt; and then you change that name in config.xml. The solution seems to be removing and adding the iOS platform again:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx ionic platform remove ios&lt;br /&gt;
npx ionic platform add ios&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
===Failed to install &#039;cordova-plugin-x&#039; ===&lt;br /&gt;
Sometimes, you may see an error message similar to this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
CordovaError: Version of installed plugin: &amp;quot;cordova-plugin-x@x.x.x&amp;quot; does not satisfy dependency plugin requirement &amp;quot;cordova-plugin-x@&amp;gt;=x.x.x&amp;quot;.&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This can happen when a cordova plugin (let&#039;s call it &amp;quot;X&amp;quot;) is installed with an incorrect version. You can find this by removing the plugin and adding it again with the correct version:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx cordova plugin remove cordova-plugin-x&lt;br /&gt;
npx cordova plugin add cordova-plugin-x@x.x.x # Make sure to use the proper version here&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Please notice that if there is any plugin installed that depends on &amp;lt;code&amp;gt;cordova-plugin-x&amp;lt;/code&amp;gt; you&#039;ll have to remove and re-add them too.&lt;br /&gt;
=== doc.find is not a function ===&lt;br /&gt;
This happens in some environments, the solution is to run &amp;lt;code&amp;gt;npx cordova platform add ios&amp;lt;/code&amp;gt; before running the failing command. You should do this in a clean environment, once you&#039;ve seen the error running the command may not work. Try deleting &amp;lt;code&amp;gt;www/&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;platforms/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;plugins/&amp;lt;/code&amp;gt; before trying again.&lt;br /&gt;
&lt;br /&gt;
[https://stackoverflow.com/questions/47404622/edit-config-for-ios-usage-descriptions-doc-find-is-not-a-function Find more about this in StackOverflow]&lt;br /&gt;
===Mac: linker code failed with exit code 1===&lt;br /&gt;
If you get this error when trying to build the Moodle app with XCode, some dependencies might not have installed correctly.&lt;br /&gt;
&lt;br /&gt;
Ensure you have followed the [#Mac_only:_Push_notifications Mac only: Push notifications] steps above (particularly opening the .xcworkspace file rather than the .xcodeproj file). Then run the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd platforms/ios&lt;br /&gt;
pod install&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Now try running the build again in XCode.&lt;br /&gt;
===Windows: &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; hangs after &amp;quot;Starting &#039;watch&#039;&amp;quot;===&lt;br /&gt;
Try running the commands generated by &amp;lt;code&amp;gt;npm start&amp;lt;/code&amp;gt; directly in bash:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
npx gulp watch &amp;amp; NODE_OPTIONS=--max-old-space-size=4096 npx ng serve &amp;quot;--host=0.0.0.0&amp;quot; &amp;quot;--port=8100&amp;quot; &amp;quot;--project=app&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Moodle App Coding Style]]&lt;br /&gt;
* [[Debugging network requests in the Moodle_App]]&lt;br /&gt;
* [https://github.com/moodlehq/moodle-docker Moodle Docker]&lt;br /&gt;
* [http://ionicframework.com/docs/cli/ Ionic CLI docs]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58612</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58612"/>
		<updated>2021-03-29T12:40:31Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
Code relating to display of user fields is in the class &amp;lt;tt&amp;gt;\core_user\fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/user/classes/fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* See also [[User-related APIs]], which has information about defining custom user fields.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This class is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This class supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table columns.&lt;br /&gt;
&lt;br /&gt;
Because the capability for the current user depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove arbitrary extra fields ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this would usually be because you know you are going to list that field in a database query separately. It is OK to exclude a field whether or not it is actually included; that way the code will continue to work whatever the field list.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is a basic SQL query that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, [], 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core_user\fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can create SQL snippets for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted {$sql-&amp;gt;selects}&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
               {$sql-&amp;gt;joins}&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core_user\fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58611</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58611"/>
		<updated>2021-03-29T12:40:14Z</updated>

		<summary type="html">&lt;p&gt;Quen: Updated for new location of class&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
Code relating to display of user fields is in the class &amp;lt;tt&amp;gt;\core_user\fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* See also [[User-related APIs]], which has information about defining custom user fields.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This class is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This class supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table columns.&lt;br /&gt;
&lt;br /&gt;
Because the capability for the current user depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove arbitrary extra fields ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this would usually be because you know you are going to list that field in a database query separately. It is OK to exclude a field whether or not it is actually included; that way the code will continue to work whatever the field list.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core_user\fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is a basic SQL query that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, [], 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core_user\fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can create SQL snippets for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted {$sql-&amp;gt;selects}&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
               {$sql-&amp;gt;joins}&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core_user\fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User-related_APIs&amp;diff=58489</id>
		<title>User-related APIs</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User-related_APIs&amp;diff=58489"/>
		<updated>2021-03-11T15:30:29Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
&lt;br /&gt;
This is a collection of miscellaneous APIs that can help with doing things with lists of users. Note that, in many cases, the more specific [[Access API]], [[Groups API]], [[Enrolment API]], etc. may be what you need.&lt;br /&gt;
&lt;br /&gt;
== User field display ==&lt;br /&gt;
&lt;br /&gt;
The [[User fields]] class is mainly used when displaying tables of data about users. It indicates which extra fields (e.g. email) should be displayed in the current context based on the permissions of the current user. It also provides ways to get the necessary data from a query, and to obtainother generally-useful fields for user names and pictures.&lt;br /&gt;
&lt;br /&gt;
== User fields definition ==&lt;br /&gt;
&lt;br /&gt;
{{Moodle 3.1}}&lt;br /&gt;
To guarantee the sanity of the data inserted into Moodle and avoid security bugs, new user fields definition methods have been created for use in Moodle 3.1 onwards. The purpose of these new methods is to create a central point of user fields data validation and guarantee all data inserted into Moodle will be cleaned against the correct parameter type. Another goal of  this new API is to create consistency across Moodle core and avoid different parameter validations for the same user fields. For now on, user data must validate against the user field, not using clean_param() directly.&lt;br /&gt;
&lt;br /&gt;
===$propertiescache===&lt;br /&gt;
Cached information of each user field and its attributes filled by the fill_properties_cache.&lt;br /&gt;
&lt;br /&gt;
===fill_properties_cache()===&lt;br /&gt;
The main method of the user definition is to keep the definition of each user field and its properties. It verifies if the &#039;&#039;&#039;core_user::$propertiescache&#039;&#039;&#039; is already filled and caches all user fields attributes into this same attribute.&lt;br /&gt;
Each field matches the exact field name on the user table. That said, every new field added to the user table should be added to fill_properties_cache $fields array, otherwise it won&#039;t be validated or cleaned.&lt;br /&gt;
Each field has four possible properties, being choices and default optional:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;null&#039;&#039;&#039; - Whether the field is NULL or NOT_NULL, it SHOULD NOT be used as form validation, as many fields in the user table have NOT_NULL property but have the default value as (``).&lt;br /&gt;
* &#039;&#039;&#039;type&#039;&#039;&#039; - The expected parameter type (PARAM_*) to be used as validation and sanitizing.&lt;br /&gt;
* &#039;&#039;&#039;choices&#039;&#039;&#039; - A list of accepted values of that field. For example the list of the available countries, timezones, calendar type etc.&lt;br /&gt;
* &#039;&#039;&#039;default&#039;&#039;&#039; - The default value in case the user field didn&#039;t pass the validation or cleaned and we must set the default value. For example if the user country is invalid and it is not in the list of choices, set $CFG-&amp;gt;country.&lt;br /&gt;
&lt;br /&gt;
===validate()===&lt;br /&gt;
A static method to validate user fields, accepts an array or the user object as parameter, validate each parameter individually and can return true if all user data is correct or an array of validation errors. The purpose of this method is to just validate the user data, it won’t do any cleaning of the data.&lt;br /&gt;
&lt;br /&gt;
===clean_data()===&lt;br /&gt;
A static method that has the purpose of clean the user data and return the same user array/object. It receives an array with user data or a user object as parameter and it checks if the data is in the list of choices and if the property has a default value and clean the data if the user object doesn’t have a choices property.&lt;br /&gt;
It will display a debugging message if one the operations above has problems.&lt;br /&gt;
&lt;br /&gt;
===clean_field()===&lt;br /&gt;
A static method to clean a single user field. It has two parameters, the data to be cleaned and its user field. The behaviour of the method is similar to the clean_data. It will do the validations and cleaning and can display a debug message if an error has been found. It returns the cleaned data.&lt;br /&gt;
&lt;br /&gt;
===get_property_type()===&lt;br /&gt;
A helper method to get the type of the property. It receives the user field name as parameter and if it doesn&#039;t exist will throw an exception. If the property has been found, it will return its type.&lt;br /&gt;
&lt;br /&gt;
===get_property_null()===&lt;br /&gt;
A helper method to get the null property of the user field. It receives the user field name as parameter and if it doesn&#039;t exist it throws an exception. If the property has been found, it will return the null value.&lt;br /&gt;
&lt;br /&gt;
===get_property_choices()===&lt;br /&gt;
A helper method to get the list of choices of a user field. It receives the user field name as parameter and if it doesn&#039;t exist will throw an exception. If the property has been found, it will return the list of accepted values.&lt;br /&gt;
&lt;br /&gt;
===get_property_default()===&lt;br /&gt;
A helper method to get the default value of a property. It receives the user field name as parameter and if it doesn&#039;t exist or if it doesn&#039;t have a default attribute will throw an exception. If the property has been found, it will return its default value.&lt;br /&gt;
&lt;br /&gt;
== User selector ==&lt;br /&gt;
&lt;br /&gt;
The base class &amp;lt;tt&amp;gt;user_selector_base&amp;lt;/tt&amp;gt; defined in &amp;lt;tt&amp;gt;user/selector/lib.php&amp;lt;/tt&amp;gt;, which you can subclass to make a widget that lets you select users in an AJAX-y way. It is used, for example, on the Add group members page. The best way to learn how to use it is to search the code for other users, and see how they work. The base class also has good PHPdoc comments.&lt;br /&gt;
&lt;br /&gt;
== Sorting lists of users ==&lt;br /&gt;
&lt;br /&gt;
When you fetch a list of users from the database, they should always be sorted consistently, by using the &amp;lt;tt&amp;gt;users_order_by_sql&amp;lt;/tt&amp;gt; function to generate the order-by clause. Again, the best way to see how that works is to search the code for existing uses.&lt;br /&gt;
&lt;br /&gt;
== Others ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Please add more..&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
* [[Core APIs]]&lt;br /&gt;
* [[Access API]]&lt;br /&gt;
* [[Groups API]]&lt;br /&gt;
* [[Enrolment API]]&lt;br /&gt;
&lt;br /&gt;
[[Category:API]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58486</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58486"/>
		<updated>2021-03-11T12:54:52Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
Code relating to display of user fields is in the class &amp;lt;tt&amp;gt;\core\user_fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* See also [[User-related APIs]], which has information about defining custom user fields.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This class is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This class supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table columns.&lt;br /&gt;
&lt;br /&gt;
Because the capability for the current user depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove arbitrary extra fields ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this would usually be because you know you are going to list that field in a database query separately. It is OK to exclude a field whether or not it is actually included; that way the code will continue to work whatever the field list.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is a basic SQL query that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, [], 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core\user_fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can create SQL snippets for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted {$sql-&amp;gt;selects}&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
               {$sql-&amp;gt;joins}&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core\user_fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58485</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58485"/>
		<updated>2021-03-11T12:54:40Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
Code relating to display of user fields is in the class &amp;lt;tt&amp;gt;\core\user_fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* See also [[User-related APIs]] which has information about defining custom user fields.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This class is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This class supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table columns.&lt;br /&gt;
&lt;br /&gt;
Because the capability for the current user depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove arbitrary extra fields ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this would usually be because you know you are going to list that field in a database query separately. It is OK to exclude a field whether or not it is actually included; that way the code will continue to work whatever the field list.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is a basic SQL query that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, [], 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core\user_fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can create SQL snippets for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted {$sql-&amp;gt;selects}&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
               {$sql-&amp;gt;joins}&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core\user_fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58484</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58484"/>
		<updated>2021-03-11T12:47:06Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
Code relating to display of user fields is in the class &amp;lt;tt&amp;gt;\core\user_fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This class is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This class supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table columns.&lt;br /&gt;
&lt;br /&gt;
Because the capability for the current user depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove arbitrary extra fields ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this would usually be because you know you are going to list that field in a database query separately. It is OK to exclude a field whether or not it is actually included; that way the code will continue to work whatever the field list.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is a basic SQL query that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, [], 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core\user_fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can create SQL snippets for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted {$sql-&amp;gt;selects}&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
               {$sql-&amp;gt;joins}&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core\user_fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User-related_APIs&amp;diff=58483</id>
		<title>User-related APIs</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User-related_APIs&amp;diff=58483"/>
		<updated>2021-03-11T12:46:11Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
&lt;br /&gt;
This is a collection of miscellaneous APIs that can help with doing things with lists of users. Note that, in many cases, the more specific [[Access API]], [[Groups API]], [[Enrolment API]], etc. may be what you need.&lt;br /&gt;
&lt;br /&gt;
== User field display ==&lt;br /&gt;
&lt;br /&gt;
The [[User fields API]] is mainly used when displaying tables of data about users. It indicates which extra fields (e.g. email) should be displayed in the current context based on the permissions of the current user. It also provides ways to get the necessary data from a query, and to obtainother generally-useful fields for user names and pictures.&lt;br /&gt;
&lt;br /&gt;
== User fields definition ==&lt;br /&gt;
&lt;br /&gt;
{{Moodle 3.1}}&lt;br /&gt;
To guarantee the sanity of the data inserted into Moodle and avoid security bugs, new user fields definition methods have been created for use in Moodle 3.1 onwards. The purpose of these new methods is to create a central point of user fields data validation and guarantee all data inserted into Moodle will be cleaned against the correct parameter type. Another goal of  this new API is to create consistency across Moodle core and avoid different parameter validations for the same user fields. For now on, user data must validate against the user field, not using clean_param() directly.&lt;br /&gt;
&lt;br /&gt;
===$propertiescache===&lt;br /&gt;
Cached information of each user field and its attributes filled by the fill_properties_cache.&lt;br /&gt;
&lt;br /&gt;
===fill_properties_cache()===&lt;br /&gt;
The main method of the user definition is to keep the definition of each user field and its properties. It verifies if the &#039;&#039;&#039;core_user::$propertiescache&#039;&#039;&#039; is already filled and caches all user fields attributes into this same attribute.&lt;br /&gt;
Each field matches the exact field name on the user table. That said, every new field added to the user table should be added to fill_properties_cache $fields array, otherwise it won&#039;t be validated or cleaned.&lt;br /&gt;
Each field has four possible properties, being choices and default optional:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;null&#039;&#039;&#039; - Whether the field is NULL or NOT_NULL, it SHOULD NOT be used as form validation, as many fields in the user table have NOT_NULL property but have the default value as (``).&lt;br /&gt;
* &#039;&#039;&#039;type&#039;&#039;&#039; - The expected parameter type (PARAM_*) to be used as validation and sanitizing.&lt;br /&gt;
* &#039;&#039;&#039;choices&#039;&#039;&#039; - A list of accepted values of that field. For example the list of the available countries, timezones, calendar type etc.&lt;br /&gt;
* &#039;&#039;&#039;default&#039;&#039;&#039; - The default value in case the user field didn&#039;t pass the validation or cleaned and we must set the default value. For example if the user country is invalid and it is not in the list of choices, set $CFG-&amp;gt;country.&lt;br /&gt;
&lt;br /&gt;
===validate()===&lt;br /&gt;
A static method to validate user fields, accepts an array or the user object as parameter, validate each parameter individually and can return true if all user data is correct or an array of validation errors. The purpose of this method is to just validate the user data, it won’t do any cleaning of the data.&lt;br /&gt;
&lt;br /&gt;
===clean_data()===&lt;br /&gt;
A static method that has the purpose of clean the user data and return the same user array/object. It receives an array with user data or a user object as parameter and it checks if the data is in the list of choices and if the property has a default value and clean the data if the user object doesn’t have a choices property.&lt;br /&gt;
It will display a debugging message if one the operations above has problems.&lt;br /&gt;
&lt;br /&gt;
===clean_field()===&lt;br /&gt;
A static method to clean a single user field. It has two parameters, the data to be cleaned and its user field. The behaviour of the method is similar to the clean_data. It will do the validations and cleaning and can display a debug message if an error has been found. It returns the cleaned data.&lt;br /&gt;
&lt;br /&gt;
===get_property_type()===&lt;br /&gt;
A helper method to get the type of the property. It receives the user field name as parameter and if it doesn&#039;t exist will throw an exception. If the property has been found, it will return its type.&lt;br /&gt;
&lt;br /&gt;
===get_property_null()===&lt;br /&gt;
A helper method to get the null property of the user field. It receives the user field name as parameter and if it doesn&#039;t exist it throws an exception. If the property has been found, it will return the null value.&lt;br /&gt;
&lt;br /&gt;
===get_property_choices()===&lt;br /&gt;
A helper method to get the list of choices of a user field. It receives the user field name as parameter and if it doesn&#039;t exist will throw an exception. If the property has been found, it will return the list of accepted values.&lt;br /&gt;
&lt;br /&gt;
===get_property_default()===&lt;br /&gt;
A helper method to get the default value of a property. It receives the user field name as parameter and if it doesn&#039;t exist or if it doesn&#039;t have a default attribute will throw an exception. If the property has been found, it will return its default value.&lt;br /&gt;
&lt;br /&gt;
== User selector ==&lt;br /&gt;
&lt;br /&gt;
The base class &amp;lt;tt&amp;gt;user_selector_base&amp;lt;/tt&amp;gt; defined in &amp;lt;tt&amp;gt;user/selector/lib.php&amp;lt;/tt&amp;gt;, which you can subclass to make a widget that lets you select users in an AJAX-y way. It is used, for example, on the Add group members page. The best way to learn how to use it is to search the code for other users, and see how they work. The base class also has good PHPdoc comments.&lt;br /&gt;
&lt;br /&gt;
== Sorting lists of users ==&lt;br /&gt;
&lt;br /&gt;
When you fetch a list of users from the database, they should always be sorted consistently, by using the &amp;lt;tt&amp;gt;users_order_by_sql&amp;lt;/tt&amp;gt; function to generate the order-by clause. Again, the best way to see how that works is to search the code for existing uses.&lt;br /&gt;
&lt;br /&gt;
== Others ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Please add more..&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
* [[Core APIs]]&lt;br /&gt;
* [[Access API]]&lt;br /&gt;
* [[Groups API]]&lt;br /&gt;
* [[Enrolment API]]&lt;br /&gt;
&lt;br /&gt;
[[Category:API]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Core_APIs&amp;diff=58482</id>
		<title>Core APIs</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Core_APIs&amp;diff=58482"/>
		<updated>2021-03-11T12:45:40Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Moodle has a number of core APIs that provide tools for Moodle scripts.&lt;br /&gt;
&lt;br /&gt;
They are essential when writing [[Plugins|Moodle plugins]].&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Most-used General APIs==&lt;br /&gt;
&lt;br /&gt;
These APIs are critical and will be used by nearly every Moodle plugin.&lt;br /&gt;
&lt;br /&gt;
=== Access API (access) ===&lt;br /&gt;
&lt;br /&gt;
The [[Access API]] gives you functions so you can determine what the current user is allowed to do, and it allows modules to extend Moodle with new capabilities.&lt;br /&gt;
&lt;br /&gt;
=== Data manipulation API (dml) ===&lt;br /&gt;
&lt;br /&gt;
The [[Data manipulation API]] allows you to read/write to databases in a consistent and safe way.&lt;br /&gt;
&lt;br /&gt;
=== File API (files) ===&lt;br /&gt;
&lt;br /&gt;
The [[File API]] controls the storage of files in connection to various plugins.&lt;br /&gt;
&lt;br /&gt;
=== Form API (form) ===&lt;br /&gt;
&lt;br /&gt;
The [[Form API]] defines and handles user data via web forms.&lt;br /&gt;
&lt;br /&gt;
=== Logging API (log) ===&lt;br /&gt;
&lt;br /&gt;
The [[Events API]] allows you to log events in Moodle, while [[Logging 2]] describes how logs are stored and retrieved.&lt;br /&gt;
&lt;br /&gt;
=== Navigation API (navigation) ===&lt;br /&gt;
&lt;br /&gt;
The [[Navigation API]] allows you to manipulate the navigation tree to add and remove items as you wish.&lt;br /&gt;
&lt;br /&gt;
=== Page API (page) ===&lt;br /&gt;
&lt;br /&gt;
The [[Page API]] is used to set up the current page, add JavaScript, and configure how things will be displayed to the user.&lt;br /&gt;
&lt;br /&gt;
=== Output API (output) ===&lt;br /&gt;
&lt;br /&gt;
The [[Output API]] is used to render the HTML for all parts of the page.&lt;br /&gt;
&lt;br /&gt;
=== String API (string) ===&lt;br /&gt;
&lt;br /&gt;
The [[String API]] is how you get language text strings to use in the user interface.   It handles any language translations that might be available.&lt;br /&gt;
&lt;br /&gt;
=== Upgrade API (upgrade) ===&lt;br /&gt;
&lt;br /&gt;
The [[Upgrade API]] is how your module installs and upgrades itself, by keeping track of its own version.&lt;br /&gt;
&lt;br /&gt;
=== Moodlelib API (core) ===&lt;br /&gt;
&lt;br /&gt;
The [[Moodlelib API]] is the central library file of miscellaneous general-purpose Moodle functions. Functions can over the handling of request parameters, configs, user preferences, time, login, mnet, plugins, strings and others. There are plenty of defined constants too.&lt;br /&gt;
&lt;br /&gt;
==Other General APIs==&lt;br /&gt;
&lt;br /&gt;
=== Admin settings API (admin) ===&lt;br /&gt;
&lt;br /&gt;
The [[Admin settings]] API deals with providing configuration options for each plugin and Moodle core.&lt;br /&gt;
&lt;br /&gt;
=== Analytics API (analytics) ===&lt;br /&gt;
&lt;br /&gt;
The [[Analytics API]] allow you to create prediction models and generate insights.&lt;br /&gt;
&lt;br /&gt;
=== Availability API (availability) ===&lt;br /&gt;
&lt;br /&gt;
The [[Availability API]] controls access to activities and sections.&lt;br /&gt;
&lt;br /&gt;
=== Backup API (backup) ===&lt;br /&gt;
&lt;br /&gt;
The [[Backup API]] defines exactly how to convert course data into XML for backup purposes, and the [[Restore API]] describes how to convert it back the other way.&lt;br /&gt;
&lt;br /&gt;
=== Cache API (cache) ===&lt;br /&gt;
&lt;br /&gt;
The [[The Moodle Universal Cache (MUC)]] is the structure for storing cache data within Moodle. [[Cache_API]] explains some of what is needed to use a cache in your code.&lt;br /&gt;
&lt;br /&gt;
=== Calendar API (calendar) ===&lt;br /&gt;
&lt;br /&gt;
The [[Calendar API]] allows you to add and modify events in the calendar for user, groups, courses, or the whole site.&lt;br /&gt;
&lt;br /&gt;
=== Check API (check) ===&lt;br /&gt;
&lt;br /&gt;
The [[Check API]] allows you to add security, performance or health checks to your site.&lt;br /&gt;
&lt;br /&gt;
=== Comment API (comment) ===&lt;br /&gt;
&lt;br /&gt;
The [[Comment API]] allows you to save and retrieve user comments, so that you can easily add commenting to any of your code.&lt;br /&gt;
&lt;br /&gt;
=== Competency API (competency) ===&lt;br /&gt;
&lt;br /&gt;
The [[Competency API]] allows you to list and add evidence of competencies to learning plans, learning plan templates, frameworks, courses and activities.&lt;br /&gt;
&lt;br /&gt;
=== Data definition API (ddl) ===&lt;br /&gt;
&lt;br /&gt;
The [[Data definition API]] is what you use to create, change and delete tables and fields in the database during upgrades.&lt;br /&gt;
&lt;br /&gt;
=== Editor API ===&lt;br /&gt;
&lt;br /&gt;
The [[Editor API]] is used to control HTML text editors.&lt;br /&gt;
&lt;br /&gt;
=== Enrolment API (enrol) ===&lt;br /&gt;
&lt;br /&gt;
The [[Enrolment API]] deals with course participants.&lt;br /&gt;
&lt;br /&gt;
=== Events API (event) ===&lt;br /&gt;
&lt;br /&gt;
The [[Events API]] allows to define &amp;quot;events&amp;quot; with payload data to be fired whenever you like, and it also allows you to define handlers to react to these events when they happen.  This is the recommended form of inter-plugin communication. This also forms the basis for logging in Moodle.&lt;br /&gt;
&lt;br /&gt;
=== Experience API (xAPI) ===&lt;br /&gt;
&lt;br /&gt;
The Experience API (xAPI) is an e-learning standard that allows learning content and learning systems to speak to each other. The [[Experience API (xAPI)]]&lt;br /&gt;
allows any plugin to generate and handle xAPI standard statements.&lt;br /&gt;
&lt;br /&gt;
=== External functions API (external) ===&lt;br /&gt;
&lt;br /&gt;
The [[External functions API]] allows you to create fully parametrised methods that can be accessed by external programs (such as [[Web services]]).&lt;br /&gt;
&lt;br /&gt;
=== Favourites API ===&lt;br /&gt;
&lt;br /&gt;
The [[Favourites API]] allows you to mark items as favourites for a user and manage these favourites. This is often referred to as &#039;Starred&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Lock API (lock) ===&lt;br /&gt;
&lt;br /&gt;
The [[Lock API]] lets you synchronise processing between multiple requests, even for separate nodes in a cluster.&lt;br /&gt;
&lt;br /&gt;
=== Message API (message) ===&lt;br /&gt;
&lt;br /&gt;
The [[Message API]] lets you post messages to users.  They decide how they want to receive them.&lt;br /&gt;
&lt;br /&gt;
=== Media API (media) ===&lt;br /&gt;
&lt;br /&gt;
The [[Media_players#Using_media_players|Media]] API can be used to embed media items such as audio, video, and Flash.&lt;br /&gt;
&lt;br /&gt;
=== My profile API ===&lt;br /&gt;
&lt;br /&gt;
The [[My profile API]] is used to add things to the profile page.&lt;br /&gt;
&lt;br /&gt;
=== OAuth 2 API (oauth2) ===&lt;br /&gt;
&lt;br /&gt;
The [[OAuth 2 API]] is used to provide a common place to configure and manage external systems using OAuth 2.&lt;br /&gt;
&lt;br /&gt;
=== Payment API (payment) ===&lt;br /&gt;
&lt;br /&gt;
The [[Payment API]] deals with payments.&lt;br /&gt;
&lt;br /&gt;
=== Preference API (preference) ===&lt;br /&gt;
&lt;br /&gt;
The [[Preference API]] is a simple way to store and retrieve preferences for individual users.&lt;br /&gt;
&lt;br /&gt;
=== Portfolio API (portfolio) ===&lt;br /&gt;
&lt;br /&gt;
The [[Portfolio API]] allows you to add portfolio interfaces on your pages and allows users to package up data to send to their portfolios.&lt;br /&gt;
&lt;br /&gt;
=== Privacy API (privacy) ===&lt;br /&gt;
&lt;br /&gt;
The [[Privacy API]] allows you to describe the personal data that you store, and provides the means for that data to be discovered, exported, and deleted on a per-user basis.&lt;br /&gt;
This allows compliance with regulation such as the General Data Protection Regulation (GDPR) in Europe.&lt;br /&gt;
&lt;br /&gt;
=== Rating API (rating) ===&lt;br /&gt;
&lt;br /&gt;
The [[Rating API]] lets you create AJAX rating interfaces so that users can rate items in your plugin.  In an activity module, you may choose to aggregate ratings to form grades.&lt;br /&gt;
&lt;br /&gt;
=== RSS API (rss) ===&lt;br /&gt;
&lt;br /&gt;
The [[RSS API]] allows you to create secure RSS feeds of data in your module.&lt;br /&gt;
&lt;br /&gt;
=== Search API (search) ===&lt;br /&gt;
&lt;br /&gt;
The [[Search API]] allows you to index contents in a search engine and query the search engine for results.&lt;br /&gt;
&lt;br /&gt;
=== Tag API (tag) ===&lt;br /&gt;
&lt;br /&gt;
The [[Tag API]] allows you to store tags (and a tag cloud) to items in your module.&lt;br /&gt;
&lt;br /&gt;
=== Task API (task) ===&lt;br /&gt;
&lt;br /&gt;
The [[Task API]] lets you run jobs in the background. Either once off, or on a regular schedule.&lt;br /&gt;
&lt;br /&gt;
=== Time API (time) ===&lt;br /&gt;
&lt;br /&gt;
The [[Time API]] takes care of translating and displaying times between users in the site.&lt;br /&gt;
&lt;br /&gt;
=== Testing API (test) ===&lt;br /&gt;
&lt;br /&gt;
The testing API contains the Unit test API ([[PHPUnit]]) and Acceptance test API ([[Acceptance testing]]). Ideally all new code should have unit tests written FIRST.&lt;br /&gt;
&lt;br /&gt;
=== User-related APIs (user) ===&lt;br /&gt;
&lt;br /&gt;
This is a rather informal grouping of miscellaneous [[User-related APIs]] relating to sorting and searching lists of users.&lt;br /&gt;
&lt;br /&gt;
=== Web services API (webservice) ===&lt;br /&gt;
&lt;br /&gt;
The [[Web services API]] allows you to expose particular functions (usually external functions) as web services.&lt;br /&gt;
&lt;br /&gt;
=== Badges API (badges) ===&lt;br /&gt;
&lt;br /&gt;
The [https://docs.moodle.org/dev/OpenBadges_User_Documentation Badges] user documentation (is a temp page until we compile a proper page with all the classes and APIs that allows you to manage particular badges and OpenBadges Backpack).&lt;br /&gt;
&lt;br /&gt;
=== Custom fields API ===&lt;br /&gt;
&lt;br /&gt;
The [[Custom fields API]] allows you to configure and add custom fields for different entities&lt;br /&gt;
&lt;br /&gt;
== Activity module APIs ==&lt;br /&gt;
&lt;br /&gt;
Activity modules are the most important plugin in Moodle.  There are several core APIs that service only Activity modules.&lt;br /&gt;
&lt;br /&gt;
=== Activity completion API (completion) ===&lt;br /&gt;
&lt;br /&gt;
The [[Activity completion API]] is to indicate to the system how activities are completed.&lt;br /&gt;
&lt;br /&gt;
=== Advanced grading API (grading) ===&lt;br /&gt;
&lt;br /&gt;
The [[Advanced grading API]] allows you to add more advanced grading interfaces (such as rubrics) that can produce simple grades for the gradebook.&lt;br /&gt;
&lt;br /&gt;
=== Conditional activities API (condition) - deprecated in 2.7 ===&lt;br /&gt;
&lt;br /&gt;
The deprecated [[Conditional activities API]] used to provide conditional access to modules and sections in Moodle 2.6 and below. It has been replaced by the [[Availability API]].&lt;br /&gt;
&lt;br /&gt;
=== Groups API (group) ===&lt;br /&gt;
&lt;br /&gt;
The [[Groups API]] allows you to check the current activity group mode and set the current group.&lt;br /&gt;
&lt;br /&gt;
=== Gradebook API (grade) ===&lt;br /&gt;
&lt;br /&gt;
The [[Gradebook API]] allows you to read and write from the gradebook.  It also allows you to provide an interface for detailed grading information.&lt;br /&gt;
&lt;br /&gt;
=== Plagiarism API (plagiarism) ===&lt;br /&gt;
&lt;br /&gt;
The [[Plagiarism API]] allows your activity module to send files and data to external services to have them checked for plagiarism.&lt;br /&gt;
&lt;br /&gt;
=== Question API (question) ===&lt;br /&gt;
&lt;br /&gt;
The [[Question API]] (which can be divided into the Question bank API and the Question engine API), can be used by activities that want to use questions from the question bank.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Plugins]] - plugin types also have their own APIs&lt;br /&gt;
* [[Callbacks]] - list of all callbacks in Moodle&lt;br /&gt;
* [[Coding style]] - general information about writing PHP code for Moodle&lt;br /&gt;
&lt;br /&gt;
[[ja:コアAPI]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User-related_APIs&amp;diff=58481</id>
		<title>User-related APIs</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User-related_APIs&amp;diff=58481"/>
		<updated>2021-03-11T12:45:16Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Overview ==&lt;br /&gt;
&lt;br /&gt;
This is a collection of miscellaneous APIs that can help with doing things with lists of users. Note that, in many cases, the more specific [[Access API]], [[Groups API]], [[Enrolment API]], etc. may be what you need.&lt;br /&gt;
&lt;br /&gt;
== User field display ==&lt;br /&gt;
&lt;br /&gt;
[[User fields API]] (temp link, updating in progress)&lt;br /&gt;
&lt;br /&gt;
== User fields definition ==&lt;br /&gt;
&lt;br /&gt;
{{Moodle 3.1}}&lt;br /&gt;
To guarantee the sanity of the data inserted into Moodle and avoid security bugs, new user fields definition methods have been created for use in Moodle 3.1 onwards. The purpose of these new methods is to create a central point of user fields data validation and guarantee all data inserted into Moodle will be cleaned against the correct parameter type. Another goal of  this new API is to create consistency across Moodle core and avoid different parameter validations for the same user fields. For now on, user data must validate against the user field, not using clean_param() directly.&lt;br /&gt;
&lt;br /&gt;
===$propertiescache===&lt;br /&gt;
Cached information of each user field and its attributes filled by the fill_properties_cache.&lt;br /&gt;
&lt;br /&gt;
===fill_properties_cache()===&lt;br /&gt;
The main method of the user definition is to keep the definition of each user field and its properties. It verifies if the &#039;&#039;&#039;core_user::$propertiescache&#039;&#039;&#039; is already filled and caches all user fields attributes into this same attribute.&lt;br /&gt;
Each field matches the exact field name on the user table. That said, every new field added to the user table should be added to fill_properties_cache $fields array, otherwise it won&#039;t be validated or cleaned.&lt;br /&gt;
Each field has four possible properties, being choices and default optional:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;null&#039;&#039;&#039; - Whether the field is NULL or NOT_NULL, it SHOULD NOT be used as form validation, as many fields in the user table have NOT_NULL property but have the default value as (``).&lt;br /&gt;
* &#039;&#039;&#039;type&#039;&#039;&#039; - The expected parameter type (PARAM_*) to be used as validation and sanitizing.&lt;br /&gt;
* &#039;&#039;&#039;choices&#039;&#039;&#039; - A list of accepted values of that field. For example the list of the available countries, timezones, calendar type etc.&lt;br /&gt;
* &#039;&#039;&#039;default&#039;&#039;&#039; - The default value in case the user field didn&#039;t pass the validation or cleaned and we must set the default value. For example if the user country is invalid and it is not in the list of choices, set $CFG-&amp;gt;country.&lt;br /&gt;
&lt;br /&gt;
===validate()===&lt;br /&gt;
A static method to validate user fields, accepts an array or the user object as parameter, validate each parameter individually and can return true if all user data is correct or an array of validation errors. The purpose of this method is to just validate the user data, it won’t do any cleaning of the data.&lt;br /&gt;
&lt;br /&gt;
===clean_data()===&lt;br /&gt;
A static method that has the purpose of clean the user data and return the same user array/object. It receives an array with user data or a user object as parameter and it checks if the data is in the list of choices and if the property has a default value and clean the data if the user object doesn’t have a choices property.&lt;br /&gt;
It will display a debugging message if one the operations above has problems.&lt;br /&gt;
&lt;br /&gt;
===clean_field()===&lt;br /&gt;
A static method to clean a single user field. It has two parameters, the data to be cleaned and its user field. The behaviour of the method is similar to the clean_data. It will do the validations and cleaning and can display a debug message if an error has been found. It returns the cleaned data.&lt;br /&gt;
&lt;br /&gt;
===get_property_type()===&lt;br /&gt;
A helper method to get the type of the property. It receives the user field name as parameter and if it doesn&#039;t exist will throw an exception. If the property has been found, it will return its type.&lt;br /&gt;
&lt;br /&gt;
===get_property_null()===&lt;br /&gt;
A helper method to get the null property of the user field. It receives the user field name as parameter and if it doesn&#039;t exist it throws an exception. If the property has been found, it will return the null value.&lt;br /&gt;
&lt;br /&gt;
===get_property_choices()===&lt;br /&gt;
A helper method to get the list of choices of a user field. It receives the user field name as parameter and if it doesn&#039;t exist will throw an exception. If the property has been found, it will return the list of accepted values.&lt;br /&gt;
&lt;br /&gt;
===get_property_default()===&lt;br /&gt;
A helper method to get the default value of a property. It receives the user field name as parameter and if it doesn&#039;t exist or if it doesn&#039;t have a default attribute will throw an exception. If the property has been found, it will return its default value.&lt;br /&gt;
&lt;br /&gt;
== User selector ==&lt;br /&gt;
&lt;br /&gt;
The base class &amp;lt;tt&amp;gt;user_selector_base&amp;lt;/tt&amp;gt; defined in &amp;lt;tt&amp;gt;user/selector/lib.php&amp;lt;/tt&amp;gt;, which you can subclass to make a widget that lets you select users in an AJAX-y way. It is used, for example, on the Add group members page. The best way to learn how to use it is to search the code for other users, and see how they work. The base class also has good PHPdoc comments.&lt;br /&gt;
&lt;br /&gt;
== Sorting lists of users ==&lt;br /&gt;
&lt;br /&gt;
When you fetch a list of users from the database, they should always be sorted consistently, by using the &amp;lt;tt&amp;gt;users_order_by_sql&amp;lt;/tt&amp;gt; function to generate the order-by clause. Again, the best way to see how that works is to search the code for existing uses.&lt;br /&gt;
&lt;br /&gt;
== Others ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Please add more..&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
* [[Core APIs]]&lt;br /&gt;
* [[Access API]]&lt;br /&gt;
* [[Groups API]]&lt;br /&gt;
* [[Enrolment API]]&lt;br /&gt;
&lt;br /&gt;
[[Category:API]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58480</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58480"/>
		<updated>2021-03-11T12:38:25Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Getting SQL */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
The user fields API is located in the class &amp;lt;tt&amp;gt;\core\user_fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This API is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This API supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table columns.&lt;br /&gt;
&lt;br /&gt;
Because the capability for the current user depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove arbitrary extra fields ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this would usually be because you know you are going to list that field in a database query separately. It is OK to exclude a field whether or not it is actually included; that way the code will continue to work whatever the field list.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is a basic SQL query that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, [], 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core\user_fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can create SQL snippets for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
        SELECT u.id, u.deleted {$sql-&amp;gt;selects}&lt;br /&gt;
          FROM {user} u&lt;br /&gt;
               {$sql-&amp;gt;joins}&lt;br /&gt;
      ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core\user_fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58479</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58479"/>
		<updated>2021-03-11T12:31:12Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Add or remove arbitrary extra fields */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
The user fields API is located in the class &amp;lt;tt&amp;gt;\core\user_fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This API is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This API supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table columns.&lt;br /&gt;
&lt;br /&gt;
Because the capability for the current user depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove arbitrary extra fields ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this would usually be because you know you are going to list that field in a database query separately. It is OK to exclude a field whether or not it is actually included; that way the code will continue to work whatever the field list.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is the basic SQL that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code sql&amp;gt;&lt;br /&gt;
  SELECT u.id, u.deleted&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
ORDER BY u.lastaccess DESC&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core\user_fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can write the SQL for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
  SELECT id, deleted {$sql-&amp;gt;selects}&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
            {$sql-&amp;gt;joins}&lt;br /&gt;
ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core\user_fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58478</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58478"/>
		<updated>2021-03-11T12:30:49Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Add or remove custom fields to a list */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
The user fields API is located in the class &amp;lt;tt&amp;gt;\core\user_fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This API is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This API supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table columns.&lt;br /&gt;
&lt;br /&gt;
Because the capability for the current user depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove arbitrary extra fields ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this could be if you know you are going to have that field in a database query separately. It is OK to exclude a field whether or not it is actually included; that way the code will continue to work whatever the field list.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is the basic SQL that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code sql&amp;gt;&lt;br /&gt;
  SELECT u.id, u.deleted&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
ORDER BY u.lastaccess DESC&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core\user_fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can write the SQL for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
  SELECT id, deleted {$sql-&amp;gt;selects}&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
            {$sql-&amp;gt;joins}&lt;br /&gt;
ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core\user_fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58477</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58477"/>
		<updated>2021-03-11T12:28:38Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* List fields required to display a user&amp;#039;s identity */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
The user fields API is located in the class &amp;lt;tt&amp;gt;\core\user_fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This API is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This API supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table columns.&lt;br /&gt;
&lt;br /&gt;
Because the capability for the current user depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove custom fields to a list ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this could be if you know you are going to have that field in a database query separately.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is the basic SQL that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code sql&amp;gt;&lt;br /&gt;
  SELECT u.id, u.deleted&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
ORDER BY u.lastaccess DESC&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core\user_fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can write the SQL for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
  SELECT id, deleted {$sql-&amp;gt;selects}&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
            {$sql-&amp;gt;joins}&lt;br /&gt;
ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core\user_fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58476</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58476"/>
		<updated>2021-03-11T12:27:19Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Custom fields */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
The user fields API is located in the class &amp;lt;tt&amp;gt;\core\user_fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This API is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This API supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field, such as one with the shortname frog, is prefixed so that it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table.&lt;br /&gt;
&lt;br /&gt;
Because the permission depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove custom fields to a list ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this could be if you know you are going to have that field in a database query separately.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is the basic SQL that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code sql&amp;gt;&lt;br /&gt;
  SELECT u.id, u.deleted&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
ORDER BY u.lastaccess DESC&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core\user_fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can write the SQL for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
  SELECT id, deleted {$sql-&amp;gt;selects}&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
            {$sql-&amp;gt;joins}&lt;br /&gt;
ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core\user_fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58475</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58475"/>
		<updated>2021-03-11T12:26:40Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
The user fields API is located in the class &amp;lt;tt&amp;gt;\core\user_fields&amp;lt;/tt&amp;gt; in file &amp;lt;tt&amp;gt;/lib/classes/user_fields.php&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
This API is normally used when displaying lists of users in a table. It has the following main functions.&lt;br /&gt;
&lt;br /&gt;
# Decide which extra user fields (e.g. email, username) should be shown in the table, based on the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, the current context, and the permissions of the user.&lt;br /&gt;
# Provide lists of fields required for showing user picture or full name.&lt;br /&gt;
# Provide SQL that can be used within a query to get that information for users.&lt;br /&gt;
&lt;br /&gt;
== Custom fields ==&lt;br /&gt;
&lt;br /&gt;
This API supports two kinds of fields: basic user fields (in the &amp;lt;tt&amp;gt;user&amp;lt;/tt&amp;gt; table) and custom profile fields (in the &amp;lt;tt&amp;gt;user_info_data&amp;lt;/tt&amp;gt; table). Because the queries are more complex when getting custom profile fields, there may be some code that doesn&#039;t support custom profile fields.&lt;br /&gt;
&lt;br /&gt;
When fields are listed, a standard field in the user table such as email is called &amp;lt;tt&amp;gt;email&amp;lt;/tt&amp;gt;. A custom profile field with shortname frog is prefixed, so it is referred to as &amp;lt;tt&amp;gt;profile_field_frog&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Listing fields ==&lt;br /&gt;
&lt;br /&gt;
The following examples describe how you can get a list of user fields for a certain situation.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will return an array of fields required for displaying userpic (id, picture, firstname, lastname, ...).&lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;$OUTPUT-&amp;gt;user_picture($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns all fields needed to display a name (firstname, lastname, firstnamephonetic, ...). &lt;br /&gt;
&lt;br /&gt;
These are the fields you need to have in the user object in order to call the &amp;lt;tt&amp;gt;fullname($user)&amp;lt;/tt&amp;gt; function.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user picture and full name ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;with_userpic()-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need both types of fields, you can combine them by creating the object initially with a &#039;for&#039; method, and then calling the &#039;with&#039;  method to add a different category of fields.&lt;br /&gt;
&lt;br /&gt;
=== List fields required to display a user&#039;s identity ===&lt;br /&gt;
&lt;br /&gt;
When displaying a table of users, the name is always included (and sometimes a picture), as above. Other &#039;identity fields&#039; are configurable using the &amp;lt;tt&amp;gt;showuseridentity&amp;lt;/tt&amp;gt; admin setting, and display only to users who have the permission to see them. For example, it could be configured so that email addresses are shown, or user names, or department, or a custom field. These fields usually display in separate table.&lt;br /&gt;
&lt;br /&gt;
Because the permission depends on the location you are looking at, you need to pass a context when getting these fields.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The list of fields from this function might include custom profile fields e.g. &#039;profile_field_frog&#039;. In legacy code which only supports fields on the user table, you can pass false to the second &#039;allow custom fields&#039; parameter of the function:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context, false)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now it won&#039;t return any custom profile fields (and your code doesn&#039;t support them, which is sad).&lt;br /&gt;
&lt;br /&gt;
=== Add or remove custom fields to a list ===&lt;br /&gt;
&lt;br /&gt;
Sometimes you might want a list of fields to contain extra fields. You could just add them to the array, but that could cause duplicates in some situations which might result in bugs that only appear in certain settings. Instead, there is a convenient way to include them in these lists:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_identity($context)-&amp;gt;including(&#039;email&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively you might want to exclude a field - this could be if you know you are going to have that field in a database query separately.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$fields = \core\user_fields::for_name()-&amp;gt;excluding(&#039;firstname&#039;)-&amp;gt;get_required_fields();&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can use both including and excluding functions if you want.&lt;br /&gt;
&lt;br /&gt;
== Getting SQL ==&lt;br /&gt;
&lt;br /&gt;
In most cases it is not very useful to just get a list of fields. You also need to create the SQL to obtain the values for the fields.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s use a simple example - we are writing code that displays a list of all users in the whole system ordered by their last access date, most recent first. For some reason we are also including deleted users (but maybe we&#039;re going to display them differently). Here is the basic SQL that does this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code sql&amp;gt;&lt;br /&gt;
  SELECT u.id, u.deleted&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
ORDER BY u.lastaccess DESC&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Suppose that we also want to include the user&#039;s name, and any identity fields that are enabled for the current user. We don&#039;t want to include the &#039;id&#039; or &#039;deleted&#039; field because they&#039;re already included in the query.&lt;br /&gt;
&lt;br /&gt;
We can create a user fields object like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$context = \context_system::instance();&lt;br /&gt;
$userfields = \core\user_fields::for_name()-&amp;gt;with_identity($context)-&amp;gt;excluding(&#039;id&#039;, &#039;deleted&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The user fields object can write the SQL for you like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$sql = $userfields-&amp;gt;get_sql(&#039;u&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns an object with the fields &#039;selects&#039;, &#039;joins&#039;, &#039;params&#039;, and (not normally needed) &#039;mappings&#039;. Here is how you use it with the above SQL query:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$result = $DB-&amp;gt;get_records_sql(&amp;quot;&lt;br /&gt;
  SELECT id, deleted {$sql-&amp;gt;selects}&lt;br /&gt;
    FROM {user} u&lt;br /&gt;
            {$sql-&amp;gt;joins}&lt;br /&gt;
ORDER BY u.lastaccess DESC&amp;quot;, $sql-&amp;gt;params, 0, 10);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are many options to the get_sql function, as well as the user table alias shown. For example, you can:&lt;br /&gt;
&lt;br /&gt;
* Use named parameters instead of ? parameters.&lt;br /&gt;
* Add a prefix (e.g.u_) to all field names in the result to ensure they don&#039;t clash with other fields in the query.&lt;br /&gt;
* Rename the userid field e.g. to &#039;userid&#039;.&lt;br /&gt;
* Turn off the leading comma in the selects list.&lt;br /&gt;
&lt;br /&gt;
This last one might need explaining. By default, the &#039;selects&#039; list starts with a comma, which means you include it in the query as shown above (without a comma before it). By doing this, it will still work correctly if there are no fields to add (then it returns selects as blank, so there is no &#039;spare&#039; comma).&lt;br /&gt;
&lt;br /&gt;
== Selective field lists ==&lt;br /&gt;
&lt;br /&gt;
When you have created a user fields object that includes a lot of fields for different purposes, like the one in the above example, you might need to get a list of only some of those fields. The most common use case for this is when deciding on columns to display in a table. In the above example, we don&#039;t want a column in the table for every fields in the $userfields object - there doesn&#039;t need to be a column for the &#039;firstnamephonetic&#039; field, for instance. But we do need to know the extra identity fields that are being included, because these should each have a column.&lt;br /&gt;
&lt;br /&gt;
An extra parameter on get_required_fields lets you include only one category of fields from the object:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$extracolumns = $userfields-&amp;gt;get_required_fields([\core\user_fields::PURPOSE_IDENTITY]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Field names ==&lt;br /&gt;
&lt;br /&gt;
To display those columns you will need a header with a field name. You can get a field name as follows:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$header = \core\user_fields::get_display_name($field);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This works for both standard user table fields e.g. email (it will get the appropriate language string), and for custom fields (it will get the name for the custom field).&lt;br /&gt;
&lt;br /&gt;
== Older Moodle versions ==&lt;br /&gt;
&lt;br /&gt;
Prior to Moodle 3.11 the main functions used to carry out the same jobs were called &amp;lt;tt&amp;gt;get_extra_user_fields()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;get_extra_user_fields_sql()&amp;lt;/tt&amp;gt;, &amp;lt;tt&amp;gt;user_picture::fields()&amp;lt;/tt&amp;gt;, and &amp;lt;tt&amp;gt;get_all_user_name_fields()&amp;lt;/tt&amp;gt;. These all work, but are deprecated, in Moodle 3.11.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58474</id>
		<title>User fields</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=User_fields&amp;diff=58474"/>
		<updated>2021-03-11T11:44:38Z</updated>

		<summary type="html">&lt;p&gt;Quen: Created page with &amp;quot;{{Moodle 3.11}}  The user fields API is located in the class &amp;lt;code&amp;gt;\core\user_fields&amp;lt;/code&amp;gt;.  (Document in progress...)&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle 3.11}}&lt;br /&gt;
&lt;br /&gt;
The user fields API is located in the class &amp;lt;code&amp;gt;\core\user_fields&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
(Document in progress...)&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Core_APIs&amp;diff=58473</id>
		<title>Core APIs</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Core_APIs&amp;diff=58473"/>
		<updated>2021-03-11T11:43:48Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* User-related APIs (user) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Moodle has a number of core APIs that provide tools for Moodle scripts.&lt;br /&gt;
&lt;br /&gt;
They are essential when writing [[Plugins|Moodle plugins]].&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Most-used General APIs==&lt;br /&gt;
&lt;br /&gt;
These APIs are critical and will be used by nearly every Moodle plugin.&lt;br /&gt;
&lt;br /&gt;
=== Access API (access) ===&lt;br /&gt;
&lt;br /&gt;
The [[Access API]] gives you functions so you can determine what the current user is allowed to do, and it allows modules to extend Moodle with new capabilities.&lt;br /&gt;
&lt;br /&gt;
=== Data manipulation API (dml) ===&lt;br /&gt;
&lt;br /&gt;
The [[Data manipulation API]] allows you to read/write to databases in a consistent and safe way.&lt;br /&gt;
&lt;br /&gt;
=== File API (files) ===&lt;br /&gt;
&lt;br /&gt;
The [[File API]] controls the storage of files in connection to various plugins.&lt;br /&gt;
&lt;br /&gt;
=== Form API (form) ===&lt;br /&gt;
&lt;br /&gt;
The [[Form API]] defines and handles user data via web forms.&lt;br /&gt;
&lt;br /&gt;
=== Logging API (log) ===&lt;br /&gt;
&lt;br /&gt;
The [[Events API]] allows you to log events in Moodle, while [[Logging 2]] describes how logs are stored and retrieved.&lt;br /&gt;
&lt;br /&gt;
=== Navigation API (navigation) ===&lt;br /&gt;
&lt;br /&gt;
The [[Navigation API]] allows you to manipulate the navigation tree to add and remove items as you wish.&lt;br /&gt;
&lt;br /&gt;
=== Page API (page) ===&lt;br /&gt;
&lt;br /&gt;
The [[Page API]] is used to set up the current page, add JavaScript, and configure how things will be displayed to the user.&lt;br /&gt;
&lt;br /&gt;
=== Output API (output) ===&lt;br /&gt;
&lt;br /&gt;
The [[Output API]] is used to render the HTML for all parts of the page.&lt;br /&gt;
&lt;br /&gt;
=== String API (string) ===&lt;br /&gt;
&lt;br /&gt;
The [[String API]] is how you get language text strings to use in the user interface.   It handles any language translations that might be available.&lt;br /&gt;
&lt;br /&gt;
=== Upgrade API (upgrade) ===&lt;br /&gt;
&lt;br /&gt;
The [[Upgrade API]] is how your module installs and upgrades itself, by keeping track of its own version.&lt;br /&gt;
&lt;br /&gt;
=== Moodlelib API (core) ===&lt;br /&gt;
&lt;br /&gt;
The [[Moodlelib API]] is the central library file of miscellaneous general-purpose Moodle functions. Functions can over the handling of request parameters, configs, user preferences, time, login, mnet, plugins, strings and others. There are plenty of defined constants too.&lt;br /&gt;
&lt;br /&gt;
==Other General APIs==&lt;br /&gt;
&lt;br /&gt;
=== Admin settings API (admin) ===&lt;br /&gt;
&lt;br /&gt;
The [[Admin settings]] API deals with providing configuration options for each plugin and Moodle core.&lt;br /&gt;
&lt;br /&gt;
=== Analytics API (analytics) ===&lt;br /&gt;
&lt;br /&gt;
The [[Analytics API]] allow you to create prediction models and generate insights.&lt;br /&gt;
&lt;br /&gt;
=== Availability API (availability) ===&lt;br /&gt;
&lt;br /&gt;
The [[Availability API]] controls access to activities and sections.&lt;br /&gt;
&lt;br /&gt;
=== Backup API (backup) ===&lt;br /&gt;
&lt;br /&gt;
The [[Backup API]] defines exactly how to convert course data into XML for backup purposes, and the [[Restore API]] describes how to convert it back the other way.&lt;br /&gt;
&lt;br /&gt;
=== Cache API (cache) ===&lt;br /&gt;
&lt;br /&gt;
The [[The Moodle Universal Cache (MUC)]] is the structure for storing cache data within Moodle. [[Cache_API]] explains some of what is needed to use a cache in your code.&lt;br /&gt;
&lt;br /&gt;
=== Calendar API (calendar) ===&lt;br /&gt;
&lt;br /&gt;
The [[Calendar API]] allows you to add and modify events in the calendar for user, groups, courses, or the whole site.&lt;br /&gt;
&lt;br /&gt;
=== Check API (check) ===&lt;br /&gt;
&lt;br /&gt;
The [[Check API]] allows you to add security, performance or health checks to your site.&lt;br /&gt;
&lt;br /&gt;
=== Comment API (comment) ===&lt;br /&gt;
&lt;br /&gt;
The [[Comment API]] allows you to save and retrieve user comments, so that you can easily add commenting to any of your code.&lt;br /&gt;
&lt;br /&gt;
=== Competency API (competency) ===&lt;br /&gt;
&lt;br /&gt;
The [[Competency API]] allows you to list and add evidence of competencies to learning plans, learning plan templates, frameworks, courses and activities.&lt;br /&gt;
&lt;br /&gt;
=== Data definition API (ddl) ===&lt;br /&gt;
&lt;br /&gt;
The [[Data definition API]] is what you use to create, change and delete tables and fields in the database during upgrades.&lt;br /&gt;
&lt;br /&gt;
=== Editor API ===&lt;br /&gt;
&lt;br /&gt;
The [[Editor API]] is used to control HTML text editors.&lt;br /&gt;
&lt;br /&gt;
=== Enrolment API (enrol) ===&lt;br /&gt;
&lt;br /&gt;
The [[Enrolment API]] deals with course participants.&lt;br /&gt;
&lt;br /&gt;
=== Events API (event) ===&lt;br /&gt;
&lt;br /&gt;
The [[Events API]] allows to define &amp;quot;events&amp;quot; with payload data to be fired whenever you like, and it also allows you to define handlers to react to these events when they happen.  This is the recommended form of inter-plugin communication. This also forms the basis for logging in Moodle.&lt;br /&gt;
&lt;br /&gt;
=== Experience API (xAPI) ===&lt;br /&gt;
&lt;br /&gt;
The Experience API (xAPI) is an e-learning standard that allows learning content and learning systems to speak to each other. The [[Experience API (xAPI)]]&lt;br /&gt;
allows any plugin to generate and handle xAPI standard statements.&lt;br /&gt;
&lt;br /&gt;
=== External functions API (external) ===&lt;br /&gt;
&lt;br /&gt;
The [[External functions API]] allows you to create fully parametrised methods that can be accessed by external programs (such as [[Web services]]).&lt;br /&gt;
&lt;br /&gt;
=== Favourites API ===&lt;br /&gt;
&lt;br /&gt;
The [[Favourites API]] allows you to mark items as favourites for a user and manage these favourites. This is often referred to as &#039;Starred&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Lock API (lock) ===&lt;br /&gt;
&lt;br /&gt;
The [[Lock API]] lets you synchronise processing between multiple requests, even for separate nodes in a cluster.&lt;br /&gt;
&lt;br /&gt;
=== Message API (message) ===&lt;br /&gt;
&lt;br /&gt;
The [[Message API]] lets you post messages to users.  They decide how they want to receive them.&lt;br /&gt;
&lt;br /&gt;
=== Media API (media) ===&lt;br /&gt;
&lt;br /&gt;
The [[Media_players#Using_media_players|Media]] API can be used to embed media items such as audio, video, and Flash.&lt;br /&gt;
&lt;br /&gt;
=== My profile API ===&lt;br /&gt;
&lt;br /&gt;
The [[My profile API]] is used to add things to the profile page.&lt;br /&gt;
&lt;br /&gt;
=== OAuth 2 API (oauth2) ===&lt;br /&gt;
&lt;br /&gt;
The [[OAuth 2 API]] is used to provide a common place to configure and manage external systems using OAuth 2.&lt;br /&gt;
&lt;br /&gt;
=== Payment API (payment) ===&lt;br /&gt;
&lt;br /&gt;
The [[Payment API]] deals with payments.&lt;br /&gt;
&lt;br /&gt;
=== Preference API (preference) ===&lt;br /&gt;
&lt;br /&gt;
The [[Preference API]] is a simple way to store and retrieve preferences for individual users.&lt;br /&gt;
&lt;br /&gt;
=== Portfolio API (portfolio) ===&lt;br /&gt;
&lt;br /&gt;
The [[Portfolio API]] allows you to add portfolio interfaces on your pages and allows users to package up data to send to their portfolios.&lt;br /&gt;
&lt;br /&gt;
=== Privacy API (privacy) ===&lt;br /&gt;
&lt;br /&gt;
The [[Privacy API]] allows you to describe the personal data that you store, and provides the means for that data to be discovered, exported, and deleted on a per-user basis.&lt;br /&gt;
This allows compliance with regulation such as the General Data Protection Regulation (GDPR) in Europe.&lt;br /&gt;
&lt;br /&gt;
=== Rating API (rating) ===&lt;br /&gt;
&lt;br /&gt;
The [[Rating API]] lets you create AJAX rating interfaces so that users can rate items in your plugin.  In an activity module, you may choose to aggregate ratings to form grades.&lt;br /&gt;
&lt;br /&gt;
=== RSS API (rss) ===&lt;br /&gt;
&lt;br /&gt;
The [[RSS API]] allows you to create secure RSS feeds of data in your module.&lt;br /&gt;
&lt;br /&gt;
=== Search API (search) ===&lt;br /&gt;
&lt;br /&gt;
The [[Search API]] allows you to index contents in a search engine and query the search engine for results.&lt;br /&gt;
&lt;br /&gt;
=== Tag API (tag) ===&lt;br /&gt;
&lt;br /&gt;
The [[Tag API]] allows you to store tags (and a tag cloud) to items in your module.&lt;br /&gt;
&lt;br /&gt;
=== Task API (task) ===&lt;br /&gt;
&lt;br /&gt;
The [[Task API]] lets you run jobs in the background. Either once off, or on a regular schedule.&lt;br /&gt;
&lt;br /&gt;
=== Time API (time) ===&lt;br /&gt;
&lt;br /&gt;
The [[Time API]] takes care of translating and displaying times between users in the site.&lt;br /&gt;
&lt;br /&gt;
=== Testing API (test) ===&lt;br /&gt;
&lt;br /&gt;
The testing API contains the Unit test API ([[PHPUnit]]) and Acceptance test API ([[Acceptance testing]]). Ideally all new code should have unit tests written FIRST.&lt;br /&gt;
&lt;br /&gt;
=== User-related APIs (user) ===&lt;br /&gt;
&lt;br /&gt;
This is a rather informal grouping of miscellaneous [[User-related APIs]] relating to sorting and searching lists of users.&lt;br /&gt;
&lt;br /&gt;
=== User fields API ===&lt;br /&gt;
&lt;br /&gt;
The [[User fields API]] is mainly used when displaying tables of data about users. It indicates which extra fields (e.g. email) should be displayed in the current context based on the permissions of the current user. It also provides ways to get the necessary data from a query, and to obtainother generally-useful fields for user names and pictures.&lt;br /&gt;
&lt;br /&gt;
=== Web services API (webservice) ===&lt;br /&gt;
&lt;br /&gt;
The [[Web services API]] allows you to expose particular functions (usually external functions) as web services.&lt;br /&gt;
&lt;br /&gt;
=== Badges API (badges) ===&lt;br /&gt;
&lt;br /&gt;
The [https://docs.moodle.org/dev/OpenBadges_User_Documentation Badges] user documentation (is a temp page until we compile a proper page with all the classes and APIs that allows you to manage particular badges and OpenBadges Backpack).&lt;br /&gt;
&lt;br /&gt;
=== Custom fields API ===&lt;br /&gt;
&lt;br /&gt;
The [[Custom fields API]] allows you to configure and add custom fields for different entities&lt;br /&gt;
&lt;br /&gt;
== Activity module APIs ==&lt;br /&gt;
&lt;br /&gt;
Activity modules are the most important plugin in Moodle.  There are several core APIs that service only Activity modules.&lt;br /&gt;
&lt;br /&gt;
=== Activity completion API (completion) ===&lt;br /&gt;
&lt;br /&gt;
The [[Activity completion API]] is to indicate to the system how activities are completed.&lt;br /&gt;
&lt;br /&gt;
=== Advanced grading API (grading) ===&lt;br /&gt;
&lt;br /&gt;
The [[Advanced grading API]] allows you to add more advanced grading interfaces (such as rubrics) that can produce simple grades for the gradebook.&lt;br /&gt;
&lt;br /&gt;
=== Conditional activities API (condition) - deprecated in 2.7 ===&lt;br /&gt;
&lt;br /&gt;
The deprecated [[Conditional activities API]] used to provide conditional access to modules and sections in Moodle 2.6 and below. It has been replaced by the [[Availability API]].&lt;br /&gt;
&lt;br /&gt;
=== Groups API (group) ===&lt;br /&gt;
&lt;br /&gt;
The [[Groups API]] allows you to check the current activity group mode and set the current group.&lt;br /&gt;
&lt;br /&gt;
=== Gradebook API (grade) ===&lt;br /&gt;
&lt;br /&gt;
The [[Gradebook API]] allows you to read and write from the gradebook.  It also allows you to provide an interface for detailed grading information.&lt;br /&gt;
&lt;br /&gt;
=== Plagiarism API (plagiarism) ===&lt;br /&gt;
&lt;br /&gt;
The [[Plagiarism API]] allows your activity module to send files and data to external services to have them checked for plagiarism.&lt;br /&gt;
&lt;br /&gt;
=== Question API (question) ===&lt;br /&gt;
&lt;br /&gt;
The [[Question API]] (which can be divided into the Question bank API and the Question engine API), can be used by activities that want to use questions from the question bank.&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
&lt;br /&gt;
* [[Plugins]] - plugin types also have their own APIs&lt;br /&gt;
* [[Callbacks]] - list of all callbacks in Moodle&lt;br /&gt;
* [[Coding style]] - general information about writing PHP code for Moodle&lt;br /&gt;
&lt;br /&gt;
[[ja:コアAPI]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Message_API&amp;diff=58088</id>
		<title>Message API</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Message_API&amp;diff=58088"/>
		<updated>2020-11-26T11:54:04Z</updated>

		<summary type="html">&lt;p&gt;Quen: Changing the message send example to show how to actually send a notification from a plugin which is what this page is about...&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==What is this document?==&lt;br /&gt;
&lt;br /&gt;
This document describes how to make use of the Moodle Messaging API to send messages to Moodle users.&lt;br /&gt;
&lt;br /&gt;
If you are after a general introduction on using the Moodle Messaging system go to [[:en:Messaging|messaging user documentation]].&lt;br /&gt;
&lt;br /&gt;
If you are looking for details of how the Messaging system&#039;s internal structure was implemented, go to [[Messaging 2.0]].&lt;br /&gt;
&lt;br /&gt;
If you are looking for instructions on the implementation of a custom message processor (a component that receives messages sent to a user), go to [[Messaging custom components]].&lt;br /&gt;
&lt;br /&gt;
If you are looking for instructions on sending messages programatically within Moodle then read on...&lt;br /&gt;
&lt;br /&gt;
==Overview==&lt;br /&gt;
&lt;br /&gt;
Moodle components have the ability to send messages to users via the Moodle messaging system. Any type of component, for example a plugin or block, can register as a message producer then send messages to users.&lt;br /&gt;
&lt;br /&gt;
==File locations==&lt;br /&gt;
&lt;br /&gt;
The Message API code is contained within lib/messagelib.php and is automatically included for you during page setup.&lt;br /&gt;
&lt;br /&gt;
==Functions==&lt;br /&gt;
&lt;br /&gt;
message_send() is the primary point of contact for the message API. Call it to send a message to a user. You can find a full description of the arguments that must be supplied at (link to phpdocs). There is also an example below.&lt;br /&gt;
&lt;br /&gt;
==Message popup==&lt;br /&gt;
{{Moodle_2.9}}&lt;br /&gt;
A Javascript popup can be displayed through a link to invite a user to message another. In order to use this feature, you need to require the Javascript libraries using &#039;&#039;message_messenger_requirejs()&#039;&#039; and create a link with the attributes returned by &#039;&#039;message_messenger_sendmessage_link_params()&#039;&#039;. More in the examples.&lt;br /&gt;
&lt;br /&gt;
==Examples==&lt;br /&gt;
&lt;br /&gt;
===How to register as a message producer===&lt;br /&gt;
&lt;br /&gt;
The messages produced by a message provider is defined in the /db/messages.php file of a component. Below is code from the quiz module&#039;s messages.php file, shown as an example.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
defined(&#039;MOODLE_INTERNAL&#039;) || die();&lt;br /&gt;
$messageproviders = array (&lt;br /&gt;
    // Notify teacher that a student has submitted a quiz attempt&lt;br /&gt;
    &#039;submission&#039; =&amp;gt; array (&lt;br /&gt;
        &#039;capability&#039;  =&amp;gt; &#039;mod/quiz:emailnotifysubmission&#039;&lt;br /&gt;
    ),&lt;br /&gt;
    // Confirm a student&#039;s quiz attempt&lt;br /&gt;
    &#039;confirmation&#039; =&amp;gt; array (&lt;br /&gt;
        &#039;capability&#039;  =&amp;gt; &#039;mod/quiz:emailconfirmsubmission&#039;&lt;br /&gt;
    )&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The quiz can send two kinds of messages, quiz &amp;quot;submission&amp;quot; and &amp;quot;confirmation&amp;quot; notifications. Each message type is only available to users with the appropriate capability. Please note that the capability is checked at the system level context. Users who have this capability will have this message listed in their messaging preferences. You can omit the capability section if your message should be visible for all users. For example forum post notifications are available to all users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$messageproviders = array (&lt;br /&gt;
    // Ordinary single forum posts&lt;br /&gt;
    &#039;posts&#039; =&amp;gt; array (&lt;br /&gt;
    )&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When displaying your message types in a user&#039;s messaging preferences it will use a string from your component&#039;s language file called &amp;quot;messageprovider:messagename&amp;quot;. For example here are the relevant strings from the quiz&#039;s language file.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$string[&#039;messageprovider:confirmation&#039;] = &#039;Confirmation of your own quiz submissions&#039;;&lt;br /&gt;
$string[&#039;messageprovider:submission&#039;] = &#039;Notification of quiz submissions&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Once your messages.php is complete you need to increase the version number of your component in its version.php. That will cause Moodle to check messages.php looking for new or changed message definitions. Log in as an admin and go to /admin/index.php (the Notifications page) to start the upgrade process.&lt;br /&gt;
===Setting defaults===&lt;br /&gt;
The default processor can be set using an element of the array &lt;br /&gt;
e.g.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
 &#039;mynotification&#039; =&amp;gt; [&lt;br /&gt;
         &#039;defaults&#039; =&amp;gt; [&lt;br /&gt;
              &#039;popup&#039; =&amp;gt; MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,&lt;br /&gt;
              &#039;email&#039; =&amp;gt; MESSAGE_PERMITTED &lt;br /&gt;
          ],&lt;br /&gt;
    ],&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
With that setting email will be permitted but disabled for each user by default. It  can be turned on by each user through the preferences/notification preferences options (/message/notificationpreferences.php?userid=X)&lt;br /&gt;
The possible values are recorded in the lib.php file of messaging&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Define contants for messaging default settings population. For unambiguity of&lt;br /&gt;
 * plugin developer intentions we use 4-bit value (LSB numbering):&lt;br /&gt;
 * bit 0 - whether to send message when user is loggedin (MESSAGE_DEFAULT_LOGGEDIN)&lt;br /&gt;
 * bit 1 - whether to send message when user is loggedoff (MESSAGE_DEFAULT_LOGGEDOFF)&lt;br /&gt;
 * bit 2..3 - messaging permission (MESSAGE_DISALLOWED|MESSAGE_PERMITTED|MESSAGE_FORCED)&lt;br /&gt;
 *&lt;br /&gt;
 * MESSAGE_PERMITTED_MASK contains the mask we use to distinguish permission setting&lt;br /&gt;
 */&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Note that if you change the values in message.php and then upgrade the plugin the values will not automatically be changed in the config_plugins table where they are stored.&lt;br /&gt;
&lt;br /&gt;
===How to send a message===&lt;br /&gt;
{{Moodle_2.9}}&lt;br /&gt;
Here is example code showing you how to actually send a notification message. The example shows the construction of a object with specific properties, which is then passed to the message_send() function that uses the information to send a message.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$message = new \core\message\message();&lt;br /&gt;
$message-&amp;gt;component = &#039;mod_yourmodule&#039;; // Your plugin&#039;s name&lt;br /&gt;
$message-&amp;gt;name = &#039;mynotification&#039;; // Your notification name from message.php&lt;br /&gt;
$message-&amp;gt;userfrom = core_user::get_noreply_user(); // If the message is &#039;from&#039; a specific user you can set them here&lt;br /&gt;
$message-&amp;gt;userto = $user;&lt;br /&gt;
$message-&amp;gt;subject = &#039;message subject 1&#039;;&lt;br /&gt;
$message-&amp;gt;fullmessage = &#039;message body&#039;;&lt;br /&gt;
$message-&amp;gt;fullmessageformat = FORMAT_MARKDOWN;&lt;br /&gt;
$message-&amp;gt;fullmessagehtml = &#039;&amp;lt;p&amp;gt;message body&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
$message-&amp;gt;smallmessage = &#039;small message&#039;;&lt;br /&gt;
$message-&amp;gt;notification = 1; // Because this is a notification generated from Moodle, not a user-to-user message&lt;br /&gt;
$message-&amp;gt;contexturl = (new \moodle_url(&#039;/course/&#039;))-&amp;gt;out(false); // A relevant URL for the notification&lt;br /&gt;
$message-&amp;gt;contexturlname = &#039;Course list&#039;; // Link title explaining where users get to for the contexturl&lt;br /&gt;
$content = array(&#039;*&#039; =&amp;gt; array(&#039;header&#039; =&amp;gt; &#039; test &#039;, &#039;footer&#039; =&amp;gt; &#039; test &#039;)); // Extra content for specific processor&lt;br /&gt;
$message-&amp;gt;set_additional_content(&#039;email&#039;, $content);&lt;br /&gt;
&lt;br /&gt;
// You probably don&#039;t need attachments but if you do, here is how to add one&lt;br /&gt;
$usercontext = context_user::instance($user-&amp;gt;id);&lt;br /&gt;
$file = new stdClass;&lt;br /&gt;
$file-&amp;gt;contextid = $usercontext-&amp;gt;id;&lt;br /&gt;
$file-&amp;gt;component = &#039;user&#039;;&lt;br /&gt;
$file-&amp;gt;filearea  = &#039;private&#039;;&lt;br /&gt;
$file-&amp;gt;itemid    = 0;&lt;br /&gt;
$file-&amp;gt;filepath  = &#039;/&#039;;&lt;br /&gt;
$file-&amp;gt;filename  = &#039;1.txt&#039;;&lt;br /&gt;
$file-&amp;gt;source    = &#039;test&#039;;&lt;br /&gt;
&lt;br /&gt;
$fs = get_file_storage();&lt;br /&gt;
$file = $fs-&amp;gt;create_file_from_string($file, &#039;file1 content&#039;);&lt;br /&gt;
$message-&amp;gt;attachment = $file;&lt;br /&gt;
&lt;br /&gt;
// Actually send the message&lt;br /&gt;
$messageid = message_send($message);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Before 2.9 message data used to be a stdClass object as shown below (This formation of a message will no longer work as of Moodle 3.6. Only a message object will be accepted):-&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$message = new stdClass();&lt;br /&gt;
$message-&amp;gt;component         = &#039;mod_quiz&#039;; //your component name&lt;br /&gt;
$message-&amp;gt;name              = &#039;submission&#039;; //this is the message name from messages.php&lt;br /&gt;
$message-&amp;gt;userfrom          = $USER;&lt;br /&gt;
$message-&amp;gt;userto            = $touser;&lt;br /&gt;
$message-&amp;gt;subject           = $subject;&lt;br /&gt;
$message-&amp;gt;fullmessage       = $message;&lt;br /&gt;
$message-&amp;gt;fullmessageformat = FORMAT_PLAIN;&lt;br /&gt;
$message-&amp;gt;fullmessagehtml   = &#039;&#039;;&lt;br /&gt;
$message-&amp;gt;smallmessage      = &#039;&#039;;&lt;br /&gt;
$message-&amp;gt;notification      = 1; //this is only set to 0 for personal messages between users&lt;br /&gt;
message_send($message);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===How to set-up the message popup===&lt;br /&gt;
&lt;br /&gt;
Here is example code showing you how to set-up the Javascript popup link.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
require_once(&#039;message/lib.php&#039;);&lt;br /&gt;
$userid = 2;&lt;br /&gt;
$userto = $DB-&amp;gt;get_record(&#039;user&#039;, array(&#039;id&#039; =&amp;gt; $userid));&lt;br /&gt;
&lt;br /&gt;
message_messenger_requirejs();&lt;br /&gt;
$url = new moodle_url(&#039;message/index.php&#039;, array(&#039;id&#039; =&amp;gt; $userto-&amp;gt;id));&lt;br /&gt;
$attributes = message_messenger_sendmessage_link_params($userto);&lt;br /&gt;
echo html_writer::link($url, &#039;Send a message&#039;, $attributes);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Changes in Moodle 3.5==&lt;br /&gt;
{{Moodle_3.5}}&lt;br /&gt;
&lt;br /&gt;
In Moodle 3.5, there were some moderately big changes. The only docs I have been able to find about them are in [https://github.com/moodle/moodle/blob/33a388eff737c049786ee42d7430db549568471c/message/upgrade.txt#L56 upgrade.txt] file. However, that is the details, here is an overview:&lt;br /&gt;
&lt;br /&gt;
The main message_send() API to send a message has not changed, so if yout code is just sending messages, you don&#039;t need to do anything.&lt;br /&gt;
&lt;br /&gt;
Similarly, message_output plugins don&#039;t need to change, so no worries there.&lt;br /&gt;
&lt;br /&gt;
If you are doing things with messages, then you need to understand how the internals have changed.&lt;br /&gt;
&lt;br /&gt;
The database tables have changed. Messages from Moodle components to a user (e.g. mod_quiz), telling them that something has happened (e.g. an attempt was submitted) have always been &#039;Notifications&#039;. In the past, this was just a column in the mdl_message table. Now, messages and notifications are stored in completely separate tables. Notifications are in mdl_notifications. The strucutre of this table is very similar to the old mdl_message table which is now not used at all. Messages are in mdl_messages, and related tables, that now exist to support group messaging. Those tables join together like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code sql&amp;gt;&lt;br /&gt;
SELECT *&lt;br /&gt;
&lt;br /&gt;
FROM mdl_messages m&lt;br /&gt;
JOIN mdl_message_conversations con ON con.id = m.conversationid&lt;br /&gt;
JOIN mdl_message_conversation_members mem ON mem.conversationid = con.id&lt;br /&gt;
LEFT JOIN mdl_message_user_actions act ON act.userid = mem.userid AND act.messageid = m.id&lt;br /&gt;
&lt;br /&gt;
ORDER BY m.timecreated, m.id, mem.userid, act.id&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
* [[Core APIs]]&lt;br /&gt;
&lt;br /&gt;
[[Category:API]]&lt;br /&gt;
[[Category:Tutorial]]&lt;br /&gt;
[[Category:Plugins]]&lt;br /&gt;
[[Category:Messaging]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=57980</id>
		<title>Running acceptance test</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=57980"/>
		<updated>2020-11-04T12:00:18Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* 5. Run Behat tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Short version ==&lt;br /&gt;
&lt;br /&gt;
...or how I got it to work on Ubuntu and some of the problems encountered. &lt;br /&gt;
&lt;br /&gt;
You need a bunch of browsers and terminal windows open to do this.&lt;br /&gt;
&lt;br /&gt;
==== 1. Background ====  &lt;br /&gt;
&lt;br /&gt;
# I am using the desktop version of Ubuntu 17.04 so there are no issues about running this software in headless mode. Running in headless mode was not tested.&lt;br /&gt;
# Moodle is version 3.3 and is a fully installed and working version using the &#039;standard&#039; Ubuntu LAMP stack.&lt;br /&gt;
&lt;br /&gt;
==== 2. Set up Selenium ====&lt;br /&gt;
First, you should have a look at [[Acceptance_testing/Browsers/Working_combinations_of_OS%2BBrowser%2Bselenium#Working_combinations_of_OS.2BBrowser.2Bselenium|working combinations of OS+Browser+selenium]].&lt;br /&gt;
&lt;br /&gt;
# Download the Selenium Standalone Server from [http://www.seleniumhq.org/download/ http://www.seleniumhq.org/download/]. It&#039;s a single JAR file, put it anywhere handy.&lt;br /&gt;
# Download the Chrome driver from [https://sites.google.com/a/chromium.org/chromedriver/ https://sites.google.com/a/chromium.org/chromedriver/]. Ensure you have the right version of the Chrome driver - see [[#Trouble_shooting| Trouble shooting]]. (Firefox is currently problematic. See [[Actual_Selenium_with_old_Firefox_47.0.1]] if you need to try to make it work.)&lt;br /&gt;
# Unzip the driver (it&#039;s a single file) and copy to /usr/local/bin (should work anywhere on the path)&lt;br /&gt;
# If not installed already, &#039;&amp;lt;tt&amp;gt;sudo apt install default-jre&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Start Selenium - &#039;&amp;lt;tt&amp;gt;java -jar /path/to/your/selenium/server/selenium-server-standalone-N.NN.N.jar -port 4444&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# check it works, access &#039;localhost:4444/wd/hub/&#039; in your browser and check you can create a new Chrome session.&lt;br /&gt;
&lt;br /&gt;
If running headless or the above doesn&#039;t work (&amp;quot;Selenium server is not running&amp;quot; when running the behat tests). Try the following&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;Xvfb -ac :99 -screen 0 1280x1024x16 &amp;amp;&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Then immediately, &#039;&amp;lt;tt&amp;gt;export DISPLAY=:99&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# The run the Selenium command as above&lt;br /&gt;
&lt;br /&gt;
==== 3. Set up Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Create a new &#039;dataroot&#039; area for files especially for behat adjusting permissions accordingly. &lt;br /&gt;
# If not there already, add Section 11 from config-dist.php to your config.php file and review the settings. &lt;br /&gt;
# $CFG-&amp;gt;behat_wwwroot needs to point to your Moodle site yet be different from the &#039;normal&#039; wwwroot (e.g. if you used localhost for wwwroot use 127.0.0.1 for the behat_wwwroot). Whatever you choose, make sure it works. &lt;br /&gt;
# $CFG-&amp;gt;behat_dataroot should point to the directory you created above&lt;br /&gt;
# $CFG-&amp;gt;behat_prefix should be fine. &lt;br /&gt;
# Set up $CFG-&amp;gt;behat_profiles to select Chrome as the browser...&lt;br /&gt;
&lt;br /&gt;
     $CFG-&amp;gt;behat_profiles = [&lt;br /&gt;
         &#039;default&#039; =&amp;gt; [&lt;br /&gt;
             &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
             &#039;extensions&#039; =&amp;gt; [&lt;br /&gt;
                 &#039;Behat\MinkExtension&#039; =&amp;gt; [&lt;br /&gt;
                     &#039;selenium2&#039; =&amp;gt; [&lt;br /&gt;
                         &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
                     ]&lt;br /&gt;
                 ]&lt;br /&gt;
             ]&lt;br /&gt;
         ]&lt;br /&gt;
     ];&lt;br /&gt;
&lt;br /&gt;
==== 4. Configure Behat for Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;php admin/tool/behat/cli/init.php&amp;lt;/tt&amp;gt;&#039; (from the root of your Moodle install). This installs all the required software and creates the test version of Moodle. &lt;br /&gt;
&lt;br /&gt;
==== 5. Run Behat tests ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;vendor/bin/behat&amp;lt;/tt&amp;gt;&#039;. If you don&#039;t want to run all the tests add &#039;&amp;lt;tt&amp;gt;--tags=&amp;quot;@something&amp;quot;&amp;lt;/tt&amp;gt;&#039; where the @something refers to the tags at the top of most feature files. Use comma-separated list like &#039;&amp;lt;tt&amp;gt;@some_thing,@some_thing_else&amp;lt;/tt&amp;gt;&#039; to run tests from multiple areas. See upstream documentation on Gherkin filters for advanced syntax and more complex examples.&lt;br /&gt;
# After some initial setup dots should start to go by. It&#039;s a while before Selenium is first accessed. On the Linux desktop a new Chrome window appears and the testing process &#039;remote control&#039; starts (hopefully!)&lt;br /&gt;
# At the end of each line it shows you the number of steps that have completed so far.&lt;br /&gt;
#* If you are doing a full run, then as of November 2020 there are about 63,000 steps, which took around 25 hours to complete on one test machine (when not using the parallel running feature).&lt;br /&gt;
&lt;br /&gt;
== Prerequisite ==&lt;br /&gt;
Before initializing acceptance test environment for running behat, you should ensure:&lt;br /&gt;
# [[Acceptance_testing#Requirements Meet min. system requirements for running tests]]&lt;br /&gt;
# [[Acceptance_testing#Installation Have set min. config variable in config.php for behat]]&lt;br /&gt;
# [[Acceptance_testing#Installation Downloaded composer dependencies]]&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Acceptance tests (also known as behat), use [http://www.seleniumhq.org/download/ Selenium server] and can be run as:&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; In single run, only one behat run is executed. So all features are executed in this single run.&lt;br /&gt;
# &#039;&#039;&#039;Parallel runs:&#039;&#039;&#039; (Since Moodle 3.0) Parallel runs allow dev&#039;s to execute multiple behat runs together. This was introduced to get acceptance tests results faster. To achieve this:&lt;br /&gt;
#* Features are divided between multiple behat runs&lt;br /&gt;
#* Symlinks behatrun{x} (x being the run process number), are created pointing to moodle directory, so site for run 1 is accessible via https://localhost/moodle/behatrun1&lt;br /&gt;
#* Process number is included as suffix to $CFG-&amp;gt;behat_prefix.&lt;br /&gt;
#* Process number is suffixed to $CFG-&amp;gt;behat_dataroot.&lt;br /&gt;
&lt;br /&gt;
== Step 1: Initialise acceptance test environment ==&lt;br /&gt;
Before running acceptance tests, environment needs to be initialised for acceptance testing.&lt;br /&gt;
&lt;br /&gt;
=== Single run ===&lt;br /&gt;
For initialising acceptance tests for single run, above command is sufficient.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For initialising acceptance tests for parallel runs, you can use one of the following options&lt;br /&gt;
# &#039;&#039;&#039;-j=&amp;lt;number&amp;gt; or --parallel=&amp;lt;number&amp;gt;&#039;&#039;&#039; (required) Number of parallel behat run to initialise&lt;br /&gt;
# &#039;&#039;&#039;-m=&amp;lt;number&amp;gt; or --maxruns=&amp;lt;number&amp;gt;&#039;&#039;&#039;  (optional) Max parallel site which should be initialised at one time. If your system is slow, then you can initialise sites in chucks.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun=&amp;lt;number&amp;gt;&#039;&#039;&#039; (optional) Initialise site to run specified run from. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;--torun=&amp;lt;number&amp;gt;&#039;&#039;&#039; (optional) Initialise site to run specified run till. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;-o or --optimize-runs&#039;&#039;&#039; (optional) This option will split features with specified tags in all parallel runs, so they are executed first when parallel run gets executed.&lt;br /&gt;
# &#039;&#039;&#039;-a=&amp;lt;name&amp;gt; or --add-core-features-to-theme=&amp;lt;name&amp;gt;&#039;&#039;&#039; (optional) Since Moodle 3.2. Use this option to add all core features to specified themes (comma separated list of themes)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
// Below command will initialise moodle to run 2 parallel tests.&lt;br /&gt;
php admin/tool/behat/cli/init.php --parallel=2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 2: Running acceptance test environment ==&lt;br /&gt;
=== Single run ===&lt;br /&gt;
Run either of the following commands. For more options &#039;&#039;&#039;vendor/bin/behat --help&#039;&#039;&#039; or http://docs.behat.org/guides/6.cli.html&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You almost always want to limit the number of tests that are run. To run all the tests in one plugin:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml --tags mod_myplugin&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To run all the tests in one .feature file:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml /path/to/moodle/mod/myplugin/tests/behat/testsomething.feature&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To run a single scenario:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml /path/to/moodle/mod/myplugin/tests/behat/testsomething.feature:40&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here, &#039;40&#039; is the line-number of the feature file where the Scenario starts.&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For running parallel runs, use following command&lt;br /&gt;
&lt;br /&gt;
 php admin/tool/behat/cli/run.php&lt;br /&gt;
&lt;br /&gt;
Following optional options are available for custom run:&lt;br /&gt;
# &#039;&#039;&#039;--feature&#039;&#039;&#039; Only execute specified feature file (Absolute path of feature file).&lt;br /&gt;
# &#039;&#039;&#039;--suite&#039;&#039;&#039; Features for specified theme will be executed.&lt;br /&gt;
# &#039;&#039;&#039;--replace&#039;&#039;&#039; Replace args string with run process number, useful for output and reruns.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun&#039;&#039;&#039; Execute run starting from (Used for parallel runs on different vms)&lt;br /&gt;
# &#039;&#039;&#039;--torun&#039;&#039;&#039; Execute run till (Used for parallel runs on different vms)&lt;br /&gt;
# Behat options can be passed for filtering features/scenarios:&lt;br /&gt;
#* In case you don&#039;t want to run Javascript tests, use the Behat tags option to skip them, &#039;&#039;&#039;--tags=&amp;quot;~@javascript&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific scenario, use the Behat name option to run it, &#039;&#039;&#039;--name=&amp;quot;Filter user accounts by role and cohort&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific feature file, use the Behat feature option to run it, &#039;&#039;&#039;--feature=&amp;quot;/PATH/TO/MOODLE/admin/tests/behat/filter_users.feature&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Example: Initialise and run Behat tests for a custom plugin under a custom theme:&lt;br /&gt;
&lt;br /&gt;
 php admin/tool/behat/cli/init.php --parallel=3 --add-core-features-to-theme=&amp;quot;mytheme&amp;quot;&lt;br /&gt;
 php admin/tool/behat/cli/run.php --tags=&amp;quot;@tool_myplugin&amp;quot; --suite=&amp;quot;mytheme&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Common options for running tests ===&lt;br /&gt;
==== Tests filters ====&lt;br /&gt;
With the &#039;&#039;&#039;--tags&#039;&#039;&#039; or the &#039;&#039;&#039;-name&#039;&#039;&#039; Behat options you can filter which tests are going to run or which ones are going to be skipped. There are a few tags that you might be interested in:&lt;br /&gt;
* &#039;&#039;&#039;@javascript&#039;&#039;&#039;: All the tests that runs in a browser using Javascript; they require Selenium to be running, otherwise an exception will be thrown.&lt;br /&gt;
* &#039;&#039;&#039;@_file_upload&#039;&#039;&#039;: All the tests that involves file uploading or any OS feature that is not 100% part of the browser. They should only be executed when Selenium is running in the same machine where the tests are running.&lt;br /&gt;
* &#039;&#039;&#039;@_alert&#039;&#039;&#039;: All the tests that involves Javascript dialogs (alerts, confirms...) are using a feature that is OS-dependant and out of the browser scope, so they should be tag appropriately as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_window&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; window&#039;&#039;&#039; step should be tagged as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_iframe&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; iframe&#039;&#039;&#039; steps should be tagged as it is an advanced feature and some browsers may have problems dealing with them&lt;br /&gt;
* &#039;&#039;&#039;@_cross_browser&#039;&#039;&#039;: All the tests that should run against multiple combinations of browsers + OS in a regular basis. The features that are sensitive to different combinations of OS and browsers should be tagges as @_cross_browser.&lt;br /&gt;
* &#039;&#039;&#039;@componentname&#039;&#039;&#039;: Moodle features uses the [https://docs.moodle.org/dev/Frankenstyle Frankenstyle] component name to tag the features according to the Moodle subsystem they belong to.&lt;br /&gt;
&lt;br /&gt;
==== Output formats ====&lt;br /&gt;
Since Moodle 3.1 option for output is:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=pretty --out=/path/to/pretty.txt --format=moodle_progress --out=std&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Before Moodle 3.1 option for output was:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=&#039;moodle_progress,pretty&#039; --out=&#039;,/path/to/pretty.txt&#039;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Following output formats are supported:&lt;br /&gt;
# &#039;&#039;&#039;progress&#039;&#039;&#039;: Prints one character per step.&lt;br /&gt;
# &#039;&#039;&#039;pretty&#039;&#039;&#039;: Prints the feature as is.&lt;br /&gt;
# &#039;&#039;&#039;junit&#039;&#039;&#039;: Outputs the failures in JUnit compatible files.&lt;br /&gt;
# &#039;&#039;&#039;moodle_progress&#039;&#039;&#039;: Prints Moodle branch information and dots for each step.&lt;br /&gt;
# &#039;&#039;&#039;moodle_list&#039;&#039;&#039;: List all scenarios.&lt;br /&gt;
# &#039;&#039;&#039;moodle_stepcount&#039;&#039;&#039;: List all features with total steps in each feature file. Used for parallel run.&lt;br /&gt;
# &#039;&#039;&#039;moodle_screenshot&#039;&#039;&#039;: (since Moodle 3.1) Take screenshot and core dump of each step. With following options you can dump either or both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;image&amp;quot;}&#039;**: will dump image only&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;}&#039;**: will dump html only.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html,image&amp;quot;}&#039;**: will dump both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;, &amp;quot;dir_permissions&amp;quot;: &amp;quot;0777&amp;quot;}&#039;**&lt;br /&gt;
If you want to see the failures immediately (rather than waiting ~3 hours for all the tests to finish) then either use the -v option to output a bit more information, or change the output format using --format. Format &#039;pretty&#039; (&#039;&#039;&#039;-f pretty&#039;&#039;&#039;) is sufficient for most cases, as it outputs each step outcomes in the command line making easier to see the progress.&lt;br /&gt;
&lt;br /&gt;
== Advance usage ==&lt;br /&gt;
=== Rerun failed scenarios ===&lt;br /&gt;
With slow systems or parallel run you might see some random failures, to rerun only failed scenarios (to eliminate random failures), use --rerun option&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; --run=&amp;quot;absolute_path_to_empty_file&amp;quot; (Behat will record failed scenarios in this file, and when run again only failed scenarios will be run)&lt;br /&gt;
# &#039;&#039;&#039;Parallel run:&#039;&#039;&#039; --rerun=&amp;quot;absolute_path_to_empty_file_{runprocess}.txt --replace=&amp;quot;{runprocess}&amp;quot; ({runprocess} will be replaced with the process number for recording fails in the specific run process).&lt;br /&gt;
&#039;&#039;&#039;Since Moodle 3.1 --rerun option don&#039;t accept any value, as it is handled internally by behat&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Running behat with specified theme (Since Moodle 3.2) ===&lt;br /&gt;
You can run behat with any theme installed. To execute behat with specified theme use &#039;&#039;&#039;--suite={THEME_NAME}&#039;&#039;&#039; option, while running behat. By default the features in theme behat folder will be executed for the suite. But if you want to run all core features with the specific theme then initialise acceptance test with --add-core-features-to-theme={THEME_NAME}, e.g.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php --add-core-features-to-theme=clean&lt;br /&gt;
vendor/bin/behat --suite=clean --tags=&amp;quot;@enrol_foobar&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That is a core theme but it will work with custom theme. No = or quotes needed around the theme name.&lt;br /&gt;
&lt;br /&gt;
Make sure that &amp;lt;tt&amp;gt;$CFG-&amp;gt;theme&amp;lt;/tt&amp;gt; is &#039;&#039;&#039;not set&#039;&#039;&#039; in your config.php.&lt;br /&gt;
&lt;br /&gt;
==== Override behat core context for theme suite ====&lt;br /&gt;
To override behat step definitions so as to run behat with specified theme, you should create a contexts within &#039;&#039;&#039;/theme/{MYTHEME}/tests/behat/&#039;&#039;&#039; with prefix behat_theme_{MYTHEME}_ and suffixed with the context being overridden. For example, if you want to override behat_mod_forum context, then you should create a class /theme/{MYTHEME}/tests/behat/mod_forum/behat_theme_{MYTHEME}_behat_mod_forum.php&lt;br /&gt;
&lt;br /&gt;
==== Blacklist behat context or features to run in theme suite ====&lt;br /&gt;
To blacklist contexts/ features to be executed by theme suite you should create a /theme/{MYTHEME}/tests/behat/blacklist.json file with following format. Following will not use step_definitions from  behat_grade and behat_navigation while running theme suite. Also, scenarios in auth/tests/behat/login.feature and grade/tests/behat/grade_hidden_items.feature won&#039;t be executed with theme suite.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;contexts&amp;quot;: [&lt;br /&gt;
    &amp;quot;behat_grade&amp;quot;,&lt;br /&gt;
    &amp;quot;behat_navigation&amp;quot;,&lt;br /&gt;
  ],&lt;br /&gt;
  &amp;quot;features&amp;quot;: [&lt;br /&gt;
    &amp;quot;auth/tests/behat/login.feature&amp;quot;,&lt;br /&gt;
    &amp;quot;grade/tests/behat/grade_hidden_items.feature&amp;quot;,&lt;br /&gt;
   ]&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
==== Override core behat selectors ====&lt;br /&gt;
To override behat selectors in specific theme, you should create a class behat_theme_{MYTHEME}_behat_selectors in /theme/{MYTHEME}/tests/behat/behat_theme_{MYTHEME}_behat_selectors.php extending behat_selectors.&lt;br /&gt;
&lt;br /&gt;
=== Use php built in web server ===&lt;br /&gt;
You can use php built-in-web server for executing behat runs. To do so:&lt;br /&gt;
# Open a command line interface and &#039;&#039;&#039;cd /to/your/moodle/dirroot&#039;&#039;&#039;&lt;br /&gt;
# &#039;&#039;&#039;php -S localhost:8000&#039;&#039;&#039; (This is the test site URL that moodle uses by default, if you want to use another one you can override it in config.php with $CFG-&amp;gt;behat_wwwroot attribute; more info in https://docs.moodle.org/dev/Acceptance_testing#Advanced_usage or config-dist.php)&lt;br /&gt;
# Update $CFG-&amp;gt;behat_wwwroot = localhost:8000; in config.php&lt;br /&gt;
&lt;br /&gt;
=== Define custom options for parallel runs ===&lt;br /&gt;
You can set following custom config options for parallel runs via $CFG-&amp;gt;behat_parallel_run. It&#039;s an array of options where 1st array is for 1st run and so on.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
       array (&lt;br /&gt;
           &#039;dbtype&#039; =&amp;gt; &#039;mysqli&#039;,&lt;br /&gt;
           &#039;dblibrary&#039; =&amp;gt; &#039;native&#039;,&lt;br /&gt;
           &#039;dbhost&#039; =&amp;gt; &#039;localhost&#039;,&lt;br /&gt;
           &#039;dbname&#039; =&amp;gt; &#039;moodletest&#039;,&lt;br /&gt;
           &#039;dbuser&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;dbpass&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;behat_prefix&#039; =&amp;gt; &#039;mdl_&#039;,&lt;br /&gt;
           &#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;,&lt;br /&gt;
           &#039;behat_wwwroot&#039; =&amp;gt; &#039;http://127.0.0.1/moodle&#039;,&lt;br /&gt;
           &#039;behat_dataroot&#039; =&amp;gt; &#039;/home/example/bht_moodledata&#039;&lt;br /&gt;
       )&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To set different selenium servers for parallel runs, you can use following. NOTE: Running parallel (headless) runs on different selenium servers avoid random focus failures.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
    $CFG-&amp;gt;behat_parallel_run = array (&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4445/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4446/wd/hub&#039;),&lt;br /&gt;
    );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Write new tests and behat methods ===&lt;br /&gt;
&lt;br /&gt;
If you want to write tests for your own integration, you can do so by creating new tests with format .feature. Follow instructions in [[Writing_acceptance_tests|this page]] to write new tests. &lt;br /&gt;
&lt;br /&gt;
It is also possible to add new steps the moodle behat integration. In order to do so, you will have to create a new .php class with the prefix &#039;&#039;&#039;behat_&#039;&#039;&#039;. Copy the format from &#039;&#039;&#039;lib\behat\behat_base.php&#039;&#039;&#039;, but set your class to extend the behat_base class instead of the MinkExtension. You can define new behat steps by declaring functions with the appropriate heading. &lt;br /&gt;
&lt;br /&gt;
You will not be  able to use these steps and features right away. Check [[Running_acceptance_test#New_step_definitions_or_features_are_not_executed|this section]] for instructions on how to update the behat integration. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
For further information on how to create new steps definitions, check [[Acceptance testing/Custom acceptance steps]].&lt;br /&gt;
&lt;br /&gt;
=== Running acceptance tests with different browser ===&lt;br /&gt;
&lt;br /&gt;
If you follow the steps above, Behat will run with Chrome.&lt;br /&gt;
&lt;br /&gt;
You can get it to run with other browsers. The basic idea is to expand the $CFG-&amp;gt;behat_profiles array in config.php to list more browsers.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_profiles = array(&lt;br /&gt;
   &#039;chrome&#039; =&amp;gt; array(&lt;br /&gt;
       &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
       &#039;tags&#039; =&amp;gt; &#039;@javascript&#039;,&lt;br /&gt;
   )&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Acceptance_testing/Browsers|More info about alternative browsers]]&lt;br /&gt;
&lt;br /&gt;
=== Start multiple selenium servers ===&lt;br /&gt;
From command line Start selenium servers at different ports (say 4444, 4445, 4446 for 3 parallel runs)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4444 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4445 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4446&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternative way of running three Selenium servers in parallel:&lt;br /&gt;
&lt;br /&gt;
 $ printf %d\\n {4444..4446} | xargs -n 1 -P 3 java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port&lt;br /&gt;
&lt;br /&gt;
=== Run tests directly in Chrome, with no Selenium ===&lt;br /&gt;
&lt;br /&gt;
Historically, the tests would talk to a Selenium server, which would then tell the target browser what to do. More and more, the browsers themselves contain such a server and you can talk to them directly, which is faster and easier. Work was done to get this working with Chrome in MDL-58948, but it still needs some further setup to get it working, which is detailed in the bug ticket, and which I&#039;ll copy below:&lt;br /&gt;
&lt;br /&gt;
Replace your current behat config with the abve, the api_url is where you&#039;ll talk directly to your Chrome browser.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_config = [&lt;br /&gt;
    &#039;default&#039; =&amp;gt; [&lt;br /&gt;
        &#039;extensions&#039; =&amp;gt; [&lt;br /&gt;
            &#039;DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension&#039; =&amp;gt; [],&lt;br /&gt;
            &#039;Behat\MinkExtension&#039;                                          =&amp;gt; [&lt;br /&gt;
                &#039;browser_name&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
                &#039;base_url&#039;     =&amp;gt; $CFG-&amp;gt;behat_wwwroot,&lt;br /&gt;
                &#039;goutte&#039;       =&amp;gt; null,&lt;br /&gt;
                &#039;selenium2&#039;    =&amp;gt; null,&lt;br /&gt;
                &#039;sessions&#039;     =&amp;gt; [&lt;br /&gt;
                    &#039;javascript&#039; =&amp;gt; [&lt;br /&gt;
                        &#039;chrome&#039; =&amp;gt; [&lt;br /&gt;
                            &#039;api_url&#039; =&amp;gt; &#039;http://localhost:9222&#039;&lt;br /&gt;
                        ]&lt;br /&gt;
                    ]&lt;br /&gt;
                ]&lt;br /&gt;
            ]&lt;br /&gt;
        ]&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Use composer to install two more required libraries:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
composer require --dev dmore/behat-chrome-extension&lt;br /&gt;
composer require --dev dmore/chrome-mink-driver&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you try to run the tests it will tell you that Chrome isn&#039;t running, in a second tab run the following to start Chrome (or chromium-browser on Ubuntu)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;chrome --disable-gpu --headless --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is running headless so you won&#039;t see any windows pop up as the tests run, though it runs in the other mode too.&lt;br /&gt;
&lt;br /&gt;
Behat should now run as before, but faster.&lt;br /&gt;
&lt;br /&gt;
==== Run Chrome locally with Behat and Moodle on a remote server ====&lt;br /&gt;
If you have Moodle running on a remote (headless) server, but don&#039;t want to install a window manager, you can do the following:&lt;br /&gt;
&lt;br /&gt;
1. Go through all the steps from above&lt;br /&gt;
&lt;br /&gt;
2. Establish an SSH SOCKS5 proxy connection to your server:&lt;br /&gt;
&amp;lt;code&amp;gt;ssh -D VARIABLE-PORT-A -N -q -C USER@SERVER&amp;lt;/code&amp;gt;&lt;br /&gt;
3. Forward the port to connect to the DevTools-API of your browser:&lt;br /&gt;
&amp;lt;code&amp;gt;ssh -R VARIABLE-PORT-B:localhost:VARIABLE-PORT-B USER@SERVER -N -q -C&amp;lt;/code&amp;gt;&lt;br /&gt;
4. Tell your local Chrome to use custom settings:&lt;br /&gt;
&amp;lt;code&amp;gt;/path/to/Google\ Chrome [--disable-gpu] --remote-debugging-address=0.0.0.0 --remote-debugging-port=VARIABLE-PORT-B --proxy-server=socks://127.0.0.1:VARIABLE-PORT-A --proxy-bypass-list=&#039;&amp;lt;-loopback&amp;gt;&#039;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Or, all in one command:&lt;br /&gt;
&amp;lt;code&amp;gt;ssh -D VARIABLE-PORT-A -N -q -C -f USER@SERVER &amp;amp;&amp;amp; ssh -R VARIABLE-PORT-B:localhost:VARIABLE-PORT-B USER@SERVER -N -q -C -f &amp;amp;&amp;amp; /path/to/Google\ Chrome [--disable-gpu] --remote-debugging-address=0.0.0.0 --remote-debugging-port=VARIABLE-PORT-B --proxy-server=socks://127.0.0.1:VARIABLE-PORT-A --proxy-bypass-list=&#039;&amp;lt;-loopback&amp;gt;&#039;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explanation for the SSH settings:&lt;br /&gt;
* VARIABLE-PORT-B &amp;amp; VARIABLE-PORT-A : Choose these as you like. Bear in mind that you will need root rights if the port number is 1023 or lower&lt;br /&gt;
* -N: Tells SSH not to open an actual command prompt&lt;br /&gt;
* -C: Compress all data passed through the tunnel&lt;br /&gt;
* -q: Quiet mode. Causes most warning and diagnostic messages to be suppressed&lt;br /&gt;
*-R: Open a tunnel binding to localhost on the remote machine, going to localhost on the local machine&lt;br /&gt;
* -f: Fork the process into background&lt;br /&gt;
&lt;br /&gt;
Explanation for the Chrome settings:&lt;br /&gt;
* --proxy-bypass-list=&#039;&amp;lt;-loopback&amp;gt;&#039; : Tells Chrome to send requests to localhost through the tunnel&lt;br /&gt;
* --proxy-server=socks://127.0.0.1:VARIABLE-PORT-A : To use the SOCKS5 tunnel we just set up&lt;br /&gt;
* --remote-debugging-port=VARIABLE-PORT-B &amp;amp; --remote-debugging-address=0.0.0.0 : To use the SSH tunnel we just set up&lt;br /&gt;
&lt;br /&gt;
=== Using Docker to start selenium server ===&lt;br /&gt;
==== What is Docker ====&lt;br /&gt;
Docker is a app container,  it&#039;s a kind of virtual machine, but only for one app, service,  so you can download&lt;br /&gt;
a docker image and run a selenium server without worry in how to configure selenium in your machine, one for chrome, others for firefox, you either don&#039;t need to install the browsers in your machine&lt;br /&gt;
To install docker follow this link; https://docs.docker.com/engine/installation/&lt;br /&gt;
&lt;br /&gt;
==== Selenium docker images ====&lt;br /&gt;
There is many docker images available,  for many browser, the complete list is in https://hub.docker.com/u/selenium/&lt;br /&gt;
for moodle you can use standalone version.&lt;br /&gt;
You can download  specific selenium version too,  for example,  for firefox,  moodle recommend selenium 2.53.1, see: [https://docs.moodle.org/dev/Acceptance_testing/Browsers/Working_combinations_of_OS%2BBrowser%2Bselenium What version do I need?]&lt;br /&gt;
&lt;br /&gt;
so  the command will be:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -d -p4444:4444 selenium/standalone-firefox:2.53.1-beryllium&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
to see all available version click in tags.   For firefox you can find at: https://hub.docker.com/r/selenium/standalone-firefox/tags/&lt;br /&gt;
&lt;br /&gt;
==== Change config.php file ====&lt;br /&gt;
In config.php file you must change the $CFG-&amp;gt;behat_wwwroot=   to your network card (NIC) ip address,  you can&#039;t use &lt;br /&gt;
localhost , 127.0.0.1, ...  or selenium docker server  will fail&lt;br /&gt;
&lt;br /&gt;
=== Increasing timeouts ===&lt;br /&gt;
&lt;br /&gt;
You may see errors such as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
Javascript code and/or AJAX requests are not ready after 10 seconds. &lt;br /&gt;
There is a Javascript error or the code is extremely slow.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Sometimes this indicates a genuine problem with the code, but if you are using a slow computer, it could just mean that the browser was not yet ready. You may find that the test works if you run it again. If you get this error frequently, it might be useful to increase the timeout.&lt;br /&gt;
&lt;br /&gt;
It is possible to increase this timeout by adding a line in your config.php. (Requires Moodle versions 3.5 (from 3.5.6), 3.6 (from 3.6.4), or 3.7+.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_increasetimeout = 2;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will increase all the timeouts by a factor of 2; if that isn&#039;t sufficient, you could use 3. &lt;br /&gt;
&lt;br /&gt;
Increasing timeouts will make tests run a bit slower (because there are points where Behat waits up to a timeout to make sure something doesn&#039;t happen) so don&#039;t do this unless you need to.&lt;br /&gt;
&lt;br /&gt;
== NOTE ==&lt;br /&gt;
# Start the Selenium server (in case you want to run tests that involves Javascript)&lt;br /&gt;
#* (See http://www.installationpage.com/selenium/how-to-run-selenium-headless-firefox-in-ubuntu/ for running &#039;headless&#039; Firefox and xvfm in a server environment)&lt;br /&gt;
#* Open another command line interface and &#039;&#039;&#039;java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Disable acceptance test environment ===&lt;br /&gt;
if you want to prevent access to test environment&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --disable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Note that if you have the HTTP_PROXY environment variable set, which you may have had to do to run composer, then you also need to set NO_PROXY=localhost.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Trouble shooting ==&lt;br /&gt;
&lt;br /&gt;
=== New step definitions or features are not executed === &lt;br /&gt;
If you are adding new tests or steps definitions update the tests list&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --enable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039; For parallel runs, all options for initialising parallel runs are valid &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Tests are failing ===&lt;br /&gt;
&lt;br /&gt;
If you followed all the steps and you receive an unknown weird error probably your browser version is not compatible with the Selenium version you are running.  Please refer Working combinations to ensure you have correct [[Acceptance_testing/Browsers#Working_combinations_of_OS.2BBrowser.2Bselenium]] of them to run acceptance test.&lt;br /&gt;
&lt;br /&gt;
=== The tests are failing, and the error message is completely useless ===&lt;br /&gt;
&lt;br /&gt;
For example, it just says &amp;quot;Error writing to database&amp;quot; with no stack trace.&lt;br /&gt;
&lt;br /&gt;
Add -vv command-line option to get very verbose output.&lt;br /&gt;
&lt;br /&gt;
=== Errors during setup (before test are launched) ===&lt;br /&gt;
Typical errors are:&lt;br /&gt;
* Behat requirement not satisfied: http://127.0.0.1/m/stable_master is not available, ensure you specified correct url and that the server is set up and started.&lt;br /&gt;
* Behat is configured but not enabled on this test site.&lt;br /&gt;
&lt;br /&gt;
In order to fix those errors please check that: the behat_dataroot has correct write permissions and that the $CFG-&amp;gt;behat* variables are placed before the lib/setup.php include:&lt;br /&gt;
 require_once(__DIR__ . &#039;/lib/setup.php&#039;);&lt;br /&gt;
&lt;br /&gt;
=== Selenium server is not running ===&lt;br /&gt;
==== Chrome specific ====&lt;br /&gt;
&lt;br /&gt;
If you are using chrome, you need to ensure that the driver matches the version of the installed chrome browser – which may change on OS updates/upgrades.  Moodle or Selenium will not give the appropriate message – see [https://tracker.moodle.org/browse/MDL-67659/ MDL-67659].  One solution is the one suggested in the issue and use Andrew Nicols’ [https://github.com/andrewnicols/chromedriver-wrapper/  Chromedriver Wrapper] which will ensure you have the appropriate driver before running the tests.&lt;br /&gt;
&lt;br /&gt;
== External links ==&lt;br /&gt;
* Vagrant profile with Moodle and Behat preconfigured: https://github.com/mackensen/moodle-hat&lt;br /&gt;
* Docker containers for Moodle Developers and Behat: https://github.com/moodlehq/moodle-docker&lt;br /&gt;
* Docker environment with Behat preconfigured : https://github.com/tobiga/docker_moodle_environment&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Acceptance testing for the mobile app]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quality Assurance]][[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=57979</id>
		<title>Running acceptance test</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=57979"/>
		<updated>2020-11-04T11:59:56Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Short version ==&lt;br /&gt;
&lt;br /&gt;
...or how I got it to work on Ubuntu and some of the problems encountered. &lt;br /&gt;
&lt;br /&gt;
You need a bunch of browsers and terminal windows open to do this.&lt;br /&gt;
&lt;br /&gt;
==== 1. Background ====  &lt;br /&gt;
&lt;br /&gt;
# I am using the desktop version of Ubuntu 17.04 so there are no issues about running this software in headless mode. Running in headless mode was not tested.&lt;br /&gt;
# Moodle is version 3.3 and is a fully installed and working version using the &#039;standard&#039; Ubuntu LAMP stack.&lt;br /&gt;
&lt;br /&gt;
==== 2. Set up Selenium ====&lt;br /&gt;
First, you should have a look at [[Acceptance_testing/Browsers/Working_combinations_of_OS%2BBrowser%2Bselenium#Working_combinations_of_OS.2BBrowser.2Bselenium|working combinations of OS+Browser+selenium]].&lt;br /&gt;
&lt;br /&gt;
# Download the Selenium Standalone Server from [http://www.seleniumhq.org/download/ http://www.seleniumhq.org/download/]. It&#039;s a single JAR file, put it anywhere handy.&lt;br /&gt;
# Download the Chrome driver from [https://sites.google.com/a/chromium.org/chromedriver/ https://sites.google.com/a/chromium.org/chromedriver/]. Ensure you have the right version of the Chrome driver - see [[#Trouble_shooting| Trouble shooting]]. (Firefox is currently problematic. See [[Actual_Selenium_with_old_Firefox_47.0.1]] if you need to try to make it work.)&lt;br /&gt;
# Unzip the driver (it&#039;s a single file) and copy to /usr/local/bin (should work anywhere on the path)&lt;br /&gt;
# If not installed already, &#039;&amp;lt;tt&amp;gt;sudo apt install default-jre&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Start Selenium - &#039;&amp;lt;tt&amp;gt;java -jar /path/to/your/selenium/server/selenium-server-standalone-N.NN.N.jar -port 4444&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# check it works, access &#039;localhost:4444/wd/hub/&#039; in your browser and check you can create a new Chrome session.&lt;br /&gt;
&lt;br /&gt;
If running headless or the above doesn&#039;t work (&amp;quot;Selenium server is not running&amp;quot; when running the behat tests). Try the following&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;Xvfb -ac :99 -screen 0 1280x1024x16 &amp;amp;&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Then immediately, &#039;&amp;lt;tt&amp;gt;export DISPLAY=:99&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# The run the Selenium command as above&lt;br /&gt;
&lt;br /&gt;
==== 3. Set up Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Create a new &#039;dataroot&#039; area for files especially for behat adjusting permissions accordingly. &lt;br /&gt;
# If not there already, add Section 11 from config-dist.php to your config.php file and review the settings. &lt;br /&gt;
# $CFG-&amp;gt;behat_wwwroot needs to point to your Moodle site yet be different from the &#039;normal&#039; wwwroot (e.g. if you used localhost for wwwroot use 127.0.0.1 for the behat_wwwroot). Whatever you choose, make sure it works. &lt;br /&gt;
# $CFG-&amp;gt;behat_dataroot should point to the directory you created above&lt;br /&gt;
# $CFG-&amp;gt;behat_prefix should be fine. &lt;br /&gt;
# Set up $CFG-&amp;gt;behat_profiles to select Chrome as the browser...&lt;br /&gt;
&lt;br /&gt;
     $CFG-&amp;gt;behat_profiles = [&lt;br /&gt;
         &#039;default&#039; =&amp;gt; [&lt;br /&gt;
             &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
             &#039;extensions&#039; =&amp;gt; [&lt;br /&gt;
                 &#039;Behat\MinkExtension&#039; =&amp;gt; [&lt;br /&gt;
                     &#039;selenium2&#039; =&amp;gt; [&lt;br /&gt;
                         &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
                     ]&lt;br /&gt;
                 ]&lt;br /&gt;
             ]&lt;br /&gt;
         ]&lt;br /&gt;
     ];&lt;br /&gt;
&lt;br /&gt;
==== 4. Configure Behat for Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;php admin/tool/behat/cli/init.php&amp;lt;/tt&amp;gt;&#039; (from the root of your Moodle install). This installs all the required software and creates the test version of Moodle. &lt;br /&gt;
&lt;br /&gt;
==== 5. Run Behat tests ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;vendor/bin/behat&amp;lt;/tt&amp;gt;&#039;. If you don&#039;t want to run all the tests add &#039;&amp;lt;tt&amp;gt;--tags=&amp;quot;@something&amp;quot;&amp;lt;/tt&amp;gt;&#039; where the @something refers to the tags at the top of most feature files. Use comma-separated list like &#039;&amp;lt;tt&amp;gt;@some_thing,@some_thing_else&amp;lt;/tt&amp;gt;&#039; to run tests from multiple areas. See upstream documentation on Gherkin filters for advanced syntax and more complex examples.&lt;br /&gt;
# After some initial setup dots should start to go by. It&#039;s a while before Selenium is first accessed. On the Linux desktop a new Chrome window appears and the testing process &#039;remote control&#039; starts (hopefully!)&lt;br /&gt;
# At the end of each line it shows you the number of steps that have completed so far.&lt;br /&gt;
#* If you are doing a full run, then as of November 2020 there are in total approximately 63,000 steps, which took around 25 hours to complete on one test machine (when not using the parallel running feature).&lt;br /&gt;
&lt;br /&gt;
== Prerequisite ==&lt;br /&gt;
Before initializing acceptance test environment for running behat, you should ensure:&lt;br /&gt;
# [[Acceptance_testing#Requirements Meet min. system requirements for running tests]]&lt;br /&gt;
# [[Acceptance_testing#Installation Have set min. config variable in config.php for behat]]&lt;br /&gt;
# [[Acceptance_testing#Installation Downloaded composer dependencies]]&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Acceptance tests (also known as behat), use [http://www.seleniumhq.org/download/ Selenium server] and can be run as:&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; In single run, only one behat run is executed. So all features are executed in this single run.&lt;br /&gt;
# &#039;&#039;&#039;Parallel runs:&#039;&#039;&#039; (Since Moodle 3.0) Parallel runs allow dev&#039;s to execute multiple behat runs together. This was introduced to get acceptance tests results faster. To achieve this:&lt;br /&gt;
#* Features are divided between multiple behat runs&lt;br /&gt;
#* Symlinks behatrun{x} (x being the run process number), are created pointing to moodle directory, so site for run 1 is accessible via https://localhost/moodle/behatrun1&lt;br /&gt;
#* Process number is included as suffix to $CFG-&amp;gt;behat_prefix.&lt;br /&gt;
#* Process number is suffixed to $CFG-&amp;gt;behat_dataroot.&lt;br /&gt;
&lt;br /&gt;
== Step 1: Initialise acceptance test environment ==&lt;br /&gt;
Before running acceptance tests, environment needs to be initialised for acceptance testing.&lt;br /&gt;
&lt;br /&gt;
=== Single run ===&lt;br /&gt;
For initialising acceptance tests for single run, above command is sufficient.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For initialising acceptance tests for parallel runs, you can use one of the following options&lt;br /&gt;
# &#039;&#039;&#039;-j=&amp;lt;number&amp;gt; or --parallel=&amp;lt;number&amp;gt;&#039;&#039;&#039; (required) Number of parallel behat run to initialise&lt;br /&gt;
# &#039;&#039;&#039;-m=&amp;lt;number&amp;gt; or --maxruns=&amp;lt;number&amp;gt;&#039;&#039;&#039;  (optional) Max parallel site which should be initialised at one time. If your system is slow, then you can initialise sites in chucks.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun=&amp;lt;number&amp;gt;&#039;&#039;&#039; (optional) Initialise site to run specified run from. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;--torun=&amp;lt;number&amp;gt;&#039;&#039;&#039; (optional) Initialise site to run specified run till. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;-o or --optimize-runs&#039;&#039;&#039; (optional) This option will split features with specified tags in all parallel runs, so they are executed first when parallel run gets executed.&lt;br /&gt;
# &#039;&#039;&#039;-a=&amp;lt;name&amp;gt; or --add-core-features-to-theme=&amp;lt;name&amp;gt;&#039;&#039;&#039; (optional) Since Moodle 3.2. Use this option to add all core features to specified themes (comma separated list of themes)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
// Below command will initialise moodle to run 2 parallel tests.&lt;br /&gt;
php admin/tool/behat/cli/init.php --parallel=2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 2: Running acceptance test environment ==&lt;br /&gt;
=== Single run ===&lt;br /&gt;
Run either of the following commands. For more options &#039;&#039;&#039;vendor/bin/behat --help&#039;&#039;&#039; or http://docs.behat.org/guides/6.cli.html&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You almost always want to limit the number of tests that are run. To run all the tests in one plugin:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml --tags mod_myplugin&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To run all the tests in one .feature file:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml /path/to/moodle/mod/myplugin/tests/behat/testsomething.feature&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To run a single scenario:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml /path/to/moodle/mod/myplugin/tests/behat/testsomething.feature:40&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Here, &#039;40&#039; is the line-number of the feature file where the Scenario starts.&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For running parallel runs, use following command&lt;br /&gt;
&lt;br /&gt;
 php admin/tool/behat/cli/run.php&lt;br /&gt;
&lt;br /&gt;
Following optional options are available for custom run:&lt;br /&gt;
# &#039;&#039;&#039;--feature&#039;&#039;&#039; Only execute specified feature file (Absolute path of feature file).&lt;br /&gt;
# &#039;&#039;&#039;--suite&#039;&#039;&#039; Features for specified theme will be executed.&lt;br /&gt;
# &#039;&#039;&#039;--replace&#039;&#039;&#039; Replace args string with run process number, useful for output and reruns.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun&#039;&#039;&#039; Execute run starting from (Used for parallel runs on different vms)&lt;br /&gt;
# &#039;&#039;&#039;--torun&#039;&#039;&#039; Execute run till (Used for parallel runs on different vms)&lt;br /&gt;
# Behat options can be passed for filtering features/scenarios:&lt;br /&gt;
#* In case you don&#039;t want to run Javascript tests, use the Behat tags option to skip them, &#039;&#039;&#039;--tags=&amp;quot;~@javascript&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific scenario, use the Behat name option to run it, &#039;&#039;&#039;--name=&amp;quot;Filter user accounts by role and cohort&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific feature file, use the Behat feature option to run it, &#039;&#039;&#039;--feature=&amp;quot;/PATH/TO/MOODLE/admin/tests/behat/filter_users.feature&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Example: Initialise and run Behat tests for a custom plugin under a custom theme:&lt;br /&gt;
&lt;br /&gt;
 php admin/tool/behat/cli/init.php --parallel=3 --add-core-features-to-theme=&amp;quot;mytheme&amp;quot;&lt;br /&gt;
 php admin/tool/behat/cli/run.php --tags=&amp;quot;@tool_myplugin&amp;quot; --suite=&amp;quot;mytheme&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Common options for running tests ===&lt;br /&gt;
==== Tests filters ====&lt;br /&gt;
With the &#039;&#039;&#039;--tags&#039;&#039;&#039; or the &#039;&#039;&#039;-name&#039;&#039;&#039; Behat options you can filter which tests are going to run or which ones are going to be skipped. There are a few tags that you might be interested in:&lt;br /&gt;
* &#039;&#039;&#039;@javascript&#039;&#039;&#039;: All the tests that runs in a browser using Javascript; they require Selenium to be running, otherwise an exception will be thrown.&lt;br /&gt;
* &#039;&#039;&#039;@_file_upload&#039;&#039;&#039;: All the tests that involves file uploading or any OS feature that is not 100% part of the browser. They should only be executed when Selenium is running in the same machine where the tests are running.&lt;br /&gt;
* &#039;&#039;&#039;@_alert&#039;&#039;&#039;: All the tests that involves Javascript dialogs (alerts, confirms...) are using a feature that is OS-dependant and out of the browser scope, so they should be tag appropriately as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_window&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; window&#039;&#039;&#039; step should be tagged as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_iframe&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; iframe&#039;&#039;&#039; steps should be tagged as it is an advanced feature and some browsers may have problems dealing with them&lt;br /&gt;
* &#039;&#039;&#039;@_cross_browser&#039;&#039;&#039;: All the tests that should run against multiple combinations of browsers + OS in a regular basis. The features that are sensitive to different combinations of OS and browsers should be tagges as @_cross_browser.&lt;br /&gt;
* &#039;&#039;&#039;@componentname&#039;&#039;&#039;: Moodle features uses the [https://docs.moodle.org/dev/Frankenstyle Frankenstyle] component name to tag the features according to the Moodle subsystem they belong to.&lt;br /&gt;
&lt;br /&gt;
==== Output formats ====&lt;br /&gt;
Since Moodle 3.1 option for output is:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=pretty --out=/path/to/pretty.txt --format=moodle_progress --out=std&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Before Moodle 3.1 option for output was:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=&#039;moodle_progress,pretty&#039; --out=&#039;,/path/to/pretty.txt&#039;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Following output formats are supported:&lt;br /&gt;
# &#039;&#039;&#039;progress&#039;&#039;&#039;: Prints one character per step.&lt;br /&gt;
# &#039;&#039;&#039;pretty&#039;&#039;&#039;: Prints the feature as is.&lt;br /&gt;
# &#039;&#039;&#039;junit&#039;&#039;&#039;: Outputs the failures in JUnit compatible files.&lt;br /&gt;
# &#039;&#039;&#039;moodle_progress&#039;&#039;&#039;: Prints Moodle branch information and dots for each step.&lt;br /&gt;
# &#039;&#039;&#039;moodle_list&#039;&#039;&#039;: List all scenarios.&lt;br /&gt;
# &#039;&#039;&#039;moodle_stepcount&#039;&#039;&#039;: List all features with total steps in each feature file. Used for parallel run.&lt;br /&gt;
# &#039;&#039;&#039;moodle_screenshot&#039;&#039;&#039;: (since Moodle 3.1) Take screenshot and core dump of each step. With following options you can dump either or both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;image&amp;quot;}&#039;**: will dump image only&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;}&#039;**: will dump html only.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html,image&amp;quot;}&#039;**: will dump both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;, &amp;quot;dir_permissions&amp;quot;: &amp;quot;0777&amp;quot;}&#039;**&lt;br /&gt;
If you want to see the failures immediately (rather than waiting ~3 hours for all the tests to finish) then either use the -v option to output a bit more information, or change the output format using --format. Format &#039;pretty&#039; (&#039;&#039;&#039;-f pretty&#039;&#039;&#039;) is sufficient for most cases, as it outputs each step outcomes in the command line making easier to see the progress.&lt;br /&gt;
&lt;br /&gt;
== Advance usage ==&lt;br /&gt;
=== Rerun failed scenarios ===&lt;br /&gt;
With slow systems or parallel run you might see some random failures, to rerun only failed scenarios (to eliminate random failures), use --rerun option&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; --run=&amp;quot;absolute_path_to_empty_file&amp;quot; (Behat will record failed scenarios in this file, and when run again only failed scenarios will be run)&lt;br /&gt;
# &#039;&#039;&#039;Parallel run:&#039;&#039;&#039; --rerun=&amp;quot;absolute_path_to_empty_file_{runprocess}.txt --replace=&amp;quot;{runprocess}&amp;quot; ({runprocess} will be replaced with the process number for recording fails in the specific run process).&lt;br /&gt;
&#039;&#039;&#039;Since Moodle 3.1 --rerun option don&#039;t accept any value, as it is handled internally by behat&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Running behat with specified theme (Since Moodle 3.2) ===&lt;br /&gt;
You can run behat with any theme installed. To execute behat with specified theme use &#039;&#039;&#039;--suite={THEME_NAME}&#039;&#039;&#039; option, while running behat. By default the features in theme behat folder will be executed for the suite. But if you want to run all core features with the specific theme then initialise acceptance test with --add-core-features-to-theme={THEME_NAME}, e.g.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php --add-core-features-to-theme=clean&lt;br /&gt;
vendor/bin/behat --suite=clean --tags=&amp;quot;@enrol_foobar&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That is a core theme but it will work with custom theme. No = or quotes needed around the theme name.&lt;br /&gt;
&lt;br /&gt;
Make sure that &amp;lt;tt&amp;gt;$CFG-&amp;gt;theme&amp;lt;/tt&amp;gt; is &#039;&#039;&#039;not set&#039;&#039;&#039; in your config.php.&lt;br /&gt;
&lt;br /&gt;
==== Override behat core context for theme suite ====&lt;br /&gt;
To override behat step definitions so as to run behat with specified theme, you should create a contexts within &#039;&#039;&#039;/theme/{MYTHEME}/tests/behat/&#039;&#039;&#039; with prefix behat_theme_{MYTHEME}_ and suffixed with the context being overridden. For example, if you want to override behat_mod_forum context, then you should create a class /theme/{MYTHEME}/tests/behat/mod_forum/behat_theme_{MYTHEME}_behat_mod_forum.php&lt;br /&gt;
&lt;br /&gt;
==== Blacklist behat context or features to run in theme suite ====&lt;br /&gt;
To blacklist contexts/ features to be executed by theme suite you should create a /theme/{MYTHEME}/tests/behat/blacklist.json file with following format. Following will not use step_definitions from  behat_grade and behat_navigation while running theme suite. Also, scenarios in auth/tests/behat/login.feature and grade/tests/behat/grade_hidden_items.feature won&#039;t be executed with theme suite.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;contexts&amp;quot;: [&lt;br /&gt;
    &amp;quot;behat_grade&amp;quot;,&lt;br /&gt;
    &amp;quot;behat_navigation&amp;quot;,&lt;br /&gt;
  ],&lt;br /&gt;
  &amp;quot;features&amp;quot;: [&lt;br /&gt;
    &amp;quot;auth/tests/behat/login.feature&amp;quot;,&lt;br /&gt;
    &amp;quot;grade/tests/behat/grade_hidden_items.feature&amp;quot;,&lt;br /&gt;
   ]&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
==== Override core behat selectors ====&lt;br /&gt;
To override behat selectors in specific theme, you should create a class behat_theme_{MYTHEME}_behat_selectors in /theme/{MYTHEME}/tests/behat/behat_theme_{MYTHEME}_behat_selectors.php extending behat_selectors.&lt;br /&gt;
&lt;br /&gt;
=== Use php built in web server ===&lt;br /&gt;
You can use php built-in-web server for executing behat runs. To do so:&lt;br /&gt;
# Open a command line interface and &#039;&#039;&#039;cd /to/your/moodle/dirroot&#039;&#039;&#039;&lt;br /&gt;
# &#039;&#039;&#039;php -S localhost:8000&#039;&#039;&#039; (This is the test site URL that moodle uses by default, if you want to use another one you can override it in config.php with $CFG-&amp;gt;behat_wwwroot attribute; more info in https://docs.moodle.org/dev/Acceptance_testing#Advanced_usage or config-dist.php)&lt;br /&gt;
# Update $CFG-&amp;gt;behat_wwwroot = localhost:8000; in config.php&lt;br /&gt;
&lt;br /&gt;
=== Define custom options for parallel runs ===&lt;br /&gt;
You can set following custom config options for parallel runs via $CFG-&amp;gt;behat_parallel_run. It&#039;s an array of options where 1st array is for 1st run and so on.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
       array (&lt;br /&gt;
           &#039;dbtype&#039; =&amp;gt; &#039;mysqli&#039;,&lt;br /&gt;
           &#039;dblibrary&#039; =&amp;gt; &#039;native&#039;,&lt;br /&gt;
           &#039;dbhost&#039; =&amp;gt; &#039;localhost&#039;,&lt;br /&gt;
           &#039;dbname&#039; =&amp;gt; &#039;moodletest&#039;,&lt;br /&gt;
           &#039;dbuser&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;dbpass&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;behat_prefix&#039; =&amp;gt; &#039;mdl_&#039;,&lt;br /&gt;
           &#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;,&lt;br /&gt;
           &#039;behat_wwwroot&#039; =&amp;gt; &#039;http://127.0.0.1/moodle&#039;,&lt;br /&gt;
           &#039;behat_dataroot&#039; =&amp;gt; &#039;/home/example/bht_moodledata&#039;&lt;br /&gt;
       )&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To set different selenium servers for parallel runs, you can use following. NOTE: Running parallel (headless) runs on different selenium servers avoid random focus failures.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
    $CFG-&amp;gt;behat_parallel_run = array (&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4445/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4446/wd/hub&#039;),&lt;br /&gt;
    );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Write new tests and behat methods ===&lt;br /&gt;
&lt;br /&gt;
If you want to write tests for your own integration, you can do so by creating new tests with format .feature. Follow instructions in [[Writing_acceptance_tests|this page]] to write new tests. &lt;br /&gt;
&lt;br /&gt;
It is also possible to add new steps the moodle behat integration. In order to do so, you will have to create a new .php class with the prefix &#039;&#039;&#039;behat_&#039;&#039;&#039;. Copy the format from &#039;&#039;&#039;lib\behat\behat_base.php&#039;&#039;&#039;, but set your class to extend the behat_base class instead of the MinkExtension. You can define new behat steps by declaring functions with the appropriate heading. &lt;br /&gt;
&lt;br /&gt;
You will not be  able to use these steps and features right away. Check [[Running_acceptance_test#New_step_definitions_or_features_are_not_executed|this section]] for instructions on how to update the behat integration. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
For further information on how to create new steps definitions, check [[Acceptance testing/Custom acceptance steps]].&lt;br /&gt;
&lt;br /&gt;
=== Running acceptance tests with different browser ===&lt;br /&gt;
&lt;br /&gt;
If you follow the steps above, Behat will run with Chrome.&lt;br /&gt;
&lt;br /&gt;
You can get it to run with other browsers. The basic idea is to expand the $CFG-&amp;gt;behat_profiles array in config.php to list more browsers.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_profiles = array(&lt;br /&gt;
   &#039;chrome&#039; =&amp;gt; array(&lt;br /&gt;
       &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
       &#039;tags&#039; =&amp;gt; &#039;@javascript&#039;,&lt;br /&gt;
   )&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Acceptance_testing/Browsers|More info about alternative browsers]]&lt;br /&gt;
&lt;br /&gt;
=== Start multiple selenium servers ===&lt;br /&gt;
From command line Start selenium servers at different ports (say 4444, 4445, 4446 for 3 parallel runs)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4444 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4445 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4446&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternative way of running three Selenium servers in parallel:&lt;br /&gt;
&lt;br /&gt;
 $ printf %d\\n {4444..4446} | xargs -n 1 -P 3 java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port&lt;br /&gt;
&lt;br /&gt;
=== Run tests directly in Chrome, with no Selenium ===&lt;br /&gt;
&lt;br /&gt;
Historically, the tests would talk to a Selenium server, which would then tell the target browser what to do. More and more, the browsers themselves contain such a server and you can talk to them directly, which is faster and easier. Work was done to get this working with Chrome in MDL-58948, but it still needs some further setup to get it working, which is detailed in the bug ticket, and which I&#039;ll copy below:&lt;br /&gt;
&lt;br /&gt;
Replace your current behat config with the abve, the api_url is where you&#039;ll talk directly to your Chrome browser.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_config = [&lt;br /&gt;
    &#039;default&#039; =&amp;gt; [&lt;br /&gt;
        &#039;extensions&#039; =&amp;gt; [&lt;br /&gt;
            &#039;DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension&#039; =&amp;gt; [],&lt;br /&gt;
            &#039;Behat\MinkExtension&#039;                                          =&amp;gt; [&lt;br /&gt;
                &#039;browser_name&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
                &#039;base_url&#039;     =&amp;gt; $CFG-&amp;gt;behat_wwwroot,&lt;br /&gt;
                &#039;goutte&#039;       =&amp;gt; null,&lt;br /&gt;
                &#039;selenium2&#039;    =&amp;gt; null,&lt;br /&gt;
                &#039;sessions&#039;     =&amp;gt; [&lt;br /&gt;
                    &#039;javascript&#039; =&amp;gt; [&lt;br /&gt;
                        &#039;chrome&#039; =&amp;gt; [&lt;br /&gt;
                            &#039;api_url&#039; =&amp;gt; &#039;http://localhost:9222&#039;&lt;br /&gt;
                        ]&lt;br /&gt;
                    ]&lt;br /&gt;
                ]&lt;br /&gt;
            ]&lt;br /&gt;
        ]&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Use composer to install two more required libraries:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
composer require --dev dmore/behat-chrome-extension&lt;br /&gt;
composer require --dev dmore/chrome-mink-driver&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you try to run the tests it will tell you that Chrome isn&#039;t running, in a second tab run the following to start Chrome (or chromium-browser on Ubuntu)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;chrome --disable-gpu --headless --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is running headless so you won&#039;t see any windows pop up as the tests run, though it runs in the other mode too.&lt;br /&gt;
&lt;br /&gt;
Behat should now run as before, but faster.&lt;br /&gt;
&lt;br /&gt;
==== Run Chrome locally with Behat and Moodle on a remote server ====&lt;br /&gt;
If you have Moodle running on a remote (headless) server, but don&#039;t want to install a window manager, you can do the following:&lt;br /&gt;
&lt;br /&gt;
1. Go through all the steps from above&lt;br /&gt;
&lt;br /&gt;
2. Establish an SSH SOCKS5 proxy connection to your server:&lt;br /&gt;
&amp;lt;code&amp;gt;ssh -D VARIABLE-PORT-A -N -q -C USER@SERVER&amp;lt;/code&amp;gt;&lt;br /&gt;
3. Forward the port to connect to the DevTools-API of your browser:&lt;br /&gt;
&amp;lt;code&amp;gt;ssh -R VARIABLE-PORT-B:localhost:VARIABLE-PORT-B USER@SERVER -N -q -C&amp;lt;/code&amp;gt;&lt;br /&gt;
4. Tell your local Chrome to use custom settings:&lt;br /&gt;
&amp;lt;code&amp;gt;/path/to/Google\ Chrome [--disable-gpu] --remote-debugging-address=0.0.0.0 --remote-debugging-port=VARIABLE-PORT-B --proxy-server=socks://127.0.0.1:VARIABLE-PORT-A --proxy-bypass-list=&#039;&amp;lt;-loopback&amp;gt;&#039;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Or, all in one command:&lt;br /&gt;
&amp;lt;code&amp;gt;ssh -D VARIABLE-PORT-A -N -q -C -f USER@SERVER &amp;amp;&amp;amp; ssh -R VARIABLE-PORT-B:localhost:VARIABLE-PORT-B USER@SERVER -N -q -C -f &amp;amp;&amp;amp; /path/to/Google\ Chrome [--disable-gpu] --remote-debugging-address=0.0.0.0 --remote-debugging-port=VARIABLE-PORT-B --proxy-server=socks://127.0.0.1:VARIABLE-PORT-A --proxy-bypass-list=&#039;&amp;lt;-loopback&amp;gt;&#039;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Explanation for the SSH settings:&lt;br /&gt;
* VARIABLE-PORT-B &amp;amp; VARIABLE-PORT-A : Choose these as you like. Bear in mind that you will need root rights if the port number is 1023 or lower&lt;br /&gt;
* -N: Tells SSH not to open an actual command prompt&lt;br /&gt;
* -C: Compress all data passed through the tunnel&lt;br /&gt;
* -q: Quiet mode. Causes most warning and diagnostic messages to be suppressed&lt;br /&gt;
*-R: Open a tunnel binding to localhost on the remote machine, going to localhost on the local machine&lt;br /&gt;
* -f: Fork the process into background&lt;br /&gt;
&lt;br /&gt;
Explanation for the Chrome settings:&lt;br /&gt;
* --proxy-bypass-list=&#039;&amp;lt;-loopback&amp;gt;&#039; : Tells Chrome to send requests to localhost through the tunnel&lt;br /&gt;
* --proxy-server=socks://127.0.0.1:VARIABLE-PORT-A : To use the SOCKS5 tunnel we just set up&lt;br /&gt;
* --remote-debugging-port=VARIABLE-PORT-B &amp;amp; --remote-debugging-address=0.0.0.0 : To use the SSH tunnel we just set up&lt;br /&gt;
&lt;br /&gt;
=== Using Docker to start selenium server ===&lt;br /&gt;
==== What is Docker ====&lt;br /&gt;
Docker is a app container,  it&#039;s a kind of virtual machine, but only for one app, service,  so you can download&lt;br /&gt;
a docker image and run a selenium server without worry in how to configure selenium in your machine, one for chrome, others for firefox, you either don&#039;t need to install the browsers in your machine&lt;br /&gt;
To install docker follow this link; https://docs.docker.com/engine/installation/&lt;br /&gt;
&lt;br /&gt;
==== Selenium docker images ====&lt;br /&gt;
There is many docker images available,  for many browser, the complete list is in https://hub.docker.com/u/selenium/&lt;br /&gt;
for moodle you can use standalone version.&lt;br /&gt;
You can download  specific selenium version too,  for example,  for firefox,  moodle recommend selenium 2.53.1, see: [https://docs.moodle.org/dev/Acceptance_testing/Browsers/Working_combinations_of_OS%2BBrowser%2Bselenium What version do I need?]&lt;br /&gt;
&lt;br /&gt;
so  the command will be:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -d -p4444:4444 selenium/standalone-firefox:2.53.1-beryllium&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
to see all available version click in tags.   For firefox you can find at: https://hub.docker.com/r/selenium/standalone-firefox/tags/&lt;br /&gt;
&lt;br /&gt;
==== Change config.php file ====&lt;br /&gt;
In config.php file you must change the $CFG-&amp;gt;behat_wwwroot=   to your network card (NIC) ip address,  you can&#039;t use &lt;br /&gt;
localhost , 127.0.0.1, ...  or selenium docker server  will fail&lt;br /&gt;
&lt;br /&gt;
=== Increasing timeouts ===&lt;br /&gt;
&lt;br /&gt;
You may see errors such as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
Javascript code and/or AJAX requests are not ready after 10 seconds. &lt;br /&gt;
There is a Javascript error or the code is extremely slow.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Sometimes this indicates a genuine problem with the code, but if you are using a slow computer, it could just mean that the browser was not yet ready. You may find that the test works if you run it again. If you get this error frequently, it might be useful to increase the timeout.&lt;br /&gt;
&lt;br /&gt;
It is possible to increase this timeout by adding a line in your config.php. (Requires Moodle versions 3.5 (from 3.5.6), 3.6 (from 3.6.4), or 3.7+.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_increasetimeout = 2;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will increase all the timeouts by a factor of 2; if that isn&#039;t sufficient, you could use 3. &lt;br /&gt;
&lt;br /&gt;
Increasing timeouts will make tests run a bit slower (because there are points where Behat waits up to a timeout to make sure something doesn&#039;t happen) so don&#039;t do this unless you need to.&lt;br /&gt;
&lt;br /&gt;
== NOTE ==&lt;br /&gt;
# Start the Selenium server (in case you want to run tests that involves Javascript)&lt;br /&gt;
#* (See http://www.installationpage.com/selenium/how-to-run-selenium-headless-firefox-in-ubuntu/ for running &#039;headless&#039; Firefox and xvfm in a server environment)&lt;br /&gt;
#* Open another command line interface and &#039;&#039;&#039;java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Disable acceptance test environment ===&lt;br /&gt;
if you want to prevent access to test environment&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --disable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Note that if you have the HTTP_PROXY environment variable set, which you may have had to do to run composer, then you also need to set NO_PROXY=localhost.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Trouble shooting ==&lt;br /&gt;
&lt;br /&gt;
=== New step definitions or features are not executed === &lt;br /&gt;
If you are adding new tests or steps definitions update the tests list&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --enable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039; For parallel runs, all options for initialising parallel runs are valid &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Tests are failing ===&lt;br /&gt;
&lt;br /&gt;
If you followed all the steps and you receive an unknown weird error probably your browser version is not compatible with the Selenium version you are running.  Please refer Working combinations to ensure you have correct [[Acceptance_testing/Browsers#Working_combinations_of_OS.2BBrowser.2Bselenium]] of them to run acceptance test.&lt;br /&gt;
&lt;br /&gt;
=== The tests are failing, and the error message is completely useless ===&lt;br /&gt;
&lt;br /&gt;
For example, it just says &amp;quot;Error writing to database&amp;quot; with no stack trace.&lt;br /&gt;
&lt;br /&gt;
Add -vv command-line option to get very verbose output.&lt;br /&gt;
&lt;br /&gt;
=== Errors during setup (before test are launched) ===&lt;br /&gt;
Typical errors are:&lt;br /&gt;
* Behat requirement not satisfied: http://127.0.0.1/m/stable_master is not available, ensure you specified correct url and that the server is set up and started.&lt;br /&gt;
* Behat is configured but not enabled on this test site.&lt;br /&gt;
&lt;br /&gt;
In order to fix those errors please check that: the behat_dataroot has correct write permissions and that the $CFG-&amp;gt;behat* variables are placed before the lib/setup.php include:&lt;br /&gt;
 require_once(__DIR__ . &#039;/lib/setup.php&#039;);&lt;br /&gt;
&lt;br /&gt;
=== Selenium server is not running ===&lt;br /&gt;
==== Chrome specific ====&lt;br /&gt;
&lt;br /&gt;
If you are using chrome, you need to ensure that the driver matches the version of the installed chrome browser – which may change on OS updates/upgrades.  Moodle or Selenium will not give the appropriate message – see [https://tracker.moodle.org/browse/MDL-67659/ MDL-67659].  One solution is the one suggested in the issue and use Andrew Nicols’ [https://github.com/andrewnicols/chromedriver-wrapper/  Chromedriver Wrapper] which will ensure you have the appropriate driver before running the tests.&lt;br /&gt;
&lt;br /&gt;
== External links ==&lt;br /&gt;
* Vagrant profile with Moodle and Behat preconfigured: https://github.com/mackensen/moodle-hat&lt;br /&gt;
* Docker containers for Moodle Developers and Behat: https://github.com/moodlehq/moodle-docker&lt;br /&gt;
* Docker environment with Behat preconfigured : https://github.com/tobiga/docker_moodle_environment&lt;br /&gt;
&lt;br /&gt;
== See also ==&lt;br /&gt;
* [[Acceptance testing for the mobile app]]&lt;br /&gt;
&lt;br /&gt;
[[Category:Quality Assurance]][[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=57647</id>
		<title>Moodle App Plugins Development Guide</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=57647"/>
		<updated>2020-06-23T11:18:48Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Initialization */ Fix punctuation in javascript example, it should be semicolon&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
==Before 3.5==&lt;br /&gt;
&lt;br /&gt;
Since Moodle 3.1 Moodle plugins could be supported in the Mobile app, but only by writing an Angular JS/Ionic module, compiling it to a zip, and including that in your plugin. See [[Moodle_Mobile_2_(Ionic_1)_Remote_add-ons|Remote add-ons]] for details.&lt;br /&gt;
&lt;br /&gt;
In Moodle 3.5 the app switched to a new way to support plugins that was much easier for developers.&lt;br /&gt;
* This new way will allow developers to support plugins using PHP code, templates and Ionic markup (html components).&lt;br /&gt;
* The use of JavaScript is optional (but some type of advanced plugins may require it)&lt;br /&gt;
* Developers won’t need to set up a Mobile development environment, they will be able to test using the latest version of the official app (although setting up a local Mobile environment is recommended for complex plugins).&lt;br /&gt;
&lt;br /&gt;
This means that remote add-ons won’t be necessary anymore, and developers won’t have to learn Ionic 3 / Angular and set up a new mobile development environment to migrate them. Plugins using the old Remote add-ons mechanism will have to be migrated to the new simpler way (following this documentation)&lt;br /&gt;
&lt;br /&gt;
This new way is natively supported in Moodle 3.5. For previous versions you will need to install the Moodle Mobile Additional Features plugin.&lt;br /&gt;
&lt;br /&gt;
==How it works==&lt;br /&gt;
&lt;br /&gt;
The overall idea is to allow Moodle plugins to extend different areas in the app with &#039;&#039;just PHP server side&#039;&#039; code and Ionic 3 markup (custom html elements that are called components) using a set of custom Ionic directives and components.&lt;br /&gt;
&lt;br /&gt;
Developers will have to:&lt;br /&gt;
# Create a db/mobile.php file in their plugins. In this file developers will be able to indicate which areas of the app they want to extend, for example, adding a new option in the main menu, implementing an activity module not supported, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document.&lt;br /&gt;
# Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered (html). The template should use [https://ionicframework.com/docs/components/ Ionic components] so that it looks native (custom html elements) but it can be generated using mustache templates. &lt;br /&gt;
&lt;br /&gt;
Let’s clarify some points:&lt;br /&gt;
&lt;br /&gt;
* You don’t need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace.&lt;br /&gt;
* Those functions will be exported via the Web Service function tool_mobile_get_content&lt;br /&gt;
* As arguments of your functions you will always receive the userid, some relevant details of the app (app version, current language in the app, etc…) and some specific data depending on the type of plugin (courseid, cmid, …).&lt;br /&gt;
* We provide a list of custom Ionic components and directives (html tags) that will provide dynamic behaviour, like indicating that you are linking a file that can be downloaded, or to allow a transition to new pages into the app calling a specific function in the server, submit form data to the server  etc..&lt;br /&gt;
&lt;br /&gt;
==Types of plugins==&lt;br /&gt;
&lt;br /&gt;
We could classify all the plugins in 3 different types:&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_when_requested.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template of your plugin will be generated and downloaded when the user opens your plugin in the app. This means that your function will receive some context params. For example, if you&#039;re developing a course module plugin you will receive the courseid and the cmid (course module ID). You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_on_login.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template for your plugin will be downloaded when the user logins in the app and will be stored in the device. This means that your function will not receive any context params, and you need to return a generic template that will be built with JS data like the ones in the Mobile app. When the user opens a page that includes your plugin, your template will receive the required JS data and your template will be rendered. You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
You can always implement your whole plugin yourself using Javascript instead of using our API. In fact, this is required if you want to implement some features like capturing links in the Mobile app. You can see the list of delegates that only support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
==Step by step example==&lt;br /&gt;
&lt;br /&gt;
In this example, we are going to update an existing plugin ([https://github.com/markn86/moodle-mod_certificate Certificate activity module]) that currently uses a Remote add-on.&lt;br /&gt;
This is a simple activity module that displays the certificate issued for the current user along with the list of the dates of previously issued certificates. It also stores in the course log that the user viewed a certificate. This module also works offline: when the user downloads the course or activity, the data is pre-fetched and can be viewed offline.&lt;br /&gt;
&lt;br /&gt;
The example code can be downloaded from here (https://github.com/markn86/moodle-mod_certificate/commit/003fbac0d80fd96baf428255500980bf95a7a0d6)&lt;br /&gt;
&lt;br /&gt;
TIP: Make sure to ([https://docs.moodle.org/35/en/Developer_tools#Purge_all_caches purge all cache]) after making an edit to one of the following files for your changes to be taken into account.&lt;br /&gt;
&lt;br /&gt;
===Step 1. Update the db/mobile.php file===&lt;br /&gt;
In this case, we are updating an existing file but for new plugins, you should create this new file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$addons = [&lt;br /&gt;
    &#039;mod_certificate&#039; =&amp;gt; [ // Plugin identifier&lt;br /&gt;
        &#039;handlers&#039; =&amp;gt; [ // Different places where the plugin will display content.&lt;br /&gt;
            &#039;coursecertificate&#039; =&amp;gt; [ // Handler unique name (alphanumeric).&lt;br /&gt;
                &#039;displaydata&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;icon&#039; =&amp;gt; $CFG-&amp;gt;wwwroot . &#039;/mod/certificate/pix/icon.gif&#039;,&lt;br /&gt;
                    &#039;class&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
                ],&lt;br /&gt;
       &lt;br /&gt;
                &#039;delegate&#039; =&amp;gt; &#039;CoreCourseModuleDelegate&#039;, // Delegate (where to display the link to the plugin)&lt;br /&gt;
                &#039;method&#039; =&amp;gt; &#039;mobile_course_view&#039;, // Main function in \mod_certificate\output\mobile&lt;br /&gt;
                &#039;offlinefunctions&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;mobile_course_view&#039; =&amp;gt; [],&lt;br /&gt;
                    &#039;mobile_issues_view&#039; =&amp;gt; [],&lt;br /&gt;
                ], // Function that needs to be downloaded for offline.&lt;br /&gt;
            ],&lt;br /&gt;
        ],&lt;br /&gt;
        &#039;lang&#039; =&amp;gt; [ // Language strings that are used in all the handlers.&lt;br /&gt;
            [&#039;pluginname&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;summaryofattempts&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;getcertificate&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;requiredtimenotmet&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;viewcertificateviews&#039;, &#039;certificate&#039;],&lt;br /&gt;
        ],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Plugin identifier:&lt;br /&gt;
: A unique name for the plugin, it can be anything (there’s no need to match the module name).&lt;br /&gt;
 &lt;br /&gt;
;Handlers  (Different places where the plugin will display content):&lt;br /&gt;
: A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).&lt;br /&gt;
&lt;br /&gt;
; Display data:&lt;br /&gt;
: This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields), in this case we are indicating the module icon.&lt;br /&gt;
	&lt;br /&gt;
; Delegate&lt;br /&gt;
: Where to display the link to the plugin, see the Delegates chapter in this documentation for all the possible options.&lt;br /&gt;
&lt;br /&gt;
; Method:&lt;br /&gt;
: This is the method in the Moodle \(component)\output\mobile class to be executed the first time the user clicks in the new option displayed in the app.	&lt;br /&gt;
&lt;br /&gt;
; Offlinefunctions&lt;br /&gt;
: These are the functions that need to be downloaded for offline usage. This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the mobile.php file. &lt;br /&gt;
: In our example, downloading for offline access will mean that we&#039;ll execute the functions for getting the certificate and issued certificates passing as parameters the current userid (and courseid when we are using the mod or course delegate). If we have the result of those functions stored in the app, we&#039;ll be able to display the certificate information even if the user is offline.&lt;br /&gt;
: Offline functions will be mostly used to display information for final users, any further interaction with the view won’t be supported offline (for example, trying to send information when the user is offline).&lt;br /&gt;
: You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently userid and courseid)&lt;br /&gt;
: Prefetching the module will also download all the files returned by the methods in these offline functions (in the &#039;&#039;files&#039;&#039; array).&lt;br /&gt;
: Note: If your functions use additional custom parameters (for example, if you implement multiple pages within a module&#039;s view function by using a &#039;page&#039; parameter in addition to the usual cmid, courseid, userid) then the app will not know which additional parameters to supply. In this case, do not list the function in offlinefunctions; instead, you will need to manually implement a [[#Module_prefetch_handler|module prefetch handler]].&lt;br /&gt;
&lt;br /&gt;
;Lang:&lt;br /&gt;
: &amp;lt;nowiki&amp;gt;The language pack string ids used in the plugin by all the handlers. Normally these will be strings from your own plugin, however, you can list any strings you need here (e.g. [&#039;cancel&#039;, &#039;moodle&#039;]). If you do this, be warned that in the app you will then need to refer to that string as {{ &#039;plugin.myplugin.cancel&#039; | translate }} (not {{ &#039;plugin.moodle.cancel&#039; | translate }})&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
: Please only include the strings you actually need. The Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform, and this will then be cached, so listing too many strings is very wasteful.&lt;br /&gt;
&lt;br /&gt;
There are additional attributes supported by the mobile.php list, see “Mobile.php supported options” section below.&lt;br /&gt;
&lt;br /&gt;
===Step 2. Creating the main function===&lt;br /&gt;
&lt;br /&gt;
The main function displays the current issued certificate (or several warnings if it’s not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates.&lt;br /&gt;
&lt;br /&gt;
All the functions must be created in the plugin or subsystem classes/output directory, the name of the class must be mobile.&lt;br /&gt;
&lt;br /&gt;
For this example (mod_certificate plugin) the namespace name will be mod_certificate\output.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
namespace mod_certificate\output;&lt;br /&gt;
&lt;br /&gt;
defined(&#039;MOODLE_INTERNAL&#039;) || die();&lt;br /&gt;
&lt;br /&gt;
use context_module;&lt;br /&gt;
use mod_certificate_external;&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Mobile output class for certificate&lt;br /&gt;
 *&lt;br /&gt;
 * @package    mod_certificate&lt;br /&gt;
 * @copyright  2018 Juan Leyva&lt;br /&gt;
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later&lt;br /&gt;
 */&lt;br /&gt;
class mobile {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate course view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_course_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Set timemodified for each certificate.&lt;br /&gt;
        foreach ($issues as $issue) {&lt;br /&gt;
            if (empty($issue-&amp;gt;timemodified)) {&lt;br /&gt;
                    $issue-&amp;gt;timemodified = $issue-&amp;gt;timecreated;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $showget = true;&lt;br /&gt;
        if ($certificate-&amp;gt;requiredtime &amp;amp;&amp;amp; !has_capability(&#039;mod/certificate:manage&#039;, $context)) {&lt;br /&gt;
            if (certificate_get_course_time($certificate-&amp;gt;course) &amp;lt; ($certificate-&amp;gt;requiredtime * 60)) {&lt;br /&gt;
                    $showget = false;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $certificate-&amp;gt;name = format_string($certificate-&amp;gt;name);&lt;br /&gt;
        list($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat) =&lt;br /&gt;
                        external_format_text($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat, $context-&amp;gt;id,&#039;mod_certificate&#039;, &#039;intro&#039;);&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;certificate&#039; =&amp;gt; $certificate,&lt;br /&gt;
            &#039;showget&#039; =&amp;gt; $showget &amp;amp;&amp;amp; count($issues) &amp;gt; 0,&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues,&lt;br /&gt;
            &#039;issue&#039; =&amp;gt; $issues[0],&lt;br /&gt;
            &#039;numissues&#039; =&amp;gt; count($issues),&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;files&#039; =&amp;gt; $issues,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Let’s go through the function code to analyse the different parts.&lt;br /&gt;
&lt;br /&gt;
;Function declaration: &lt;br /&gt;
: The function name is the same as the one used in the mobile.php file (method field). There is only one argument “$args” which is an array containing all the information sent by the mobile app (the courseid, userid, appid, appversionname, appversioncode, applang, appcustomurlscheme…)&lt;br /&gt;
&lt;br /&gt;
; Function implementation:&lt;br /&gt;
: In the first part of the function, we check permissions and capabilities (like a view.php script would do normally). Then we retrieve the certificate information that’s necessary to display the template.&lt;br /&gt;
&lt;br /&gt;
Finally, we return:&lt;br /&gt;
* The rendered template (notice that we could return more than one template but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some Javascript code.&lt;br /&gt;
* JavaScript: Empty, because we don’t need any in this case&lt;br /&gt;
* Other data: Empty as well, because we don’t need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way-data-bind to the template.&lt;br /&gt;
* Files: A list of files that the app should be able to download (for offline usage mostly)&lt;br /&gt;
&lt;br /&gt;
===Step 3. Creating the template for the main function===&lt;br /&gt;
&lt;br /&gt;
This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.&lt;br /&gt;
&lt;br /&gt;
In this template we’ll be using Ionic and custom directives and components available in the Mobile app.&lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with ion- are ionic components. Most of the time the component name is self-explanatory but you may refer to a detailed guide here: https://ionicframework.com/docs/components/ &lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with &#039;&#039;core-&#039;&#039; are custom components of the Mobile app.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_page.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;core-course-module-description description=&amp;quot;&amp;lt;% certificate.intro %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-course-module-description&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;ion-list-header&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-list-header&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
                    {{ &#039;plugin.mod_certificate.viewcertificateviews&#039; | translate: {$a: &amp;lt;% numissues %&amp;gt;} }}&lt;br /&gt;
                &amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;ion-icon name=&amp;quot;cloud-download&amp;quot; item-start&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
                {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
            &amp;lt;/button&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%^showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;p&amp;gt;{{ &#039;plugin.mod_certificate.requiredtimenotmet&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;!-- Call log WS when the template is loaded. --&amp;gt;&lt;br /&gt;
        &amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Then we display the module description using &amp;lt;code&amp;gt;&amp;lt;core-course-module-description&amp;lt;/code&amp;gt; that is a component used to include the course module description.&lt;br /&gt;
&lt;br /&gt;
For displaying the certificate information we create a list of elements, adding a header on top.&lt;br /&gt;
The following line &amp;lt;code&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/code&amp;gt; indicates that the Mobile app will translate the &#039;&#039;summaryofattempts&#039;&#039; string id (here we could’ve used mustache translation but it is usually better to delegate the strings translations to the app). The string id has this format: &lt;br /&gt;
&lt;br /&gt;
“plugin” + plugin identifier (from mobile.php) +  string id (the string must be indicated in the lang field in mobile.php). &lt;br /&gt;
&lt;br /&gt;
Then we display a button to transition to another page if there are certificates issued. The attribute (directive) &amp;lt;code&amp;gt;core-site-plugins-new-content&amp;lt;/code&amp;gt; indicates that if the user clicks the button, we need to call the function “mobile_issues_view” in the component “mod_certificate” passing as arguments the cmid and courseid. The content returned by this function will be displayed in a new page (see Step 4 for the code of this new page).&lt;br /&gt;
&lt;br /&gt;
Just after this button we display another one but this time for downloading an issued certificate. The &amp;lt;code&amp;gt;core-course-download-module-main-file&amp;lt;/code&amp;gt; directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available in offline.&lt;br /&gt;
&lt;br /&gt;
Finally, just before the ion-list is closed, we use the &amp;lt;code&amp;gt;core-site-plugins-call-ws-on-load&amp;lt;/code&amp;gt; directive to indicate that once the page is loaded, we need to call to a Web Service function in the server, in this case we are calling the &#039;&#039;mod_certificate_view_certificate&#039;&#039; that will log that the user viewed this page.&lt;br /&gt;
&lt;br /&gt;
As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes.&lt;br /&gt;
&lt;br /&gt;
===Step 4. Adding an additional page===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Partial file contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate issues view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_issues_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = [&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_issues&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This function for the new page was added just after the mobile_course_view function, the code is quite similar: Capabilities checks, retrieves the information required for the template and returns the template rendered.&lt;br /&gt;
&lt;br /&gt;
The code of the mustache template is also very simple:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_issues.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &amp;lt;%timecreated%&amp;gt; | coreToLocaleString }}&amp;lt;/p&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;&amp;lt;%grade%&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Here we are creating an ionic list that will display a new item in the list per each issued certificated.&lt;br /&gt;
&lt;br /&gt;
For the issued certificated we’ll display the time when it was created (using the app filter &#039;&#039;coreToLocaleString&#039;&#039;). We are also displaying the grade displayed in the certificate (if any).&lt;br /&gt;
&lt;br /&gt;
===Step 5. Plugin webservices, if included===&lt;br /&gt;
&lt;br /&gt;
If your plugin uses its own web services, they will also need to be enabled for mobile access in your db/services.php file.&lt;br /&gt;
&lt;br /&gt;
The following line &amp;lt;code&amp;gt;&#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&amp;lt;/code&amp;gt; should be included in each webservice definition.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/db/services.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$functions = [&lt;br /&gt;
&lt;br /&gt;
    &#039;mod_certificate_get_certificates_by_courses&#039; =&amp;gt; [&lt;br /&gt;
        &#039;classname&#039;     =&amp;gt; &#039;mod_certificate_external&#039;,&lt;br /&gt;
        &#039;methodname&#039;    =&amp;gt; &#039;get_certificates_by_courses&#039;,&lt;br /&gt;
        &#039;description&#039;   =&amp;gt; &#039;Returns a list of certificate instances...&#039;,&lt;br /&gt;
        &#039;type&#039;          =&amp;gt; &#039;read&#039;,&lt;br /&gt;
        &#039;capabilities&#039;  =&amp;gt; &#039;mod/certificate:view&#039;,&lt;br /&gt;
        &#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
	&lt;br /&gt;
This extra services definition is the reason why you will need to have the local_mobile plugin installed for Moodle versions 3.4 and lower, so that your Moodle site will have all the additional webservices included to deal with all these mobile access calls. This is explained further in the [https://docs.moodle.org/dev/Mobile_support_for_plugins#Moodle_version_requirements Moodle version requirements section] below.&lt;br /&gt;
&lt;br /&gt;
==Getting started==&lt;br /&gt;
&lt;br /&gt;
The first and most important thing to know is that you don’t need a local mobile environment, you can just use the Chrome or Chromium browser to add mobile support to your plugins!&lt;br /&gt;
&lt;br /&gt;
Open this URL (with Chrome or Chromium browser): https://mobileapp.moodledemo.net/ and you will see a web version of the mobile app completely functional (except for some native features). This URL is updated with the latest integration version of the app. Alternatively, you can use the Moodle Desktop app, it is based on Chromium so you can enable the &amp;quot;Developer Tools&amp;quot; and inspect the HTML, inject javascript, debug, etc...&lt;br /&gt;
&lt;br /&gt;
Please test that your site works correctly in the web version before starting any development.&lt;br /&gt;
&lt;br /&gt;
===Moodle version requirements===&lt;br /&gt;
&lt;br /&gt;
If your Moodle version is lower than 3.5 you will need to install the [https://docs.moodle.org/en/Moodle_Mobile_additional_features Moodle Mobile additional features plugin]. &lt;br /&gt;
&lt;br /&gt;
Please use this development version for now: https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE (if your Moodle version is 3.2, 3.3 or 3.4) you will have to use the specific branch for your version but applying manually the [https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE last commit from the 3.1 branch] (the one with number MOBILE-2362).&lt;br /&gt;
&lt;br /&gt;
Also, when installing the Moodle Mobile Additional features plugin you must follow the installation instructions so the service is set up properly.&lt;br /&gt;
&lt;br /&gt;
Remember to update your plugin documentation to reflect that this plugin is mandatory for Mobile support. We don’t recommend to indicate in your plugin version.php a dependency to local_mobile though.&lt;br /&gt;
&lt;br /&gt;
===Development workflow===&lt;br /&gt;
&lt;br /&gt;
First of all, we recommend creating a simple &#039;&#039;mobile.php&#039;&#039; for displaying a new main menu option (even if your plugin won’t be in the main menu, just to verify that you are able to extend the app plugins). Then open the webapp (https://mobileapp.moodledemo.net/) or refresh the browser if it was already open. Alternatively, you can use the Moodle Desktop app, it is based on Chromium so you can enable the &amp;quot;Developer Tools&amp;quot; and inspect the HTML, inject javascript, debug, etc...&lt;br /&gt;
&lt;br /&gt;
Check that you can correctly  see the new menu option you included.&lt;br /&gt;
&lt;br /&gt;
Then, develop the main function of the app returning a “Hello world” or basic code (without using templates) to see that everything works together. After adding the classes/output/mobile.php file it is very important to “Purge all caches” to avoid problems with the auto-loading cache.&lt;br /&gt;
&lt;br /&gt;
It is important to remember that:&lt;br /&gt;
* Any change in the mobile.php file will require you to refresh the web app page in the browser (remember to disable the cache in the Chrome developer options).&lt;br /&gt;
* Any change in an existing template or function won’t require to refresh the browser page. In most cases you should just do a PTR (Pull down To Refresh) in the page that displays the view returned by the function. Be aware that PTR will work only when using the “device” emulation in the browser (see following section).&lt;br /&gt;
&lt;br /&gt;
===Testing and debugging===&lt;br /&gt;
&lt;br /&gt;
To learn how to debug with the web version of the app, please read the following documents:&lt;br /&gt;
* [[Moodle Mobile debugging WS requests]] AND&lt;br /&gt;
* [[Moodle Mobile development using Chrome or Chromium]] (please, omit the installation section)&lt;br /&gt;
&lt;br /&gt;
For plugins using the Javascript API you may develop making use of the console.log function to add trace messages in your code that will be displayed in the browser console.&lt;br /&gt;
&lt;br /&gt;
Within the app, make sure to turn on the option: &#039;&#039;&#039;App settings&#039;&#039;&#039; / &#039;&#039;&#039;General&#039;&#039;&#039; / &#039;&#039;&#039;Display debug messages&#039;&#039;&#039;. This means popup errors from the app will show more information.&lt;br /&gt;
&lt;br /&gt;
==Mobile.php supported options==&lt;br /&gt;
&lt;br /&gt;
In the Step by Step section we learned about some of the existing options for handlers configuration. This is the full list of supported options:&lt;br /&gt;
&lt;br /&gt;
===Common options===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;delegate&#039;&#039;&#039; (mandatory): Name of the delegate to register the handler in.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (mandatory): The function to call to retrieve the main page content.&lt;br /&gt;
* &#039;&#039;&#039;init&#039;&#039;&#039; (optional): A function to call to retrieve the initialization JS and the &amp;quot;restrict&amp;quot; to apply to the whole handler. It can also return templates that can be used from the Javascript of the init method or the Javascript of the handler’s method.&lt;br /&gt;
* &#039;&#039;&#039;restricttocurrentuser&#039;&#039;&#039; (optional) Only used if the delegate has a isEnabledForUser function. If true, the handler will only be shown for current user. For more info about displaying the plugin only for certain users, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;restricttoenrolledcourses&#039;&#039;&#039; (optional): Only used if the delegate has a isEnabledForCourse function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;styles&#039;&#039;&#039; (optional): An array with two properties: &#039;&#039;url&#039;&#039; and &#039;&#039;version&#039;&#039;. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It&#039;s recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number everytime you change the CSS file.&lt;br /&gt;
* &#039;&#039;&#039;moodlecomponent&#039;&#039;&#039; (optional): If your plugin supports a component in the app different than the one defined by your plugin, you can use this property to specify it. For example, you can create a local plugin to support a certain course format, activity, etc. The component of your plugin in Moodle would be &#039;&#039;local_whatever&#039;&#039;, but in &amp;quot;moodlecomponent&amp;quot; you can specify that this handler will implement &#039;&#039;format_whatever&#039;&#039; or &#039;&#039;mod_whatever&#039;&#039;. This property was introduced in the version 3.6.1 of the app.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseOptionsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first.&lt;br /&gt;
* &#039;&#039;&#039;ismenuhandler&#039;&#039;&#039;: (optional) Supported from the 3.7.1 version of the app. Set it to true if you want your plugin to be displayed in the contextual menu of the course instead of in the top tabs. The contextual menu is displayed when you click in the 3-dots button at the top right of the course.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreMainMenuDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the &amp;quot;More&amp;quot; tab, they cannot be displayed as tabs in the bottom bar.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseModuleDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): icon, class.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (optional): The function to call to retrieve the main page content. In this delegate the method is optional. If the method is not set, the module won&#039;t be clickable.&lt;br /&gt;
* &#039;&#039;&#039;offlinefunctions&#039;&#039;&#039;: (optional) List of functions to call when prefetching the module. It can be a get_content method or a WS. You can filter the params received by the WS. By default, WS will receive these params: courseid, cmid, userid. Other valid values that will be added if they are present in the list of params: courseids (it will receive a list with the courses the user is enrolled in), component + &#039;id&#039; (e.g. certificateid).&lt;br /&gt;
* &#039;&#039;&#039;downloadbutton&#039;&#039;&#039;: (optional) Whether to display download button in the module. If not defined, the button will be shown if there is any offlinefunction.&lt;br /&gt;
* &#039;&#039;&#039;isresource&#039;&#039;&#039;: (optional) Whether the module is a resource or an activity. Only used if there is any offlinefunction. If your module relies on the &amp;quot;contents&amp;quot; field, then it should be true.&lt;br /&gt;
* &#039;&#039;&#039;updatesnames&#039;&#039;&#039;: (optional) Only used if there is any offlinefunction. A Regular Expression to check if there&#039;s any update in the module. It will be compared to the result of &#039;&#039;core_course_check_updates&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;displayopeninbrowser&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Open in browser&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayOpenInBrowser = false;&lt;br /&gt;
* &#039;&#039;&#039;displaydescription&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Description&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayDescription = false;&lt;br /&gt;
* &#039;&#039;&#039;displayrefresh&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Refresh&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayRefresh = false;&lt;br /&gt;
* &#039;&#039;&#039;displayprefetch&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: this.displayPrefetch = false;&lt;br /&gt;
* &#039;&#039;&#039;displaysize&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: this.displaySize = false;&lt;br /&gt;
* &#039;&#039;&#039;coursepagemethod&#039;&#039;&#039;: (optional) Supported from the 3.8 version of the app. If set, this method will be called when the course is rendered and the HTML returned will be displayed in the course page for the module. Please notice the HTML returned should not contain directives or components, only default HTML.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseFormatDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;canviewallsections&#039;&#039;&#039;: (optional) Whether the course format allows seeing all sections in a single page. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displayenabledownload&#039;&#039;&#039;: (optional) Whether the option to enable section/module download should be displayed. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displaysectionselector&#039;&#039;&#039;: (optional) Whether the default section selector should be displayed. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreUserDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;type&#039;&#039;&#039;: The type of the addon. Values accepted: &#039;newpage&#039; (default) or  &#039;communication&#039;. &lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreSettingsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for AddonMessageOutputDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreBlockDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (optional): title, class, type. If &#039;&#039;title&#039;&#039; is not supplied, it will default to &amp;quot;plugins.block_blockname.pluginname&amp;quot;, where &#039;&#039;blockname&#039;&#039; is the name of the block. If &#039;&#039;class&#039;&#039; is not supplied, it will default to &amp;quot;block_blockname&amp;quot;, where &#039;&#039;blockname&#039;&#039; is the name of the block. Possible values of &#039;&#039;type&#039;&#039;:&lt;br /&gt;
** &amp;quot;title&amp;quot;: Your block will only display the block title, and when it&#039;s clicked it will open a new page to display the block contents (the template returned by the block&#039;s method).&lt;br /&gt;
** &amp;quot;prerendered&amp;quot;: Your block will display the content and footer returned by the WebService to get the blocks (e.g. core_block_get_course_blocks), so your block&#039;s method will never be called.&lt;br /&gt;
** any other value: Your block will immediately call the method specified in mobile.php and it will use the template to render the block.&lt;br /&gt;
&lt;br /&gt;
==Delegates==&lt;br /&gt;
&lt;br /&gt;
The delegates can be classified by type of plugin. For more info about type of plugins, please see the See [[Mobile_support_for_plugins#Types_of_plugins|Types of plugins]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
====CoreMainMenuDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app). &lt;br /&gt;
&lt;br /&gt;
====CoreCourseOptionsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate).&lt;br /&gt;
&lt;br /&gt;
====CoreCourseModuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting activity modules or resources.&lt;br /&gt;
&lt;br /&gt;
====CoreUserDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add additional options in the user profile page in the app.&lt;br /&gt;
&lt;br /&gt;
====CoreCourseFormatDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting course formats.  When you open a course from the course list in the mobile app, it will check if there is a CoreCourseFormatDelegate handler for the format that site uses.  If so, it will display the course using that handler.  Otherwise, it will use the default app course format.  More information is available on [[Creating mobile course formats]].&lt;br /&gt;
&lt;br /&gt;
====CoreSettingsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to add a new option in the settings page.&lt;br /&gt;
&lt;br /&gt;
====AddonMessageOutputDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a message output plugin.&lt;br /&gt;
&lt;br /&gt;
====CoreBlockDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a block. As of Moodle App 3.7.0, blocks are only displayed in Site Home and Dashboard, but they&#039;ll be supported in other places of the app soon (e.g. in the course page).&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question types.&lt;br /&gt;
https://docs.moodle.org/dev/Creating_mobile_question_types&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionBehaviourDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question behaviours.&lt;br /&gt;
&lt;br /&gt;
====CoreUserProfileFieldDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting user profile fields.&lt;br /&gt;
&lt;br /&gt;
====AddonModQuizAccessRuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a quiz access rule.&lt;br /&gt;
&lt;br /&gt;
====AddonModAssignSubmissionDelegate and AddonModAssignFeedbackDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use these delegates to support assign submission or feedback plugins.&lt;br /&gt;
&lt;br /&gt;
====AddonWorkshopAssessmentStrategyDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a workshop assessment strategy plugin.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
These delegates require JavaScript to be supported. See [[Mobile_support_for_plugins#Initialization|Initialization]] for more information.&lt;br /&gt;
&lt;br /&gt;
* CoreContentLinksDelegate&lt;br /&gt;
* CoreCourseModulePrefetchDelegate&lt;br /&gt;
* CoreFileUploaderDelegate&lt;br /&gt;
* CorePluginFileDelegate&lt;br /&gt;
* CoreFilterDelegate&lt;br /&gt;
&lt;br /&gt;
==Available components and directives==&lt;br /&gt;
&lt;br /&gt;
===Difference between component and directives===&lt;br /&gt;
&lt;br /&gt;
A component (represented as an HTML tag) is used to add custom elements to the app.&lt;br /&gt;
Example of components are: ion-list, ion-item, core-search-box&lt;br /&gt;
&lt;br /&gt;
A directive (represented as an HTML attribute) allows you to extend a piece of HTML with additional information or functionality.&lt;br /&gt;
Example of directives are: core-auto-focus, *ngIf, ng-repeat&lt;br /&gt;
&lt;br /&gt;
The Mobile app uses Angular, Ionic and custom components and directives, for a full reference of:&lt;br /&gt;
* Angular directives, please check: https://angular.io/api?type=directive&lt;br /&gt;
* Ionic components, please check: https://ionicframework.com/docs/&lt;br /&gt;
&lt;br /&gt;
===Custom core components and directives===&lt;br /&gt;
&lt;br /&gt;
These are some useful custom components and directives (only available in the mobile app). Please note that this isn’t the full list of components and directives of the app, it’s just an extract of the most common ones.&lt;br /&gt;
&lt;br /&gt;
For a full list of components, go to https://github.com/moodlehq/moodlemobile2/tree/master/src/components&lt;br /&gt;
&lt;br /&gt;
For a full list of directives, go to https://github.com/moodlehq/moodlemobile2/tree/master/src/directives&lt;br /&gt;
&lt;br /&gt;
====core-format-text====&lt;br /&gt;
&lt;br /&gt;
This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive.&lt;br /&gt;
&lt;br /&gt;
This directive automatically applies core-external-content and core-link to all the links and embedded media.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;text&#039;&#039;&#039; (string): The text to format.&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* &#039;&#039;&#039;adaptImg&#039;&#039;&#039; (boolean): Optional. Whether to adapt images to screen width. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;clean&#039;&#039;&#039; (boolean): Optional. Whether all the HTML tags should be removed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;singleLine&#039;&#039;&#039; (boolean): Optional. Whether new lines should be removed (all text in single line). Only if clean=true. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;maxHeight&#039;&#039;&#039; (number): Optional. Max height in pixels to render the content box. It should be 50 at least to make sense. Using this parameter will force display: block to calculate height better. If you want to avoid this use class=&amp;quot;inline&amp;quot; at the same time to use display: inline-block.&lt;br /&gt;
* &#039;&#039;&#039;fullOnClick&#039;&#039;&#039; (boolean): Optional. Whether it should open a new page with the full contents on click. Only if maxHeight is set and the content has been collapsed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;fullTitle&#039;&#039;&#039; (string): Optional. Title to use in full view. Defaults to &amp;quot;Description&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-format-text text=&amp;quot;&amp;lt;% cm.description %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-format-text&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-link====&lt;br /&gt;
&lt;br /&gt;
Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app).&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;capture&#039;&#039;&#039; (boolean): Optional, default false. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser).&lt;br /&gt;
* &#039;&#039;&#039;inApp&#039;&#039;&#039; (boolean): Optional, default false. True to open in embedded browser, false to open in system browser.&lt;br /&gt;
* &#039;&#039;&#039;autoLogin&#039;&#039;&#039; (string): Optional, default &amp;quot;check&amp;quot;. If the link should be open with auto-login. Accepts the following values:&lt;br /&gt;
** &amp;quot;yes&amp;quot; -&amp;gt; Always auto-login.&lt;br /&gt;
** &amp;quot;no&amp;quot; -&amp;gt; Never auto-login.&lt;br /&gt;
** &amp;quot;check&amp;quot; -&amp;gt; Auto-login only if it points to the current site. Default value.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a href=&amp;quot;&amp;lt;% cm.url %&amp;gt;&amp;quot; core-link&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-external-content====&lt;br /&gt;
&lt;br /&gt;
Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline. &lt;br /&gt;
&lt;br /&gt;
If a file is downloaded, its URL will be replaced by the local file URL.&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;img src=&amp;quot;&amp;lt;% event.iconurl %&amp;gt;&amp;quot; core-external-content component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% event.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-user-link====&lt;br /&gt;
&lt;br /&gt;
Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;userId&#039;&#039;&#039; (number): User id to open the profile.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): Optional. Course id to show the user info related to that course.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a ion-item core-user-link userId=&amp;quot;&amp;lt;% userid %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-file====&lt;br /&gt;
&lt;br /&gt;
Component to handle a remote file. It shows the file name, icon (depending on mimetype) and a button to download/refresh it. The user can identify if the file is downloaded or not based on the button.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* file (object): The file. Must have a property &#039;filename&#039; and a &#039;fileurl&#039; or &#039;url&#039;&lt;br /&gt;
* component (string): Optional. Component the file belongs to.&lt;br /&gt;
* componentId (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* canDelete (boolean): Optional. Whether file can be deleted.&lt;br /&gt;
* alwaysDownload (boolean): Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they&#039;re outdated or not.&lt;br /&gt;
* canDownload (boolean): Optional. Whether file can be downloaded. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-file [file]=&amp;quot;{fileurl: &#039;&amp;lt;% issue.url %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.name %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, filesize: &#039;&amp;lt;% issue.size %&amp;gt;&#039;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-file&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-download-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened.&lt;br /&gt;
&lt;br /&gt;
It is usually recommended to use the core-file component since it also displays the state of the file.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;core-download-file&#039;&#039;&#039; (object): The file to download.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage: a button to download a file.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button [core-download-file]=&amp;quot;{fileurl: &amp;lt;% issue.url %&amp;gt;, timemodified: &amp;lt;% issue.timemodified %&amp;gt;, filesize: &amp;lt;% issue.size %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.download | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-course-download-module-main-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and opening the main file of a module.&lt;br /&gt;
&lt;br /&gt;
When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like mod_resource.&lt;br /&gt;
&lt;br /&gt;
This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;module&#039;&#039;&#039; (object): Optional. The module object. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;moduleId&#039;&#039;&#039; (number): Optional. The module ID. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): The course ID the module belongs to.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component. If not defined, moduleId.&lt;br /&gt;
* &#039;&#039;&#039;files&#039;&#039;&#039; (object[]): Optional. List of files of the module. If not provided, use module.contents.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-navbar-buttons====&lt;br /&gt;
&lt;br /&gt;
Component to add buttons to the app&#039;s header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page.&lt;br /&gt;
&lt;br /&gt;
If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first &amp;lt;ion-buttons&amp;gt; found in the header.&lt;br /&gt;
&lt;br /&gt;
You can use the [hidden] input to hide all the inner buttons if a certain condition is met.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons end&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button icon-only (click)=&amp;quot;action()&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;ion-icon name=&amp;quot;funnel&amp;quot;&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can also use this to add options to the context menu. Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons&amp;gt;&lt;br /&gt;
    &amp;lt;core-context-menu&amp;gt;&lt;br /&gt;
        &amp;lt;core-context-menu-item [priority]=&amp;quot;500&amp;quot; [content]=&amp;quot;&#039;Nice boat&#039;&amp;quot; (action)=&amp;quot;boatFunction()&amp;quot; [iconAction]=&amp;quot;&#039;boat&#039;&amp;quot;&amp;gt;&amp;lt;/core-context-menu-item&amp;gt;&lt;br /&gt;
    &amp;lt;/core-context-menu&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that it is not currently possible to remove or modify options from the context menu without using a nasty hack.&lt;br /&gt;
&lt;br /&gt;
===Specific component and directives for plugins===&lt;br /&gt;
&lt;br /&gt;
These are component and directives created specifically for supporting Moodle plugins.&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. If not supplied, no other data will be added. If supplied but empty (&#039;&#039;&#039;null, false or empty string&#039;&#039;&#039;) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherData]=&amp;quot;&amp;quot; is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the new &#039;&#039;get_content&#039;&#039; WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to go to a new content page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to load new content in current page using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view.&lt;br /&gt;
&lt;br /&gt;
If you want to load a new content when the WS call is done, please see core-site-plugins-call-ws-new-content.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (&#039;&#039;&#039;null, false or empty string&#039;&#039;&#039;) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]=&amp;quot;&amp;quot; is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;successMessage&#039;&#039;&#039; (string): Message to show on success. If not supplied, no message. If supplied but empty, default message (“Success”).&lt;br /&gt;
* &#039;&#039;&#039;goBackOnSuccess&#039;&#039;&#039; (boolean): Whether to go back if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;refreshOnSuccess&#039;&#039;&#039; (boolean): Whether to refresh the current view if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server without using cache, displaying default messages and refreshing on success:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage successMessage refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server using cache without confirming, going back on success and using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; goBackOnSuccess=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;certificateViewed($event)&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.certificateViewed = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked and load a new content passing the WS result as args. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t need to load some new content when done, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (&#039;&#039;&#039;null, false or empty string&#039;&#039;&#039;) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]=&amp;quot;&amp;quot; is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;jsData&#039;&#039;&#039; (any): JS variables to pass to the new page so they can be used in the template or JS. If true is supplied instead of an object, all initial variables from current page will be copied. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;newContentPreSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server without using cache, showing default confirm and displaying a new page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server using cache, without confirm, displaying new content in same page and using &#039;&#039;userid&#039;&#039; from &#039;&#039;otherdata&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-on-load====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services.&lt;br /&gt;
&lt;br /&gt;
If you want to call a WS when the user clicks on a certain element, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Note that this will cause an error to appear on each page load if the user is offline in v3.5.1 and older, the bug was fixed in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (&#039;&#039;&#039;null, false or empty string&#039;&#039;&#039;) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array. Please notice that [useOtherDataForWS]=&amp;quot;&amp;quot; is the same as not supplying it, so nothing will be copied. Also, objects or arrays in otherdata will be converted to a JSON encoded string.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Advanced features==&lt;br /&gt;
&lt;br /&gt;
===Display the plugin only if certain conditions are met===&lt;br /&gt;
&lt;br /&gt;
You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the &amp;quot;init&amp;quot; method (for more info, please see the [[Mobile_support_for_plugins#Initialization|Initialization]] section ahead).&lt;br /&gt;
&lt;br /&gt;
All the init methods are called as soon as your plugin is retrieved. If you don&#039;t want your plugin to be displayed for the current user, then you should return this in the init method (only for Moodle 3.8 and onwards).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
return [&lt;br /&gt;
    &#039;disabled&#039; =&amp;gt; true&lt;br /&gt;
];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If the Moodle version is older than 3.8, then the init method should return this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
return [&lt;br /&gt;
    &#039;javascript&#039; =&amp;gt; &#039;this.HANDLER_DISABLED&#039;&lt;br /&gt;
];&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
On the other hand, you might want to display a plugin only for certain courses (&#039;&#039;CoreCourseOptionsDelegate&#039;&#039;) or only if the user is viewing certain users&#039; profiles (&#039;&#039;CoreUserDelegate&#039;&#039;). This can be achieved with the init method too.&lt;br /&gt;
&lt;br /&gt;
In the init method you can return a &amp;quot;restrict&amp;quot; property with two fields in it: &#039;&#039;courses&#039;&#039; and &#039;&#039;users&#039;&#039;. If you return a list of courses IDs in this restrict property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users&#039; profiles.&lt;br /&gt;
&lt;br /&gt;
===Using “otherdata”===&lt;br /&gt;
&lt;br /&gt;
The values returned by the functions in otherdata are added to a variable so they can be used both in Javascript and in templates. The otherdata returned by a init call is added to a variable named INIT_OTHERDATA, while the otherdata returned by a &#039;&#039;get_content&#039;&#039; WS call is added to a variable named CONTENT_OTHERDATA.&lt;br /&gt;
&lt;br /&gt;
The otherdata returned by a init call will be passed to the JS and template of all the get_content calls in that handler. The otherdata returned by a get_content call will only be passed to the JS and template returned by that get_content call.&lt;br /&gt;
&lt;br /&gt;
This means that, in your Javascript, you can access and use these data like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
this.CONTENT_OTHERDATA.myVar&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
And in the template you could use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
{{ CONTENT_OTHERDATA.myVar }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;myVar&#039;&#039; is the name we put to one of our variables, it can be the name you want. In the example above, this is the otherdata returned by the PHP method:&lt;br /&gt;
&lt;br /&gt;
array(&#039;myVar&#039; =&amp;gt; &#039;Initial value&#039;)&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
In our plugin we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain WebService. This can be done using otherdata.&lt;br /&gt;
&lt;br /&gt;
We will return the initial value of the input in the otherdata of our PHP method:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;myVar&#039; =&amp;gt; &#039;My initial value&#039;),&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then in the template we will use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-item text-wrap&amp;gt;&lt;br /&gt;
    &amp;lt;ion-label stacked&amp;gt;{{ &#039;plugin.mod_certificate.textlabel | translate }}&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;text&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.myVar&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [useOtherDataForWS]=&amp;quot;[&#039;myVar&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
        {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we are creating an input text and we use &#039;&#039;[(ngModel)]&#039;&#039; to use the value in &#039;&#039;myVar&#039;&#039; as the initial value and to store the changes in the same &#039;&#039;myVar&#039;&#039; variable. This means that the initial value of the input will be “My initial value”, and if the user changes the value of the input these changes will be applied to the &#039;&#039;myVar&#039;&#039; variable. This is called 2-way data binding in Angular.&lt;br /&gt;
&lt;br /&gt;
Then we add a button to send this data to a WS, and for that we use the directive core-site-plugins-call-ws. We use the &#039;&#039;useOtherDataForWS&#039;&#039; attribute to specify which variable from &#039;&#039;otherdata&#039;&#039; we want to send to our WebService. So if the user enters “A new value” in the input and then clicks the button, it will call the WebService &#039;&#039;mod_certificate_my_webservice&#039;&#039; and will send as a param: myVar -&amp;gt; “A new value”.&lt;br /&gt;
&lt;br /&gt;
We can achieve the same result using the &#039;&#039;params&#039;&#039; attribute of the core-site-plugins-call-ws directive instead of using &#039;&#039;useOtherDataForWS&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [params]=&amp;quot;{myVar: CONTENT_OTHERDATA.myVar}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
The WebService call will be exactly the same with both buttons.&lt;br /&gt;
&lt;br /&gt;
Please notice that this example could be done without using otherdata too, using the “&#039;&#039;form&#039;&#039;” input of the &#039;&#039;core-site-plugins-call-ws directive&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
===Running JS code after a content template has loaded===&lt;br /&gt;
&lt;br /&gt;
When you return JavaScript code from a handler function using the &#039;javascript&#039; array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM. &lt;br /&gt;
&lt;br /&gt;
If your code needs to run after the DOM has been updated, you can use setTimeout to call it. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
return [&lt;br /&gt;
    &#039;template&#039; =&amp;gt; [ ... ],&lt;br /&gt;
    &#039;javascript&#039; =&amp;gt; &#039;setTimeout(function() { console.log(&amp;quot;DOM is available now&amp;quot;); });&#039;,&lt;br /&gt;
    &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
    &#039;files&#039; =&amp;gt; []&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Note: If you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an init template, so that it does not get loaded again with each page of content.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
===JS functions visible in the templates===&lt;br /&gt;
&lt;br /&gt;
The app provides some Javascript functions that can be used from the templates to update, refresh or view content. These are the functions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;openContent(title: string, args: any, component?: string, method?: string)&#039;&#039;&#039;: Open a new page to display some new content. You need to specify the &#039;&#039;title&#039;&#039; of the new page and the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
* &#039;&#039;&#039;refreshContent(showSpinner = true)&#039;&#039;&#039;: Refresh the current content. By default it will display a spinner while refreshing, if you don&#039;t want it to be displayed you should pass false as a parameter.&lt;br /&gt;
* &#039;&#039;&#039;updateContent(args: any, component?: string, method?: string)&#039;&#039;&#039;: Refresh the current content using different params. You need to specify the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Group selector=====&lt;br /&gt;
&lt;br /&gt;
Imagine we have an activity that uses groups and we want to let the user select which group he wants to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting.&lt;br /&gt;
&lt;br /&gt;
To do so, we&#039;ll use a drop down to select the group. When the user selects a group using this drop down we&#039;ll update the page content to display the new group.&lt;br /&gt;
&lt;br /&gt;
The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular&#039;s &#039;&#039;ngModel&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
======Using plain HTML======&lt;br /&gt;
&lt;br /&gt;
We need to add a &amp;quot;&#039;&#039;selected&#039;&#039;&amp;quot; attribute to the option that needs to be selected. To do so, we need to pre-caclulate the selected option in the PHP code:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
        // Detect which group is selected.&lt;br /&gt;
        foreach ($groups as $gid=&amp;gt;$group) {&lt;br /&gt;
            $group-&amp;gt;selected = $gid === $groupid;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid,&lt;br /&gt;
            &#039;groups&#039; =&amp;gt; $groups&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the code above, we&#039;re retrieving the groups the user can see and then we&#039;re adding a &amp;quot;selected&amp;quot; bool to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template.&lt;br /&gt;
&lt;br /&gt;
In the template, we display the drop down like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: $event})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot; &amp;lt;%#selected%&amp;gt;selected&amp;lt;%/selected%&amp;gt; &amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;ionChange&#039;&#039; function will be called everytime the user selects a different group with the drop down. We&#039;re using the function &#039;&#039;updateContent&#039;&#039; to update the current view using the new group. &#039;&#039;$event&#039;&#039; is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work.&lt;br /&gt;
&lt;br /&gt;
======Using ngModel======&lt;br /&gt;
&lt;br /&gt;
ngModel is an Angular directive that allows storing the value of a certain input/select in a Javascript variable, and also the opposite way: tell the input/select which value to set. The main problem is that we cannot initialize a Javascript variable from the template (Angular doesn&#039;t have &#039;&#039;ng-init&#039;&#039; like in AngularJS), so we&#039;ll use &amp;quot;otherdata&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
In the PHP function we&#039;ll return the group that needs to be selected in the &#039;&#039;otherdata&#039;&#039; array:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
         ...&lt;br /&gt;
&lt;br /&gt;
         return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; array(&lt;br /&gt;
                &#039;group&#039; =&amp;gt; $groupid&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we don&#039;t need to iterate over the groups array like in the plain HTML example. However, now we&#039;re returning the groupid in the &amp;quot;otherdata&amp;quot; array. As it&#039;s explained in the [[Mobile_support_for_plugins#Using_.E2.80.9Cotherdata.E2.80.9D|Using &amp;quot;otherdata&amp;quot;]] section, this &amp;quot;otherdata&amp;quot; is visible in the templates inside a variable named &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. So in the template we&#039;ll use this variable like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.group&amp;quot; (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: CONTENT_OTHERDATA.group})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Use the rich text editor===&lt;br /&gt;
&lt;br /&gt;
The rich text editor included in the app requires a FormControl to work. You can use the library FormBuilder to create this control (or to create a whole FormGroup if you prefer).&lt;br /&gt;
&lt;br /&gt;
With the following Javascript you&#039;ll be able to create a FormControl:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.control = this.FormBuilder.control(this.CONTENT_OTHERDATA.rte);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we&#039;re using a value returned in OTHERDATA as the initial value of the rich text editor, but you can use whatever you want.&lt;br /&gt;
&lt;br /&gt;
Then you need to pass this control to the rich text editor in your template:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;core-rich-text-editor item-content [control]=&amp;quot;control&amp;quot; placeholder=&amp;quot;Enter your text here&amp;quot; name=&amp;quot;rte_answer&amp;quot;&amp;gt;&amp;lt;/core-rich-text-editor&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, there are several ways to send the value in the rich text editor to a WebService to save it. This is one of the simplest options:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_webservice&amp;quot; [params]=&amp;quot;{rte: control.value}&amp;quot; ....&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re passing the value of the rich text editor as a parameter to our WebService.&lt;br /&gt;
&lt;br /&gt;
===Initialization===&lt;br /&gt;
&lt;br /&gt;
All handlers can specify a “&#039;&#039;init&#039;&#039;” method in the mobile.php file. This method is meant to return some JavaScript code that needs to be executed as soon as the plugin is retrieved.&lt;br /&gt;
&lt;br /&gt;
When the app retrieves all the handlers, the first thing it will do is call the &#039;&#039;tool_mobile_get_content&#039;&#039; WebService with the init method. This WS call will only receive the default args.&lt;br /&gt;
&lt;br /&gt;
The app will immediately execute the JavaScript code returned by this WS call. This JavaScript can be used to manually register your handlers in the delegates you want, without having to rely on the default handlers built based on the mobile.php data.&lt;br /&gt;
&lt;br /&gt;
The templates returned by this init method will be added to a INIT_TEMPLATES variable that will be passed to all the Javascript code of that handler. This means that the Javascript returned by the init method or the “main” method can access any of the templates HTML like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.INIT_TEMPLATES[‘main’];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
In this case, “main” is the ID of the template we want to use.&lt;br /&gt;
&lt;br /&gt;
The same happens with the &#039;&#039;otherdata&#039;&#039; returned by this init method, it is added to a INIT_OTHERDATA variable.&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;restrict&#039;&#039; field returned by this init call will be used to determine if your handler is enabled or not. For example, if your handler is for the delegate &#039;&#039;CoreCourseOptionsDelegate&#039;&#039; and you return a list of courseids in restrict-&amp;gt;courses, then your handler will only be enabled in the courses you returned. This only applies to the “default” handlers, if you register your own handler using the Javascript code then you should check yourself if the handler is enabled.&lt;br /&gt;
&lt;br /&gt;
Finally, if you return an object in this init Javascript code, all the properties of that object will be passed to all the Javascript code of that handler so you can use them when the code is run. For example, if your init Javascript code does something like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    MyAddonClass: new MyAddonClass()&lt;br /&gt;
};&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then, for the rest of Javascript code of your handler (e.g. for the “main” method) you can use this variable like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.MyAddonClass&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Module link handler=====&lt;br /&gt;
&lt;br /&gt;
A link handler allows you to decide what to do when a link with a certain URL is clicked. This is useful, for example, to open your module when a link to the module is clicked. In this example we’ll create a link handler to detect links to a certificate module using a init JavaScript:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function AddonModCertificateModuleLinkHandler() {&lt;br /&gt;
    that.CoreContentLinksModuleIndexHandler.call(this, that.CoreCourseHelperProvider, &#039;mmaModCertificate&#039;, &#039;certificate&#039;);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateLinkHandler&amp;quot;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype = Object.create(this.CoreContentLinksModuleIndexHandler.prototype);&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype.constructor = AddonModCertificateModuleLinkHandler;&lt;br /&gt;
&lt;br /&gt;
this.CoreContentLinksDelegate.registerHandler(new AddonModCertificateModuleLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Advanced link handler=====&lt;br /&gt;
Link handlers have some advanced features that allow you to change how links behave under different conditions.&lt;br /&gt;
======Patterns======&lt;br /&gt;
You can define a Regular Expression pattern to match certain links.  This will apply the handler only to links that match the pattern.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.pattern = RegExp(&#039;\/mod\/foo\/specialpage.php&#039;);&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Priority======&lt;br /&gt;
Multiple link handlers may apply to a given link.  You can define the order of precedence by setting the priority - the handler with the highest priority will be used.&lt;br /&gt;
All default handlers have a priority of 0, so 1 or higher will override the default.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Multiple actions======&lt;br /&gt;
Once a link has been matched, the handler&#039;s getActions() method determines what the link should do.  This method has access to the URL and its parameters.&lt;br /&gt;
Different actions can be returned depending on different conditions.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {&lt;br /&gt;
    return [{&lt;br /&gt;
        action: function(siteId, navCtrl) {&lt;br /&gt;
            // The actual behaviour of the link goes here.&lt;br /&gt;
        },&lt;br /&gt;
        sites: [...]&lt;br /&gt;
    }, {&lt;br /&gt;
        ...&lt;br /&gt;
    }];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Once handlers have been matched for a link, the actions will be fetched for all the matching handlers, in priorty order.  The first &amp;quot;valid&amp;quot; action will be used to open the link.&lt;br /&gt;
If your handler is matched with a link, but a condition assessed in the getActions() function means you want to revert to the next highest priorty handler, you can &amp;quot;invalidate&amp;quot;&lt;br /&gt;
your action by settings its sites propety to an empty array.&lt;br /&gt;
======Complex example======&lt;br /&gt;
This will match all URLs containing /mod/foo/, and force those with an id parameter that&#039;s not in the &amp;quot;supportedModFoos&amp;quot; array to open in the user&#039;s browser, rather than the app.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
var supportedModFoos = [...];&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    this.pattern = new RegExp(&#039;\/mod\/foo\/&#039;);&lt;br /&gt;
    this.name = &amp;quot;AddonModFooLinkHandler&amp;quot;;&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
}&lt;br /&gt;
AddonModFooLinkHandler.prototype = Object.create(that.CoreContentLinksHandlerBase.prototype);&lt;br /&gt;
AddonModFooLinkHandler.prototype.constructor = AddonModFooLinkHandler;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {     &lt;br /&gt;
    var action = {&lt;br /&gt;
        action: function() {&lt;br /&gt;
            that.CoreUtilsProvider.openInBrowser(url);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) {&lt;br /&gt;
        action.sites = [];&lt;br /&gt;
    }&lt;br /&gt;
    return [action];&lt;br /&gt;
};&lt;br /&gt;
that.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Module prefetch handler=====&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;CoreCourseModuleDelegate&#039;&#039; handler allows you to define a list of &#039;&#039;offlinefunctions&#039;&#039; to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain WS calls (pass the result of a WS call to the next one), and this cannot be done using &#039;&#039;offlinefunctions&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to create a prefetch handler using init JS:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
// Create a class that &amp;quot;inherits&amp;quot; from CoreCourseActivityPrefetchHandlerBase.&lt;br /&gt;
function AddonModCertificateModulePrefetchHandler() {&lt;br /&gt;
    that.CoreCourseActivityPrefetchHandlerBase.call(this, that.TranslateService, that.CoreAppProvider, that.CoreUtilsProvider,&lt;br /&gt;
            that.CoreCourseProvider, that.CoreFilepoolProvider, that.CoreSitesProvider, that.CoreDomUtilsProvider);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateModulePrefetchHandler&amp;quot;;&lt;br /&gt;
    this.modName = &amp;quot;certificate&amp;quot;;&lt;br /&gt;
    this.component = &amp;quot;mod_certificate&amp;quot;; // This must match the plugin identifier from db/mobile.php, otherwise the download link in the context menu will not update correctly.&lt;br /&gt;
    this.updatesNames = /^configuration$|^.*files$/;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype = Object.create(this.CoreCourseActivityPrefetchHandlerBase.prototype);&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.constructor = AddonModCertificateModulePrefetchHandler;&lt;br /&gt;
&lt;br /&gt;
// Override the prefetch call.&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.prefetch = function(module, courseId, single, dirPath) {&lt;br /&gt;
    return this.prefetchPackage(module, courseId, single, prefetchCertificate);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function prefetchCertificate(module, courseId, single, siteId) {&lt;br /&gt;
    // Perform all the WS calls.&lt;br /&gt;
    // You can access most of the app providers using that.ClassName. E.g. that.CoreWSProvider.call().&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same cmid. The app will not automatically work with this situation - it will call the offline function with the standard arguments only, so you won&#039;t be able to prefetch all the possible parameters. &lt;br /&gt;
&lt;br /&gt;
To deal with this, you need to implement a web service in your Moodle component that returns the list of possible extra arguments, and then you can call this web service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module (showing only the actual prefetch function - the rest of the code is as above) where there are multiple values of a custom &#039;section&#039; parameter for the mobile function &#039;mobile_document_view&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function prefetchOucontent(module, courseId, single, siteId) {&lt;br /&gt;
    var component = &#039;mod_oucontent&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Get the site, first.&lt;br /&gt;
    return that.CoreSitesProvider.getSite(siteId).then(function(site) {&lt;br /&gt;
        // Read the list of pages in this document using a web service.&lt;br /&gt;
        return site.read(&#039;mod_oucontent_get_page_list&#039;, {&#039;cmid&#039;: module.id}).then(function(response) {&lt;br /&gt;
            var promises = [];&lt;br /&gt;
&lt;br /&gt;
            // For each page, read and process the page - this is a copy of logic in the app at&lt;br /&gt;
            // siteplugins.ts (prefetchFunctions), but modified to add the custom argument.&lt;br /&gt;
            for(var i = 0; i &amp;lt; response.length; i++) {&lt;br /&gt;
                var args = {&lt;br /&gt;
                    courseid: courseId,&lt;br /&gt;
                    cmid: module.id,&lt;br /&gt;
                    userid: site.getUserId()&lt;br /&gt;
                };&lt;br /&gt;
                if (response[i] !== &#039;&#039;) {&lt;br /&gt;
                    args.section = response[i];&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                promises.push(that.CoreSitePluginsProvider.getContent(&lt;br /&gt;
                        component, &#039;mobile_document_view&#039;, args).then(&lt;br /&gt;
                        function(result) {&lt;br /&gt;
                            var subPromises = [];&lt;br /&gt;
                            if (result.files &amp;amp;&amp;amp; result.files.length) {&lt;br /&gt;
                                subPromises.push(that.CoreFilepoolProvider.downloadOrPrefetchFiles(&lt;br /&gt;
                                        site.id, result.files, true, false, component, module.id));&lt;br /&gt;
                            }&lt;br /&gt;
                            return Promise.all(subPromises);&lt;br /&gt;
                        }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            return Promise.all(promises);&lt;br /&gt;
        });&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Single activity course format=====&lt;br /&gt;
&lt;br /&gt;
In the following example, the value of INIT_TEMPLATES[&amp;quot;main&amp;quot;] is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;core-dynamic-component [component]=&amp;quot;componentClass&amp;quot; [data]=&amp;quot;data&amp;quot;&amp;gt;&amp;lt;/core-dynamic-component&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This template is returned by the init method. And this is the JavaScript code returned by the init method:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function getAddonSingleActivityFormatComponent() {&lt;br /&gt;
    function AddonSingleActivityFormatComponent() {&lt;br /&gt;
        this.data = {};&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.constructor = AddonSingleActivityFormatComponent;&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.ngOnChanges = function(changes) {&lt;br /&gt;
        var self = this;&lt;br /&gt;
&lt;br /&gt;
        if (this.course &amp;amp;&amp;amp; this.sections &amp;amp;&amp;amp; this.sections.length) {&lt;br /&gt;
            var module = this.sections[0] &amp;amp;&amp;amp; this.sections[0].modules &amp;amp;&amp;amp; this.sections[0].modules[0];&lt;br /&gt;
            if (module &amp;amp;&amp;amp; !this.componentClass) {&lt;br /&gt;
                that.CoreCourseModuleDelegate.getMainComponent(that.Injector, this.course, module).then((component) =&amp;gt; {&lt;br /&gt;
                    self.componentClass = component || that.CoreCourseUnsupportedModuleComponent;&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.data.courseId = this.course.id;&lt;br /&gt;
            this.data.module = module;&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.doRefresh = function(refresher, done) {&lt;br /&gt;
        return Promise.resolve(this.dynamicComponent.callComponentFunction(&amp;quot;doRefresh&amp;quot;, [refresher, done]));&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
return AddonSingleActivityFormatComponent;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function AddonSingleActivityFormatHandler() {&lt;br /&gt;
    this.name = &amp;quot;singleactivity&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.constructor = AddonSingleActivityFormatHandler;&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.isEnabled = function() {&lt;br /&gt;
    return true;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.canViewAllSections = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseTitle = function(course, sections) {&lt;br /&gt;
    if (sections &amp;amp;&amp;amp; sections[0] &amp;amp;&amp;amp; sections[0].modules &amp;amp;&amp;amp; sections[0].modules[0]) {&lt;br /&gt;
        return sections[0].modules[0].name;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return course.fullname || &amp;quot;&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displayEnableDownload = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displaySectionSelector = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseFormatComponent = function(injector, course) {&lt;br /&gt;
    that.Injector = injector || that.Injector;&lt;br /&gt;
&lt;br /&gt;
    return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES[&amp;quot;main&amp;quot;], getAddonSingleActivityFormatComponent(), injector);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using the JavaScript API===&lt;br /&gt;
&lt;br /&gt;
The Javascript API is partly supported right now, only the delegates specified in the section [[Mobile_support_for_plugins#Templates_downloaded_on_login_and_rendered_using_JS_data_2|Templates downloaded on login and rendered using JS data]] supports it now. This API allows you to override any of the functions of the default handler. &lt;br /&gt;
&lt;br /&gt;
The “method” specified in a handler registered in the &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; will be called immediately after the init method, and the Javascript returned by this method will be run. If this Javascript code returns an object with certain functions, these function will override the ones in the default handler.&lt;br /&gt;
&lt;br /&gt;
For example, if the Javascript returned by the method returns something like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The the &#039;&#039;getData&#039;&#039; function of the default handler will be overridden by the returned getData function.&lt;br /&gt;
&lt;br /&gt;
The default handler for &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; only has 2 functions: &#039;&#039;getComponent&#039;&#039; and &#039;&#039;getData&#039;&#039;. In addition, the JavaScript code can return an extra function named &#039;&#039;componentInit&#039;&#039; that will be executed when the component returned by &#039;&#039;getComponent&#039;&#039; is initialized.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to support the text user profile field using this API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
var result = {&lt;br /&gt;
    componentInit: function() {&lt;br /&gt;
        if (this.field &amp;amp;&amp;amp; this.edit &amp;amp;&amp;amp; this.form) {&lt;br /&gt;
            this.field.modelName = &amp;quot;profile_field_&amp;quot; + this.field.shortname;&lt;br /&gt;
&lt;br /&gt;
            if (this.field.param2) {&lt;br /&gt;
                this.field.maxlength = parseInt(this.field.param2, 10) || &amp;quot;&amp;quot;;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? &amp;quot;password&amp;quot; : &amp;quot;text&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
            var formData = {&lt;br /&gt;
                value: this.field.defaultdata,&lt;br /&gt;
                disabled: this.disabled&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            this.form.addControl(this.field.modelName, that.FormBuilder.control(formData, this.field.required &amp;amp;&amp;amp; !this.field.locked ? that.Validators.required : null));&lt;br /&gt;
        }&lt;br /&gt;
    },&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
        var name = &amp;quot;profile_field_&amp;quot; + field.shortname;&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            type: &amp;quot;text&amp;quot;,&lt;br /&gt;
            name: name,&lt;br /&gt;
            value: that.CoreTextUtilsProvider.cleanTags(formValues[name])&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Translate dynamic strings===&lt;br /&gt;
&lt;br /&gt;
If you wish to have an element that displays a localised string based on value from your template you can doing something like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content translate&amp;gt;&lt;br /&gt;
        plugin.mod_myactivity.&amp;lt;% status %&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This could save you from having to write something like when only one value should be displayed:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content&amp;gt;&lt;br /&gt;
        &amp;lt;%#isedting%&amp;gt;{{ &#039;plugin.mod_myactivity.editing&#039; | translate }}&amp;lt;%/isediting%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isopen%&amp;gt;{{ &#039;plugin.mod_myactivity.open&#039; | translate }}&amp;lt;%/isopen%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isclosed%&amp;gt;{{ &#039;plugin.mod_myactivity.closed&#039; | translate }}&amp;lt;%/isclosed%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using strings with dates===&lt;br /&gt;
&lt;br /&gt;
If you have a string that you wish to pass a formatted date for example in the Moodle language file you have:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$string[&#039;strwithdate&#039;] = &#039;This string includes a date of {$a-&amp;gt;date} in the middle of it.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can localise the string correctly in your template using something like the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
{{ &#039;plugin.mod_myactivity.strwithdate&#039; | translate: {$a: { date: &amp;lt;% timestamp %&amp;gt; * 1000 | coreFormatDate: &amp;quot;dffulldate&amp;quot; } } }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A Unix timestamp must be multiplied by 1000 as the Mobile App expects millisecond timestamps, where as Unix timestamps are in seconds.&lt;br /&gt;
&lt;br /&gt;
===Support push notification clicks===&lt;br /&gt;
&lt;br /&gt;
If your plugin sends push notifications to the app, you might want to open a certain page in the app when the notification is clicked. There are several ways to achieve this.&lt;br /&gt;
&lt;br /&gt;
The easiest way is to include a &#039;&#039;contexturl&#039;&#039; in your notification. When the notification is clicked, the app will try to open the &#039;&#039;contexturl&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Please notice that the &#039;&#039;contexturl&#039;&#039; will also be displayed in web. If you want to use a specific URL for the app, different than the one displayed in web, you can do so by returning a &#039;&#039;customdata&#039;&#039; array that contains an &#039;&#039;appurl&#039;&#039; property:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$notification-&amp;gt;customdata = [&lt;br /&gt;
    &#039;appurl&#039; =&amp;gt; $myurl-&amp;gt;out(),&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In both cases you will have to create a link handler to treat the URL. For more info on how to create the link handler, please see [[Mobile_support_for_plugins#Advanced_link_handler|how to create an advanced link handler]].&lt;br /&gt;
&lt;br /&gt;
If you want to do something that only happens when the notification is clicked, not when the link is clicked, you&#039;ll have to implement a push click handler yourself. The way to create it is similar to [[Mobile_support_for_plugins#Advanced_link_handler|creating an advanced link handler]], but you&#039;ll have to use &#039;&#039;CorePushNotificationsDelegate&#039;&#039; and your handler will have to implement the properties and functions defined in the interface [https://github.com/moodlehq/moodlemobile2/blob/master/src/core/pushnotifications/providers/delegate.ts#L24 CorePushNotificationsClickHandler].&lt;br /&gt;
&lt;br /&gt;
===Implement a module similar to mod_label===&lt;br /&gt;
&lt;br /&gt;
In Moodle 3.8 or higher, if your plugin doesn&#039;t support &#039;&#039;FEATURE_NO_VIEW_LINK&#039;&#039; and you don&#039;t specify a &#039;&#039;coursepagemethod&#039;&#039; then the module will only display the module description in the course page and it won&#039;t be clickable in the app, just like mod_label. You can decide if you want the module icon to be displayed or not (if you don&#039;t want it to be displayed, then don&#039;t define it in &#039;&#039;displaydata&#039;&#039;).&lt;br /&gt;
&lt;br /&gt;
However, if your plugin needs to work in previous versions of Moodle or you want to display something different than the description then you need a different approach.&lt;br /&gt;
&lt;br /&gt;
If your plugin wants to render something in the course page instead of just the module name and description you should specify the property &#039;&#039;coursepagemethod&#039;&#039; in the mobile.php. The template returned by this method will be rendered in the course page. Please notice the HTML returned should not contain directives or components, only default HTML.&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t want your module to be clickable then you just need to remove the &#039;&#039;method&#039;&#039; from mobile.php. With these 2 changes you can have a module that behaves like mod_label in the app.&lt;br /&gt;
&lt;br /&gt;
===Use Ionic navigation lifecycle functions===&lt;br /&gt;
&lt;br /&gt;
Ionic let pages define some functions that will be called when certain navigation lifecycle events happen. For more info about these functions, see [https://ionicframework.com/blog/navigating-lifecycle-events/ this page].&lt;br /&gt;
&lt;br /&gt;
You can define these functions in your plugin javascript:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.ionViewCanLeave = function() {&lt;br /&gt;
    ...&lt;br /&gt;
};&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
So for example you can make your plugin ask for confirmation if the user tries to leave the page when he has some unsaved data.&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
&lt;br /&gt;
=== Invalid response received ===&lt;br /&gt;
&lt;br /&gt;
You might receive this error when using the &amp;quot;core-site-plugins-call-ws&amp;quot; directive or similar. By default, the app expects all WebService calls to return an object, if your WebService returns another type (string, bool, ...) then you need to specify it using the preSets attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{typeExpected: &#039;boolean&#039;}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
In a similar way, if your WebService returns null you need to tell the app not to expect any result using the preSets:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{responseExpected: false}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS ===&lt;br /&gt;
&lt;br /&gt;
Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, ion-radio, ion-checkbox and ion-select don&#039;t use HTML inputs, they simulate them, so the directive isn&#039;t going to find their data and so it won&#039;t be sent to the WebService.&lt;br /&gt;
&lt;br /&gt;
There are 2 workarounds to fix this problem. It seems that the next major release of Ionic framework does use HTML inputs, so these are temporary solutions.&lt;br /&gt;
&lt;br /&gt;
==== Sending the data manually ====&lt;br /&gt;
&lt;br /&gt;
The first solution is to send the missing params manually using the &amp;quot;&#039;&#039;params&#039;&#039;&amp;quot; property. We will use &#039;&#039;ngModel&#039;&#039; to store the input value in a variable, and this variable will be passed to the params. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a template like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group name=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, responses: responses}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Basically, you need to add &#039;&#039;ngModel&#039;&#039; to the affected element (in this case, the &#039;&#039;radio-group&#039;&#039;). You can put whatever name you want as the value, we used &amp;quot;responses&amp;quot;. With this, everytime the user selects a radio button the value will be stored in a variable named &amp;quot;responses&amp;quot;. Then, in the button we are passing this variable to the params of the WebService.&lt;br /&gt;
&lt;br /&gt;
Please notice that the &amp;quot;form&amp;quot; attribute has priority over &amp;quot;params&amp;quot;, so if you have an input with name=&amp;quot;responses&amp;quot; it will override what you&#039;re manually passing to params.&lt;br /&gt;
&lt;br /&gt;
==== Using a hidden input ====&lt;br /&gt;
&lt;br /&gt;
Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use &#039;&#039;ngModel&#039;&#039; to synchronize your ion-radio/ion-checkbox/ion-select with the new hidden input. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a radio button like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot; [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;hidden&amp;quot; [ngModel]=&amp;quot;responses&amp;quot; name=&amp;quot;responses&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we&#039;re using a variable named &amp;quot;responses&amp;quot; to synchronize the data between the &#039;&#039;radio-group&#039;&#039; and the hidden input. You can use whatever name you want.&lt;br /&gt;
&lt;br /&gt;
=== I can&#039;t return an object or array in otherdata ===&lt;br /&gt;
&lt;br /&gt;
If you try to return an object or an array in any field inside &#039;&#039;otherdata&#039;&#039;, the WebService call will fail with the following error:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Scalar type expected, array or object received&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Each field in &#039;&#039;otherdata&#039;&#039; must be a string, number or boolean, it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($data))&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The app will automatically parse this JSON and convert it back into an array or object.&lt;br /&gt;
&lt;br /&gt;
==Examples==&lt;br /&gt;
&lt;br /&gt;
===Accepting dynamic names in a WebService===&lt;br /&gt;
&lt;br /&gt;
We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new WebService that we have created.&lt;br /&gt;
&lt;br /&gt;
The first issue we find is that the WebService needs to define the names of the parameters received, but in this case they&#039;re dynamic. The solution is to accept an array of objects with name and value. So in the &#039;&#039;_parameters()&#039;&#039; function of our new WebService, we will add this parameter:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;data&#039; =&amp;gt; new external_multiple_structure(&lt;br /&gt;
     new external_single_structure(&lt;br /&gt;
        array(&lt;br /&gt;
            &#039;name&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data name&#039;),&lt;br /&gt;
            &#039;value&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data value&#039;),&lt;br /&gt;
        )&lt;br /&gt;
    ),&lt;br /&gt;
    &#039;The data to be saved&#039;, VALUE_DEFAULT, array()&lt;br /&gt;
)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now we need to adapt our form to send the data as the WebService requires it. In our template, we have a button with the directive &#039;&#039;core-site-plugins-call-ws&#039;&#039; that will send the form data to our WebService. To make this work we will have to pass the parameters manually, without using the &amp;quot;&#039;&#039;form&#039;&#039;&amp;quot; attribute, because we need to format the data before it is sent.&lt;br /&gt;
&lt;br /&gt;
Since we will send the params manually and we want it all to be sent in the same array, we will use &#039;&#039;ngModel&#039;&#039; to store the input data into a variable that we&#039;ll call &amp;quot;data&amp;quot;, but you can use the name you want. This &amp;quot;data&amp;quot; will be an object that will hold the input data with the format &amp;quot;name-&amp;gt;value&amp;quot;. For example, if I have an input with name &amp;quot;a1&amp;quot; and value &amp;quot;My answer&amp;quot;, the data object will be:&lt;br /&gt;
&lt;br /&gt;
{a1: &amp;quot;My answer&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
So we need to add &#039;&#039;ngModel&#039;&#039; to all the inputs whose values need to be sent to the &amp;quot;data&amp;quot; WS param. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&amp;lt;ion-input name=&amp;quot;&amp;lt;% name %&amp;gt;&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.data[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re using &#039;&#039;CONTENT_OTHERDATA&#039;&#039; to store the data. We do it like this because we&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the form, setting the values the user has already stored. If you don&#039;t need to initialize the form, then you can use the variable &amp;quot;dataObject&amp;quot;, an empty object that the Mobile app creates for you: [(ngModel)]=&amp;quot;dataObject[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&lt;br /&gt;
&lt;br /&gt;
The Mobile app has a function that allows you to convert this data object into an array like the one the WS expects: &#039;&#039;objectToArrayOfObjects&#039;&#039;. So in our button we&#039;ll use this function to format the data before it&#039;s sent:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_ws_name&amp;quot;&lt;br /&gt;
    [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, data: CoreUtilsProvider.objectToArrayOfObjects(CONTENT_OTHERDATA.data, &#039;name&#039;, &#039;value&#039;)}&amp;quot;&lt;br /&gt;
    successMessage&lt;br /&gt;
    refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see in the example above, we&#039;re specifying that the keys of the &amp;quot;data&amp;quot; object need to be stored in a property named &amp;quot;name&amp;quot;, and the values need to be stored in a property named &amp;quot;value&amp;quot;. If your WebService expects different names you need to change the parameters of the function &#039;&#039;objectToArrayOfObjects&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
If you open your plugin now in the Mobile app it will display an error in the Javascript console. The reason is that the variable &amp;quot;data&amp;quot; doesn&#039;t exist inside &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. As it is explained in previous sections, &#039;&#039;CONTENT_OTHERDATA&#039;&#039; holds the data that you return in &#039;&#039;otherdata&#039;&#039; for your method. We&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the values to be displayed in the form.&lt;br /&gt;
&lt;br /&gt;
If the user hasn&#039;t answered the form yet, we can initialize the &amp;quot;data&amp;quot; object as an empty object. Please remember that we cannot return arrays or objects in &#039;&#039;otherdata&#039;&#039;, so we&#039;ll return a JSON string.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; &#039;{}&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$userdata = get_user_responses(); // It will held the data in a format name-&amp;gt;value. Example: array(&#039;a1&#039; =&amp;gt; &#039;My value&#039;).&lt;br /&gt;
...&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($userdata))&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our WebService in array format.&lt;br /&gt;
&lt;br /&gt;
==Moodle plugins with mobile support==&lt;br /&gt;
&lt;br /&gt;
* Group choice: [https://moodle.org/plugins/mod_choicegroup Moodle plugins directory entry] and [https://github.com/ndunand/moodle-mod_choicegroup code in github].&lt;br /&gt;
* Custom certificate: [https://moodle.org/plugins/mod_customcert Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_customcert code in github].&lt;br /&gt;
* Gapfill question type: [https://moodle.org/plugins/qtype_gapfill Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_gapfill in github].&lt;br /&gt;
* Wordselect question type: [https://moodle.org/plugins/qtype_wordselect Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_wordselect in github].&lt;br /&gt;
* RegExp question type: [https://moodle.org/plugins/qtype_regexp Moodle plugins directory entry] and [https://github.com/rezeau/moodle-qtype_regexp in github].&lt;br /&gt;
* Certificate: [https://moodle.org/plugins/mod_certificate Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_certificate in github].&lt;br /&gt;
* Attendance [https://moodle.org/plugins/mod_attendance Moodle plugins directory entry] and [https://github.com/danmarsden/moodle-mod_attendance in github].&lt;br /&gt;
* ForumNG (unfinished support) [https://moodle.org/plugins/mod_forumng Moodle plugins directory entry] and [https://github.com/moodleou/moodle-mod_forumng in github].&lt;br /&gt;
* News block [https://github.com/moodleou/moodle-block_news in github].&lt;br /&gt;
* H5P activity module  [https://moodle.org/plugins/mod_hvp Moodle plugins directory entry] and [https://github.com/h5p/h5p-moodle-plugin in github].&lt;br /&gt;
&lt;br /&gt;
See the complete list in the plugins database [https://moodle.org/plugins/browse.php?list=award&amp;amp;id=6 here] (it may contain some outdated plugins)&lt;br /&gt;
&lt;br /&gt;
=== Mobile app support award ===&lt;br /&gt;
&lt;br /&gt;
If you want your plugin to be awarded in the plugins directory and marked as supporting the mobile app, please feel encouraged to contact us via email [mailto:mobile@moodle.com mobile@moodle.com].&lt;br /&gt;
&lt;br /&gt;
Don&#039;t forget to include a link to your plugin page and the location of its code repository.&lt;br /&gt;
&lt;br /&gt;
See [https://moodle.org/plugins/?q=award:mobile-app the list of awarded plugins] in the plugins directory&lt;br /&gt;
&lt;br /&gt;
[[Category:Mobile]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=lib/formslib.php_Form_Definition&amp;diff=56726</id>
		<title>lib/formslib.php Form Definition</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=lib/formslib.php_Form_Definition&amp;diff=56726"/>
		<updated>2019-12-10T16:27:50Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* autocomplete */  Added documentation of valuehtmlcallback&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Formslib}}&lt;br /&gt;
== &#039;&#039;definition()&#039;&#039; ==&lt;br /&gt;
&lt;br /&gt;
The definition of the elements to be included in the form, their &#039;types&#039; (PARAM_*), helpbuttons included, etc. is all included in a function you must define in your class.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;definition()&#039;&#039; is used to define the elements in the form and &#039;&#039;&#039;this definition will be used for validating data submitted as well as for printing the form.&#039;&#039;&#039; For select and checkbox type elements only data that could have been selected will be allowed. And only data that corresponds to a form element in the definition will be accepted as submitted data.&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;definition()&#039;&#039; should include all elements that are going to be used on form, some elements may be removed or tweaked later in &#039;&#039;definition_after_data()&#039;&#039;. Please do not create conditional elements in &#039;&#039;definition()&#039;&#039;, the definition() should not directly depend on the submitted data.&lt;br /&gt;
&lt;br /&gt;
Note that the definition function is called when the form class is instantiated. There is no option to (say) manipulate data in the class (that may affect the rendering of the form) between instantiating the form and calling any other methods. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
require_once(&amp;quot;$CFG-&amp;gt;libdir/formslib.php&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
class simplehtml_form extends moodleform {&lt;br /&gt;
&lt;br /&gt;
    function definition() {&lt;br /&gt;
        global $CFG;&lt;br /&gt;
       &lt;br /&gt;
        $mform = $this-&amp;gt;_form; // Don&#039;t forget the underscore! &lt;br /&gt;
&lt;br /&gt;
        $mform-&amp;gt;addElement()... // Add elements to your form&lt;br /&gt;
            ...&lt;br /&gt;
    }                           // Close the function&lt;br /&gt;
}                               // Close the class&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
===Passing parameters to the Form===&lt;br /&gt;
&lt;br /&gt;
The constructor for &#039;&#039;moodleform&#039;&#039; allows a number of parameters including one (&#039;&#039;$customdata&#039;&#039;) to permit an array of arbitrary data to be passed to your form. &lt;br /&gt;
&lt;br /&gt;
For example, you can pass the data &amp;quot;$email&amp;quot; and &amp;quot;$username&amp;quot; to the Form&#039;s class for use inside (say) the definition.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
 $mform_simple = new simplehtml_form( null, array(&#039;email&#039;=&amp;gt;$email, &#039;username&#039;=&amp;gt;$username ) );&lt;br /&gt;
...&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
(the first parameter is $action, &#039;&#039;null&#039;&#039; will cause the form action to be determined automatically)&lt;br /&gt;
&lt;br /&gt;
Secondly, inside the form definition you can use those parameters to set the default values to some of the form&#039;s fields&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
...&lt;br /&gt;
 $mform-&amp;gt;addElement(&#039;text&#039;, &#039;email&#039;, get_string(&#039;email&#039;), &#039;maxlength=&amp;quot;100&amp;quot; size=&amp;quot;25&amp;quot; &#039;);&lt;br /&gt;
 $mform-&amp;gt;setType(&#039;email&#039;, PARAM_NOTAGS);&lt;br /&gt;
 $mform-&amp;gt;addRule(&#039;email&#039;, get_string(&#039;missingemail&#039;), &#039;required&#039;, null, &#039;server&#039;);&lt;br /&gt;
 // Set default value by using a passed parameter&lt;br /&gt;
 $mform-&amp;gt;setDefault(&#039;email&#039;,$this-&amp;gt;_customdata[&#039;email&#039;]);&lt;br /&gt;
...&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Use Fieldsets to group Form Elements==&lt;br /&gt;
&lt;br /&gt;
You use code like this to open a fieldset with a &#039;&#039;legend&#039;&#039;. &amp;lt;br /&amp;gt;&lt;br /&gt;
(&#039;&#039;&#039;Note&#039;&#039;&#039;: Some themes turn off legends on admin setting pages by using CSS: &amp;lt;nowiki&amp;gt;#adminsettings legend {display:none;}&amp;lt;/nowiki&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;header&#039;, &#039;nameforyourheaderelement&#039;, get_string(&#039;titleforlegened&#039;, &#039;modulename&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can&#039;t yet nest these visible fieldsets unfortunately. But in fact groups of elements are wrapped in invisible fieldsets.&lt;br /&gt;
&lt;br /&gt;
{{Moodle 2.5}}&lt;br /&gt;
Since Moodle 2.5 fieldsets without any required fields are collapsed by default. To display these fieldsets on page load, use:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
 $mform-&amp;gt;setExpanded(&#039;foo&#039;)&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You close a fieldset with moodle_form&#039;s closeHeaderBefore method. You tell closeHeaderBefore the element before you wish to end the fieldset. A fieldset is automatically closed if you open a new one. You need to use this code only if you want to close a fieldset and the subsequent form elements are not to be enclosed by a visible fieldset (they are still enclosed with an invisibe one with no legend) :&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;closeHeaderBefore(&#039;buttonar&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==addElement==&lt;br /&gt;
&lt;br /&gt;
Use the addElement method to add an element to a form. The first few arguments are always the same. The first param is the type of the element to add. The second is the elementname to use which is normally the html name of the element in the form. The third is often the text for the label for the element.&lt;br /&gt;
&lt;br /&gt;
Some examples are below :&lt;br /&gt;
=== button ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;button&#039;, &#039;intro&#039;, get_string(&amp;quot;buttonlabel&amp;quot;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button element. If you want a submit or cancel button see &#039;submit&#039; element.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== autocomplete ===&lt;br /&gt;
{{Moodle 3.1}}&lt;br /&gt;
Available since Moodle 3.1&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$searchareas = \core_search\manager::get_search_areas_list(true);                                                           &lt;br /&gt;
$areanames = array();                                                                                                       &lt;br /&gt;
foreach ($searchareas as $areaid =&amp;gt; $searcharea) {                                                                          &lt;br /&gt;
    $areanames[$areaid] = $searcharea-&amp;gt;get_visible_name();                                                                  &lt;br /&gt;
}                                                                                                                           &lt;br /&gt;
$options = array(                                                                                                           &lt;br /&gt;
    &#039;multiple&#039; =&amp;gt; true,                                                  &lt;br /&gt;
&lt;br /&gt;
                                                   &lt;br /&gt;
    &#039;noselectionstring&#039; =&amp;gt; get_string(&#039;allareas&#039;, &#039;search&#039;),                                                                &lt;br /&gt;
);         &lt;br /&gt;
$mform-&amp;gt;addElement(&#039;autocomplete&#039;, &#039;areaids&#039;, get_string(&#039;searcharea&#039;, &#039;search&#039;), $areanames, $options);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The autocomplete element is an advanced form element that supports server-side searching - or simple filtering of a predefined list of options. Some benefits of using this form element are that it handles very large datasets extremely well - it has great accessibility built in - and it gives a good user experience. If you have so much data you need to build pagination into a page - you could probably come up with a better design using this. The simplest way to use it is compatible with the standard &#039;select&#039; form element. You give it a list of options and some parameters to configure how it behaves. The valid parameters for this simple mode of operation are:&lt;br /&gt;
* multiple (boolean - default false) - Allow more than one selected item. The data coming from the form will be an array in this case.&lt;br /&gt;
&lt;br /&gt;
[[image:autocomplete_multiple2.png|center|thumb|alt=autocomplete with multiple option|autocomplete with multiple option.]]&lt;br /&gt;
&lt;br /&gt;
* noselectionstring (string - default &amp;lt;nowiki&amp;gt;&#039;&#039;&amp;lt;/nowiki&amp;gt;) - The text to display when nothing is selected.&lt;br /&gt;
* showsuggestions (boolean - default true) - Do not show the list of suggestions when the user starts typing.&lt;br /&gt;
* placeholder (string - default &amp;lt;nowiki&amp;gt;&#039;&#039;&amp;lt;/nowiki&amp;gt;) - The text to show in the search box when it is empty.&lt;br /&gt;
* casesensitive (boolean - default false) - Is the search case sensitive ?&lt;br /&gt;
* tags (boolean - default false) - This changes the behaviour so that the user can create new valid entries in the list by typing them and pressing enter.&lt;br /&gt;
* ajax (string - default &amp;lt;nowiki&amp;gt;&#039;&#039;&amp;lt;/nowiki&amp;gt;) - This string is the name of an AMD module that can fetch and format results.&lt;br /&gt;
* valuehtmlcallback - For use with the AJAX option, so that it can format the initial value of the form field (available since Moodle 3.5)&lt;br /&gt;
&lt;br /&gt;
More explanation on the &#039;ajax&#039; option. This should be the name of an AMD module that implements 2 functions:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
/**                                                                                                                         &lt;br /&gt;
 * Source of data for Ajax element.                                                                                         &lt;br /&gt;
 *                                                                                                                          &lt;br /&gt;
 * @param {String} selector The selector of the auto complete element.                                                      &lt;br /&gt;
 * @param {String} query The query string.                                                                                  &lt;br /&gt;
 * @param {Function} callback A callback function receiving an array of results.                                            &lt;br /&gt;
 * @return {Void}                                                                                                           &lt;br /&gt;
*/                                                                                                                         &lt;br /&gt;
transport: function(selector, query, callback) ...&lt;br /&gt;
&lt;br /&gt;
/**                                                                                                                         &lt;br /&gt;
 * Process the results for auto complete elements.                                                                          &lt;br /&gt;
 *                                                                                                                          &lt;br /&gt;
 * @param {String} selector The selector of the auto complete element.                                                      &lt;br /&gt;
 * @param {Array} results An array or results.                                                                              &lt;br /&gt;
 * @return {Array} New array of results.                                                                                    &lt;br /&gt;
 */                                                                                                                         &lt;br /&gt;
processResults: function(selector, results)...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
A good example is here: [https://github.com/moodle/moodle/blob/MOODLE_31_STABLE/admin/tool/lp/amd/src/frameworks_datasource.js admin/tool/lp/amd/src/frameworks_datasource.js]&lt;br /&gt;
&lt;br /&gt;
The &#039;valuehtmlcallback&#039; function is needed when an AJAX-supporting form field has an initial value that needs special rendering, similar to how the AJAX code would render it when the user changes it dynamically. For example, if the field contains user ids and its initial value is &#039;1,2&#039; then you want it to use the rendered HTML display for each value (probably a user&#039;s name and picture), not just display those numbers. Here is an example, from /search/classes/output/form/search.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;valuehtmlcallback&#039; =&amp;gt; function($value) {&lt;br /&gt;
    global $DB, $OUTPUT;&lt;br /&gt;
    $user = $DB-&amp;gt;get_record(&#039;user&#039;, [&#039;id&#039; =&amp;gt; (int)$value], &#039;*&#039;, IGNORE_MISSING);&lt;br /&gt;
    if (!$user || !user_can_view_profile($user)) {&lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
    $details = user_get_user_details($user);&lt;br /&gt;
    return $OUTPUT-&amp;gt;render_from_template(&lt;br /&gt;
        &#039;core_search/form-user-selector-suggestion&#039;, $details);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The Mustache template used here is the same as the one used by the AJAX code when the field is changed dynamically.&lt;br /&gt;
&lt;br /&gt;
When using the ajax option in an mform with validation etc - it is recommended to sub-class the php class &amp;quot;MoodleQuickForm_autocomplete&amp;quot; so that you can provide a list of name and&lt;br /&gt;
values to populate the form element if the form is re-displayed due to a validation error. An example is [https://github.com/moodle/moodle/blob/MOODLE_31_STABLE/admin/tool/lp/classes/form/framework_autocomplete.php admin/tool/lp/classes/form/framework_autocomplete.php].&lt;br /&gt;
&lt;br /&gt;
We have provided several useful subclasses of this form element already that are simple to use (course and tags).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt; &lt;br /&gt;
    // Course example.&lt;br /&gt;
    //  Valid options are:                                                                                     &lt;br /&gt;
    //                       &#039;multiple&#039; - boolean multi select                                                                      &lt;br /&gt;
    //                       &#039;exclude&#039; - array or int, list of course ids to never show                                             &lt;br /&gt;
    //                       &#039;requiredcapabilities&#039; - array of capabilities. Uses ANY to combine them.                              &lt;br /&gt;
    //                       &#039;limittoenrolled&#039; - boolean Limits to enrolled courses.                                                &lt;br /&gt;
    //                       &#039;includefrontpage&#039; - boolean Enables the frontpage to be selected.  &lt;br /&gt;
    $options = array(&#039;multiple&#039; =&amp;gt; true, &#039;includefrontpage&#039; =&amp;gt; true);                                                           &lt;br /&gt;
    $mform-&amp;gt;addElement(&#039;course&#039;, &#039;mappedcourses&#039;, get_string(&#039;courses&#039;), $options); &lt;br /&gt;
&lt;br /&gt;
    // Tags&lt;br /&gt;
    //  Valid options are:                                                                                     &lt;br /&gt;
    //                       &#039;showstandard&#039; - boolean One of the core_tag_tag constants to say which tags to display&lt;br /&gt;
    //                       &#039;component&#039; - string The component name and itemtype define the tag area&lt;br /&gt;
    //                       &#039;itemtype&#039; - string The component name and itemtype define the tag area&lt;br /&gt;
    $mform-&amp;gt;addElement(&#039;tags&#039;, &#039;interests&#039;, get_string(&#039;interestslist&#039;), array(&#039;itemtype&#039; =&amp;gt; &#039;user&#039;, &#039;component&#039; =&amp;gt; &#039;core&#039;));     &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== checkbox ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;checkbox&#039;, &#039;ratingtime&#039;, get_string(&#039;ratingtime&#039;, &#039;forum&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a simple checkbox. The third parameter for this element is the label to display on the left side of the form. You can also supply a string as a fourth parameter to specify a label that will appear on the right of the element. Checkboxes and radio buttons can be grouped and have individual labels on their right.&lt;br /&gt;
&lt;br /&gt;
You can have a 5th parameter $attributes, as on other elements.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;BEWARE:&#039;&#039;&#039; Unchecked checkboxes return nothing at all (as if they didn&#039;t exist). This can surprise the unwary. You may wish to use advcheckbox instead, which does return a value when not checked. &#039;Advcheckbox&#039; eliminates this problem. &lt;br /&gt;
&lt;br /&gt;
==== advcheckbox ====&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;advcheckbox&#039;, &#039;ratingtime&#039;, get_string(&#039;ratingtime&#039;, &#039;forum&#039;), &#039;Label displayed after checkbox&#039;, array(&#039;group&#039; =&amp;gt; 1), array(0, 1));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Similar to the checkbox above, but with some important improvements:&lt;br /&gt;
&lt;br /&gt;
# The (optional) 5th parameter is a normal $attributes array, normally used to set HTML attributes for the &amp;lt;input&amp;gt; element. However, a special value of &#039;group&#039; can be given, which will add a class name to the element, and enable its grouping for a [[lib/formslib.php_add_checkbox_controller|checkbox controller]]&lt;br /&gt;
#The (optional) 6th parameter is an array of values that will be associated with the checked/unchecked state of the checkbox. With a normal checkbox you cannot choose that value, and in fact an unchecked checkbox will not even be sent with the form data.&lt;br /&gt;
#It returns a 0 value when unchecked. Compare with the ordinary checkbox which does not return anything at all.&lt;br /&gt;
&lt;br /&gt;
=== choosecoursefile ===&lt;br /&gt;
{{Moodle 1.9}}&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;choosecoursefile&#039;, &#039;mediafile&#039;, get_string(&#039;mediafile&#039;, &#039;lesson&#039;), array(&#039;courseid&#039;=&amp;gt;$COURSE-&amp;gt;id));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Choose a file from the course files area. The fourth option is a list of options for the element. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note: This has been superceded by [[#filepicker|filepicker]] in Moodle 2.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
array(&#039;courseid&#039; =&amp;gt;null,  //if it is null (default then use global $COURSE&lt;br /&gt;
      &#039;height&#039;   =&amp;gt;500,   // height of the popup window&lt;br /&gt;
      &#039;width&#039;    =&amp;gt;750,   // width of the popup window&lt;br /&gt;
      &#039;options&#039;  =&amp;gt;&#039;none&#039;); //options string for the pop up window &lt;br /&gt;
                          //eg. &#039;menubar=0,location=0,scrollbars,resizable&#039;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can also pass an optional 5th parameter of attributes, as for other elements. The most useful way of using that is something like &lt;br /&gt;
&amp;lt;code php&amp;gt;array(&#039;maxlength&#039; =&amp;gt; 255, &#039;size&#039; =&amp;gt; 48)&amp;lt;/code&amp;gt;&lt;br /&gt;
to control the maxlength / size of the text box (note size will default to 48 if not specified)&lt;br /&gt;
&lt;br /&gt;
Finally, as this element is a group containing two elements (button + value), you can add validation rules by using the &#039;&#039;&#039;addGroupRule()&#039;&#039;&#039; method in this (complex) way:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;$mform-&amp;gt;addGroupRule(&#039;elementname&#039;, array(&#039;value&#039; =&amp;gt; array(array(list, of, rule, params, but, fieldname))));&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Where: &#039;&#039;&#039;&amp;quot;elementname&amp;quot;&#039;&#039;&#039; is the name of the choosecoursefile group element, &#039;&#039;&#039;&amp;quot;value&amp;quot;&#039;&#039;&#039; is the name of the text field within the group and the &#039;&#039;&#039;&amp;quot;list, of, addrule, params, but, fieldname&amp;quot;&#039;&#039;&#039; is exactly that, the list of fields in the normal addRule() function but ommiting the first one, the fieldname.&lt;br /&gt;
&lt;br /&gt;
For example, the [http://cvs.moodle.org/moodle/mod/resource/type/file/resource.class.php?view=markup file/url resource type], uses one &amp;quot;choosecoursefile&amp;quot; element, and it controls the maximum length of the field (255) with this use of addGroupRule():&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;$mform-&amp;gt;addGroupRule(&#039;reference&#039;, array(&#039;value&#039; =&amp;gt; array(array(get_string(&#039;maximumchars&#039;, &#039;&#039;, 255), &#039;maxlength&#039;, 255, &#039;client&#039;))));&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== date_selector ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;date_selector&#039;, &#039;assesstimefinish&#039;, get_string(&#039;to&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a date selector. You can select a Day, Month and Year using a group of select boxes. The fourth param here is an array of options. The defaults for the options are :&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
array(&lt;br /&gt;
    &#039;startyear&#039; =&amp;gt; 1970, &lt;br /&gt;
    &#039;stopyear&#039;  =&amp;gt; 2020,&lt;br /&gt;
    &#039;timezone&#039;  =&amp;gt; 99,&lt;br /&gt;
    &#039;optional&#039;  =&amp;gt; false&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can override these defaults by supplying an array as fourth param with one or more keys with a value to override the default. You can supply a fifth param of attributes here as well.&lt;br /&gt;
&lt;br /&gt;
=== date_time_selector ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;date_time_selector&#039;, &#039;assesstimestart&#039;, get_string(&#039;from&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a group of select boxes to select a date (Day Month and Year) and time (Hour and Minute). When submitted, submitted data is processed and a timestamp is passed to $form-&amp;gt;get_data(); the fourth param here is an array of options. The defaults for the options are:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
array(&lt;br /&gt;
    &#039;startyear&#039; =&amp;gt; 1970, &lt;br /&gt;
    &#039;stopyear&#039;  =&amp;gt; 2020,&lt;br /&gt;
    &#039;timezone&#039;  =&amp;gt; 99,&lt;br /&gt;
    &#039;step&#039;      =&amp;gt; 5&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can override these defaults by supplying an array as fourth param with one or more keys with a value to override the default. You can supply a fifth param of attributes here as well.&lt;br /&gt;
&lt;br /&gt;
===duration===&lt;br /&gt;
{{Moodle 2.0}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $mform-&amp;gt;addElement(&#039;duration&#039;, &#039;timelimit&#039;, get_string(&#039;timelimit&#039;, &#039;quiz&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
This field type lets the user input an interval of time. It comprises a text field, where you can type a number, and a dropdown for selecting a unit (days, hours, minutes or seconds). When submitted the value is converted to a number of seconds.&lt;br /&gt;
&lt;br /&gt;
You can add a fourth parameter to give options. At the moment the only option supported is here is an array of options. The defaults for the options is:&lt;br /&gt;
&amp;lt;code php&amp;gt;array(&#039;optional&#039; =&amp;gt; true)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can also pass an optional 5th parameter of attributes, as for other elements. The most useful way of using that is something like &lt;br /&gt;
&amp;lt;code php&amp;gt;array(&#039;size&#039; =&amp;gt; 5)&amp;lt;/code&amp;gt;&lt;br /&gt;
to control the size of the text box.&lt;br /&gt;
&lt;br /&gt;
=== editor ===&lt;br /&gt;
&lt;br /&gt;
This replaces the old htmleditor field type. It allows the user to enter rich text content in a variety of formats.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;editor&#039;, &#039;fieldname&#039;, get_string(&#039;labeltext&#039;, &#039;langfile&#039;));&lt;br /&gt;
$mform-&amp;gt;setType(&#039;fieldname&#039;, PARAM_RAW);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
NOTE: It won&#039;t work properly without the setType() as shown.&lt;br /&gt;
&lt;br /&gt;
If you would like to let the user use the filepicker to upload images etc. that are used in the content, then see [[Using_the_File_API_in_Moodle_forms]].&lt;br /&gt;
&lt;br /&gt;
You can supply a fourth param to htmleditor of an array of options that are mostly related to file handling:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
array(&lt;br /&gt;
    &#039;subdirs&#039;=&amp;gt;0,&lt;br /&gt;
    &#039;maxbytes&#039;=&amp;gt;0,&lt;br /&gt;
    &#039;maxfiles&#039;=&amp;gt;0,&lt;br /&gt;
    &#039;changeformat&#039;=&amp;gt;0,&lt;br /&gt;
    &#039;context&#039;=&amp;gt;null,&lt;br /&gt;
    &#039;noclean&#039;=&amp;gt;0,&lt;br /&gt;
    &#039;trusttext&#039;=&amp;gt;0,&lt;br /&gt;
    &#039;enable_filemanagement&#039; =&amp;gt; true);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The option &#039;enable_filemanagement&#039; will display the file management button on true and remove it on false.&lt;br /&gt;
&lt;br /&gt;
To save the data if you don&#039;t care about files:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$formdata = $mform-&amp;gt;get_data();&lt;br /&gt;
$text     = $formdata-&amp;gt;fieldname[&#039;text&#039;];&lt;br /&gt;
$format   = $formdata-&amp;gt;fieldname[&#039;format&#039;];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note: Because the text editor might be &amp;quot;Atto&amp;quot; (depending on user preferences) and Atto has an &amp;quot;autosave&amp;quot; feature - it requires that the combination of $PAGE-&amp;gt;url and this elementid are unique. If not, the autosaved text for a different form may be restored into this form.&lt;br /&gt;
&lt;br /&gt;
=== file ===&lt;br /&gt;
&lt;br /&gt;
File upload input box with browse button. In the form definition type&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;file&#039;, &#039;attachment&#039;, get_string(&#039;attachment&#039;, &#039;forum&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
after form submission and validation use&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
if ($data = $mform-&amp;gt;get_data()) {&lt;br /&gt;
      ...&lt;br /&gt;
    $mform-&amp;gt;save_files($destination_directory);&lt;br /&gt;
      ...&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If there is no requirement to save the file, you can read the file contents directly into a string as follows...&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    $mform-&amp;gt;get_file_content(&#039;attachment&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you need advanced settings such as required file, different max upload size or name of uploaded file&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$this-&amp;gt;set_upload_manager(new upload_manager(&#039;attachment&#039;, true, false, $COURSE, false, 0, true, true, false));&lt;br /&gt;
            $mform-&amp;gt;addElement(&#039;file&#039;, &#039;attachment&#039;, get_string(&#039;attachment&#039;, &#039;forum&#039;));&lt;br /&gt;
            $mform-&amp;gt;addRule(&#039;attachment&#039;, null, &#039;required&#039;);&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
if ($data = $mform-&amp;gt;get_data()) {&lt;br /&gt;
      ...&lt;br /&gt;
    $mform-&amp;gt;save_files($destination_directory);&lt;br /&gt;
    $newfilename = $mform-&amp;gt;get_new_filename();&lt;br /&gt;
      ...&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When porting old code it is also possible to use the upload manager directly for processing of uploaded files.&lt;br /&gt;
&lt;br /&gt;
Please note that if using set_upload_manager() it must be before addElement(&#039;file&#039;,..).&lt;br /&gt;
&lt;br /&gt;
{{Moodle 2.0}}&lt;br /&gt;
File uploading was rewritten in 2.0. Please see inline docs for now. This page will be updated when the new API stabilises.&lt;br /&gt;
&lt;br /&gt;
===filepicker===&lt;br /&gt;
{{Moodle 2.0}}&lt;br /&gt;
General replacement of &#039;&#039;file&#039;&#039; element.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;filepicker&#039;, &#039;userfile&#039;, get_string(&#039;file&#039;), null, array(&#039;maxbytes&#039; =&amp;gt; $maxbytes, &#039;accepted_types&#039; =&amp;gt; &#039;*&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
See also [[Using the File API in Moodle forms]]&lt;br /&gt;
&lt;br /&gt;
=== hidden ===&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;hidden&#039;, &#039;reply&#039;, &#039;yes&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A hidden element. Set the element name (in this case &#039;&#039;&#039;reply&#039;&#039;&#039;) to the stated value (in this case &#039;&#039;&#039;yes&#039;&#039;&#039;).&lt;br /&gt;
&lt;br /&gt;
=== html ===&lt;br /&gt;
You can add arbitrary HTML to your Moodle form:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;html&#039;, &#039;&amp;lt;div class=&amp;quot;qheader&amp;quot;&amp;gt;&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
See [http://moodle.org/mod/forum/discuss.php?d=126935 &amp;quot;Question: Can I put a moodleform inside a table td?&amp;quot;] for a concrete example.&lt;br /&gt;
&lt;br /&gt;
=== htmleditor &amp;amp; format ===&lt;br /&gt;
&lt;br /&gt;
These elements are now deprecated. Please use the [[#editor|editor]] field type instead.&lt;br /&gt;
&lt;br /&gt;
===modgrade===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;modgrade&#039;, &#039;scale&#039;, get_string(&#039;grade&#039;), false);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
This is a custom element for selecting a grade for any activity module. The fourth argument is whether to include an option for no grade which has a value 0. This select box does include scales. The default is true, include no grade option.&lt;br /&gt;
&lt;br /&gt;
A helpbutton is automatically added.&lt;br /&gt;
&lt;br /&gt;
===modvisible===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;modvisible&#039;, &#039;visible&#039;, get_string(&#039;visible&#039;));&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
This is a custom element for selecting a grade visibility in an activity mod update form.&lt;br /&gt;
&lt;br /&gt;
===password===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;password&#039;, &#039;password&#039;, get_string(&#039;label&#039;), $attributes);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A password element. Fourth param is an array or string of attributes.&lt;br /&gt;
&lt;br /&gt;
===passwordunmask===&lt;br /&gt;
{{Moodle 1.9}}&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;passwordunmask&#039;, &#039;password&#039;, get_string(&#039;label&#039;), $attributes);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A password element with option to show the password in plaintext. Fourth param is an array or string of attributes.&lt;br /&gt;
&lt;br /&gt;
=== radio ===&lt;br /&gt;
{{Moodle 2.3}}&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$radioarray=array();&lt;br /&gt;
$radioarray[] = $mform-&amp;gt;createElement(&#039;radio&#039;, &#039;yesno&#039;, &#039;&#039;, get_string(&#039;yes&#039;), 1, $attributes);&lt;br /&gt;
$radioarray[] = $mform-&amp;gt;createElement(&#039;radio&#039;, &#039;yesno&#039;, &#039;&#039;, get_string(&#039;no&#039;), 0, $attributes);&lt;br /&gt;
$mform-&amp;gt;addGroup($radioarray, &#039;radioar&#039;, &#039;&#039;, array(&#039; &#039;), false);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Second param names the radio button and should be the same for each button in the group in order to toggle correctly. Third param would be the label for the form element but is generally ignored as this element will always be in a group which has it&#039;s own label. Fourth param is a string, a label to be displayed on the right of the element. The fifth is the value for this radio button. $attributes can be a string or an array of attributes.&lt;br /&gt;
&lt;br /&gt;
It is possible to add help to individual radio buttons but this requires a custom template to be defined for the group elements. See MDL-15571.&lt;br /&gt;
&lt;br /&gt;
Since 2.3 it cannot be statically called anymore, so we need to call createElement from $mform reference.&lt;br /&gt;
&lt;br /&gt;
==== setDefault ====&lt;br /&gt;
&lt;br /&gt;
To set the default for a radio button group as above use the following :&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;setDefault(&#039;yesno&#039;, 0);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This would make the default &#039;no&#039;.&lt;br /&gt;
&lt;br /&gt;
===select===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;select&#039;, &#039;type&#039;, get_string(&#039;forumtype&#039;, &#039;forum&#039;), $FORUM_TYPES, $attributes);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The fourth param for this element is an array of options for the select box. The keys are the values for the option and the value of the array is the text for the option. The fifth param $attributes is optional, see text element for description of attributes param.&lt;br /&gt;
&lt;br /&gt;
It is also possible to create a select with certain options disabled, using [http://stackoverflow.com/questions/2138089/how-can-i-use-quickform-to-add-disabled-select-options/2150275#2150275 this technique].&lt;br /&gt;
&lt;br /&gt;
You can set an &#039;onchange&#039; attribute when adding or creating the select element: &lt;br /&gt;
&lt;br /&gt;
$form-&amp;gt;addElement(&#039;select&#039;, &#039;iselTest&#039;, &#039;Test Select:&#039;, $arrayOfOptions, array(&#039;onchange&#039; =&amp;gt; &#039;javascript:myFunctionToDoSomething();&#039;));&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
====multi-select====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$select = $mform-&amp;gt;addElement(&#039;select&#039;, &#039;colors&#039;, get_string(&#039;colors&#039;), array(&#039;red&#039;, &#039;blue&#039;, &#039;green&#039;), $attributes);&lt;br /&gt;
$select-&amp;gt;setMultiple(true);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====setSelected=====&lt;br /&gt;
&lt;br /&gt;
To set the default selected item in a select element, you can use the &#039;setSelected&#039; method. The &#039;setSelected&#039; can either get a value or an array of values.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$options = array(&lt;br /&gt;
    &#039;ff0000&#039; =&amp;gt; &#039;Red&#039;,&lt;br /&gt;
    &#039;00ff00&#039; =&amp;gt; &#039;Green&#039;,&lt;br /&gt;
    &#039;0000ff&#039; =&amp;gt; &#039;Blue&#039;&lt;br /&gt;
);&lt;br /&gt;
$select = $mform-&amp;gt;addElement(&#039;select&#039;, &#039;colors&#039;, get_string(&#039;colors&#039;), $options);&lt;br /&gt;
// This will select the colour blue.&lt;br /&gt;
$select-&amp;gt;setSelected(&#039;0000ff&#039;);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Or for multiple-selection:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$skillsarray = array(&lt;br /&gt;
    &#039;val1&#039; =&amp;gt; &#039;Skill A&#039;,&lt;br /&gt;
    &#039;val2&#039; =&amp;gt; &#039;Skill B&#039;,&lt;br /&gt;
    &#039;val3&#039; =&amp;gt; &#039;Skill C&#039;&lt;br /&gt;
);&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;select&#039;, &#039;md_skills&#039;, get_string(&#039;skills&#039;, &#039;metadata&#039;), $skillsarray);&lt;br /&gt;
$mform-&amp;gt;getElement(&#039;md_skills&#039;)-&amp;gt;setMultiple(true);&lt;br /&gt;
// This will select the skills A and B.&lt;br /&gt;
$mform-&amp;gt;getElement(&#039;md_skills&#039;)-&amp;gt;setSelected(array(&#039;val1&#039;, &#039;val2&#039;));&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
However you probably don&#039;t want to do this. Instead you probably want to use setDefault, or set it using the form&#039;s setData method.&lt;br /&gt;
&lt;br /&gt;
===selectyesno===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;selectyesno&#039;, &#039;maxbytes&#039;, get_string(&#039;maxattachmentsize&#039;, &#039;forum&#039;));&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you want a yes / no select box this one automatically translates itself and has value 1 for yes and 0 for no.&lt;br /&gt;
&lt;br /&gt;
===selectwithlink===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$options = array();&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;selectwithlink&#039;, &#039;scaleid&#039;, get_string(&#039;scale&#039;), $options, null, &lt;br /&gt;
    array(&#039;link&#039; =&amp;gt; $CFG-&amp;gt;wwwroot.&#039;/grade/edit/scale/edit.php?courseid=&#039;.$COURSE-&amp;gt;id, &#039;label&#039; =&amp;gt; get_string(&#039;scalescustomcreate&#039;)));&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
select type element with options containing link&lt;br /&gt;
===static===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;static&#039;, &#039;description&#039;, get_string(&#039;description&#039;, &#039;exercise&#039;),&lt;br /&gt;
    get_string(&#039;descriptionofexercise&#039;, &#039;exercise&#039;, $COURSE-&amp;gt;students));&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a static element. It should be used with care if it is used to display a static piece of text with a label. The third param is the label and the fourth is the static text itself.&lt;br /&gt;
&lt;br /&gt;
===submit, reset and cancel===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
//normally you use add_action_buttons instead of this code&lt;br /&gt;
$buttonarray=array();&lt;br /&gt;
$buttonarray[] = $mform-&amp;gt;createElement(&#039;submit&#039;, &#039;submitbutton&#039;, get_string(&#039;savechanges&#039;));&lt;br /&gt;
$buttonarray[] = $mform-&amp;gt;createElement(&#039;reset&#039;, &#039;resetbutton&#039;, get_string(&#039;revert&#039;));&lt;br /&gt;
$buttonarray[] = $mform-&amp;gt;createElement(&#039;cancel&#039;);&lt;br /&gt;
$mform-&amp;gt;addGroup($buttonarray, &#039;buttonar&#039;, &#039;&#039;, &#039; &#039;, false);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A &#039;Submit&#039; type element is a submit type form element which will submit the form. A &#039;Reset&#039; will not submit the form but will reset any changes the user has made to form contents. A &#039;Cancel&#039; element cancels form submission. You need to have a branch in your code before you check for get_data() to check if submission has been cancelled with is_cancelled(); See the example on the usage page.&lt;br /&gt;
&lt;br /&gt;
You should name your submit and reset buttons &#039;submitbutton&#039; and &#039;resetbutton&#039; or something similar (not &#039;submit&#039; and &#039;reset&#039;). This avoids problems in JavaScript of collisions between form element names and names of JavaScript methods of the form object.&lt;br /&gt;
&lt;br /&gt;
====add_action_buttons($cancel = true, $submitlabel=null);====&lt;br /&gt;
&lt;br /&gt;
You will normally use this helper function which is a method of moodleform to add all the &#039;action&#039; buttons to the end of your form. A boolean parameter allow you to specify whether to include a cancel button and specify the label for your submit button (pass the result of get_string). Default for the submit button label is get_string(&#039;savechanges&#039;). Note the &#039;&#039;&#039;$this&#039;&#039;&#039; not &#039;&#039;&#039;$mform&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$this-&amp;gt;add_action_buttons();&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===text===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;text&#039;, &#039;name&#039;, get_string(&#039;forumname&#039;, &#039;forum&#039;), $attributes);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
For a simple text input element. (For text labels, use the &#039;static&#039; element.)  Your fourth parameter here can be a string or array of attributes for the text element. The following are equivalent :&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$attributes=&#039;size=&amp;quot;20&amp;quot;&#039;;&lt;br /&gt;
$attributes=array(&#039;size&#039;=&amp;gt;&#039;20&#039;);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
Generally you are encouraged to use CSS instead of using attributes for styling.&lt;br /&gt;
&lt;br /&gt;
A format element can be used as a format select box. It will be non-selectable if you&#039;re using an html editor.&lt;br /&gt;
&lt;br /&gt;
The third param for this element is $useHtmlEditor and it defaults to null in which case an html editor is used if the browser and user profile support it.&lt;br /&gt;
&lt;br /&gt;
====RTL support====&lt;br /&gt;
{{Moodle 3.2}}&lt;br /&gt;
&lt;br /&gt;
As of Moodle 3.2, some form elements have been refined to better support right-to-left languages. In RTL, most fields should not have their direction flipped, a URL, a path to a file, a number, ... are always displayed LTR. Input fields and text areas now will best guess whether they should be forced to be displayed in LTR based on the PARAM type associated with it. You can call:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;setForceLtr(&#039;name&#039;, true/false);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
on some form fields (like &#039;text&#039;) to manually set the value, and for directionality.&lt;br /&gt;
&lt;br /&gt;
====float====&lt;br /&gt;
{{Moodle 3.7}}&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;float&#039;, &#039;defaultmark&#039;, get_string(&#039;defaultmark&#039;, &#039;question&#039;), $attributes);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
Use the float element if you want a text box to get a floating point number. This element automatically supports localised decimal separators. You don&#039;t need to use setType with the float element.&lt;br /&gt;
&lt;br /&gt;
===textarea===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;textarea&#039;, &#039;introduction&#039;, get_string(&amp;quot;introtext&amp;quot;, &amp;quot;survey&amp;quot;), &#039;wrap=&amp;quot;virtual&amp;quot; rows=&amp;quot;20&amp;quot; cols=&amp;quot;50&amp;quot;&#039;);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A textarea element. If you want an htmleditor use htmleditor element. Fourth element here is a string or array of attributes.&lt;br /&gt;
&lt;br /&gt;
===recaptcha===&lt;br /&gt;
{{Moodle 1.9}}&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;recaptcha&#039;, &#039;recaptcha_field_name&#039;, $attributes);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Use this recaptcha element to reduce the spam risk in your forms. Third element here is a string or array of attributes. Take care to get an API key from http://recaptcha.net/api/getkey before using this element.&lt;br /&gt;
&lt;br /&gt;
To check whether recaptcha is enabled at site level use:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
if (!empty($CFG-&amp;gt;recaptchapublickey) &amp;amp;&amp;amp; !empty($CFG-&amp;gt;recaptchaprivatekey)) {&lt;br /&gt;
    //recaptcha is enabled&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===tags===&lt;br /&gt;
{{Moodle 2.0}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;tags&#039;, &#039;field_name&#039;, $lable, $options, $attributes);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Used for editing a list of tags, for example on a blog post.&lt;br /&gt;
&lt;br /&gt;
There is only one option available, &#039;display&#039;, which should be set to one of the contstants MoodleQuickForm_tags::ONLYOFFICIAL, NOOFFICIAL or DEFAULTUI. This controls whether the official tags are listed for easy selection, or a text area where arbitrary tags may be typed, or both. The default is both.&lt;br /&gt;
&lt;br /&gt;
The value should be set/returned as an array of tags.&lt;br /&gt;
&lt;br /&gt;
===grading===&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;grading&#039;, &#039;advancedgrading&#039;, get_string(&#039;grade&#039;).&#039;:&#039;, array(&#039;gradinginstance&#039; =&amp;gt; $gradinginstance));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Custom element for advanced grading plugins.&lt;br /&gt;
&lt;br /&gt;
When adding the &#039;grading&#039; element to the form, developer must pass an object of class gradingform_instance as $attributes[&#039;gradinginstance&#039;]. Otherwise an exception will be thrown.&lt;br /&gt;
&lt;br /&gt;
===questioncategory===&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;questioncategory&#039;, &#039;category&#039;, get_string(&#039;category&#039;, &#039;question&#039;),&lt;br /&gt;
    array(&#039;contexts&#039;=&amp;gt;$contexts, &#039;top&#039;=&amp;gt;true, &#039;currentcat&#039;=&amp;gt;$currentcat, &#039;nochildrenof&#039;=&amp;gt;$currentcat));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Creates a drop down element to select a question category.&lt;br /&gt;
&lt;br /&gt;
Options are:&lt;br /&gt;
&#039;&#039;&#039;contexts&#039;&#039;&#039; - (required) context in which question appears&lt;br /&gt;
&#039;&#039;&#039;currentcat&#039;&#039;&#039; - (optional) course category&lt;br /&gt;
&#039;&#039;&#039;top&#039;&#039;&#039; - (optional) if true will put top categories on top&lt;br /&gt;
&#039;&#039;&#039;nochildrenof&#039;&#039;&#039; - (optional) Format categories into an indented list reflecting the tree structure&lt;br /&gt;
&lt;br /&gt;
=== filetypes ===&lt;br /&gt;
{{Moodle 3.4}}&lt;br /&gt;
Available since Moodle 3.4&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;filetypes&#039;, &#039;allowedfiletypes&#039;, get_string(&#039;allowedfiletypes&#039;, &#039;tool_myplugin&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Creates  an input element allowing the user to specify file types for the given purpose. The typical scenario is a setting that allows the teacher define a list of allowed file types submitted by students.&lt;br /&gt;
&lt;br /&gt;
The element allows the user to either type the list of filetypes manually, or select the types from the list. Also supported is selecting the whole group of file types - such as &amp;quot;image&amp;quot;. The element integrates with the [[Core filetypes]] system so all default types and groups are presented, as well as those [[:en:Working with files#Site administration settings|defined locally by the admin]].&lt;br /&gt;
&lt;br /&gt;
As the list can be types in manually, the form processing code should always normalize it first via the provided utility methods:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$formdata = $mform-&amp;gt;get_data();&lt;br /&gt;
$filetypesutil = new \core_form\filetypes_util();&lt;br /&gt;
$allowedfiletypes = $filetypesutil-&amp;gt;normalize_file_types($formdata-&amp;gt;allowedfiletypes);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This always returns an array of recognized valid values. The original list can be separated by whitespace, end of lines, commas, colons and semicolons. During the normalization, values are converted to lowercase, empty valies and duplicates are removed. Glob evaluation is not supported.&lt;br /&gt;
&lt;br /&gt;
The normalization should also happen if the previously defined list had been saved to the database and re-read for actual usage. The normalization output value can be directly used as the accepted_types option for the filepicker.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$filetypesutil = new \core_form\filetypes_util();&lt;br /&gt;
$options[&#039;accepted_types&#039;] = $filetypesutil-&amp;gt;normalize_file_types($allowedfiletypes);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, user input is validated against the list of known file types and groups. This validation can be disabled via options.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Supported options&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
; onlytypes : Allow selection from these file types only; for example [&#039;onlytypes&#039; =&amp;gt; [&#039;web_image&#039;]].&lt;br /&gt;
; allowall : Allow to select &#039;All file types&#039;, defaults to true. Does not apply with onlytypes are set.&lt;br /&gt;
; allowunknown : Skip implicit validation against the list of known file types.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;filetypes&#039;, &#039;doctypes&#039;, get_string(&#039;doctypes&#039;, &#039;tool_myplugin&#039;), [&#039;onlytypes&#039; =&amp;gt; [&#039;document&#039;], &#039;allowunknown&#039; =&amp;gt; true]);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==addGroup==&lt;br /&gt;
&lt;br /&gt;
A &#039;group&#039; in formslib is just a group of elements that will have a label and will be included on one line. &lt;br /&gt;
&lt;br /&gt;
For example typical code to include a submit and cancel button on the same line : &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$buttonarray=array();&lt;br /&gt;
$buttonarray[] =&amp;amp; $mform-&amp;gt;createElement(&#039;submit&#039;, &#039;submitbutton&#039;, get_string(&#039;savechanges&#039;));&lt;br /&gt;
$buttonarray[] =&amp;amp; $mform-&amp;gt;createElement(&#039;submit&#039;, &#039;cancel&#039;, get_string(&#039;cancel&#039;));&lt;br /&gt;
$mform-&amp;gt;addGroup($buttonarray, &#039;buttonar&#039;, &#039;&#039;, array(&#039; &#039;), false);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You use the same arguments for createElement as you do for addElement. Any label for the element in the third argument is normally ignored (but not in the case of the submit buttons above where the third argument is not for a label but is the text for the button).&lt;br /&gt;
&lt;br /&gt;
Here&#039;s a bad example (don&#039;t do this for real, use the &#039;optional&#039; =&amp;gt; true option of the date element): putting a date_selector (which is itself a group of elements) and a checkbox on the same line, note that you can disable every element in the group using the group name &#039;availablefromgroup&#039; but it doesn&#039;t disable the controlling element the &#039;availablefromenabled&#039; checkbox:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$availablefromgroup=array();&lt;br /&gt;
$availablefromgroup[] =&amp;amp; $mform-&amp;gt;createElement(&#039;date_selector&#039;, &#039;availablefrom&#039;, &#039;&#039;);&lt;br /&gt;
$availablefromgroup[] =&amp;amp; $mform-&amp;gt;createElement(&#039;checkbox&#039;, &#039;availablefromenabled&#039;, &#039;&#039;, get_string(&#039;enable&#039;));&lt;br /&gt;
$mform-&amp;gt;addGroup($availablefromgroup, &#039;availablefromgroup&#039;, get_string(&#039;availablefromdate&#039;, &#039;data&#039;), &#039;&amp;amp;nbsp;&#039;, false);&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;availablefromgroup&#039;, &#039;availablefromenabled&#039;);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* If you want to put a group inside another array so that you can repeat items, use createElement instead of addGroup:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$group = $mform-&amp;gt;createElement(&#039;group&#039;, &#039;groupname&#039;, get_string(&#039;label&#039;), $groupitems);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* By default, groups modify the names of elements inside them by appending a number. This is often unhelpful, for example if you want to use disabledIf on the element. To prevent this behaviour, set the last parameter to false when creating a group.:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$group = $mform-&amp;gt;createElement(&#039;group&#039;, &#039;groupname&#039;, get_string(&#039;label&#039;), $groupitems, null, false);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==addRule==&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addRule(&#039;elementname&#039;, get_string(&#039;error&#039;), &#039;rule type&#039;, &#039;extraruledata&#039;, &#039;server&#039;(default), false, false);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The first param(element) is an element name and second(message) is the error message that will be displayed to the user.&lt;br /&gt;
The third parameter(type) is the type of rule. The fourth param(format) is used for extra data needed with some rules such as minlength and regex. The fifth parameter(validation) validates input data on server or client side, if validation is done on client side then it will be checked on the server side as well.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
 * @param    string     $element       Form element name&lt;br /&gt;
 * @param    string     $message       Message to display for invalid data&lt;br /&gt;
 * @param    string     $type          Rule type, use getRegisteredRules() to get types&lt;br /&gt;
 * @param    string     $format        (optional)Required for extra rule data&lt;br /&gt;
 * @param    string     $validation    (optional)Where to perform validation: &amp;quot;server&amp;quot;, &amp;quot;client&amp;quot;&lt;br /&gt;
 * @param    boolean    $reset         Client-side validation: reset the form element to its original value if there is an error?&lt;br /&gt;
 * @param    boolean    $force         Force the rule to be applied, even if the target form element does not exist&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Common Rule Types&#039;&#039;&#039;&lt;br /&gt;
* required &lt;br /&gt;
* maxlength&lt;br /&gt;
* minlength&lt;br /&gt;
* rangelength&lt;br /&gt;
* email&lt;br /&gt;
* regex&lt;br /&gt;
* lettersonly&lt;br /&gt;
* alphanumeric&lt;br /&gt;
* numeric&lt;br /&gt;
* nopunctuation&lt;br /&gt;
* nonzero&lt;br /&gt;
* callback&lt;br /&gt;
* compare&lt;br /&gt;
&lt;br /&gt;
===Server side and Client side===&lt;br /&gt;
In case you use the &#039;&#039;Client side&#039;&#039; validation option, you can mainly check for an empty or not input field. unless you write some &#039;&#039;Client side&#039;&#039; code which will probably be JavaScript functions to verify the data inside the input fields before it is submitted to the server. It could save some time if those functions are short, simple and quick to compute.&lt;br /&gt;
In case you need a more complex validation checks which relay on Moodle&#039;s internal PHP libraries (or other/external PHP libraries) you better use the &#039;&#039;Server side&#039;&#039; validation checks. Where you can query the DB, write complex PHP validation functions and much much more, that are not available (easily) when using JavaScript on the client&#039;s side.&lt;br /&gt;
&lt;br /&gt;
==addHelpButton==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addHelpButton(&#039;api_key_field&#039;, &#039;api_key&#039;, &#039;block_extsearch&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The following parameters are expected:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * @param $elementname The name of the form element to add the help button for&lt;br /&gt;
 * @param $identifier The identifier for the help string and its title (see below)&lt;br /&gt;
 * @param $component The component name to look for the help string in&lt;br /&gt;
 */&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# get_string($identifier, $component) // The title of the help page&lt;br /&gt;
# get_string(&amp;quot;{$identifier}_help&amp;quot;, $component) // The content of the help page&lt;br /&gt;
&lt;br /&gt;
So you will need to have &#039;&#039;&#039;$identifier&#039;&#039;&#039; and &#039;&#039;&#039;{$identifier}_help&#039;&#039;&#039; defined in order for the help button to be created properly. For example the multiple choice question editing form has a button for shuffling the answers. &lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addHelpButton(&#039;shuffleanswers&#039;, &#039;shuffleanswers&#039;, &#039;qtype_multichoice&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
and so the language file includes the strings&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$string[&#039;shuffleanswers&#039;] = &#039;Shuffle the choices?&#039;; &lt;br /&gt;
$string[&#039;shuffleanswers_help&#039;] = &#039;If enabled,.....&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
You can also add the language string like&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$string[&#039;shuffleanswers_link&#039;] = &#039;question/shuffleanswers&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
to add a link to more help on Moodle docs. See [[String_API]] for more information about help icons.&lt;br /&gt;
&lt;br /&gt;
==setDefault==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;select&#039;, &#039;grade&#039;, get_string(&#039;gradeforsubmission&#039;, &#039;exercise&#039;), $grades);&lt;br /&gt;
$mform-&amp;gt;setHelpButton(&#039;grade&#039;, array(&#039;grade&#039;, get_string(&#039;gradeforsubmission&#039;, &#039;exercise&#039;), &#039;exercise&#039;));&lt;br /&gt;
$mform-&amp;gt;setDefault(&#039;grade&#039;, 100);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Set the default of the form value with setDefault($elementname, $value); where elementname is the elementname whose default you want to set and $value is the default to set. We set the defaults for the form in definition(). This default is what is used if no data is loaded into the form with set_data(); eg. on display of the form for an &#039;add&#039; rather than &#039;update&#039; function.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
$mform-&amp;gt;addElement(&#039;editor&#039;, &#039;desc&#039;, get_string(&#039;description&#039;));     &lt;br /&gt;
$mform-&amp;gt;setDefault(&#039;desc&#039;, array(&#039;text&#039;=&amp;gt;$defaulttext));&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that when setting the default for an editor element you must use an array to define the default &amp;quot;text&amp;quot; value as shown above.&lt;br /&gt;
&lt;br /&gt;
==disabledIf==&lt;br /&gt;
&lt;br /&gt;
For any element or groups of element in a form you can conditionally disable the group or individual element depending on conditions.&lt;br /&gt;
&lt;br /&gt;
You can use $mform-&amp;gt;disabledIf($elementName, $dependentOn, $condition = &#039;notchecked&#039;, $value=&#039;1&#039;)&lt;br /&gt;
&lt;br /&gt;
* elementname can be a group. If you specify a group all elements in the group will be disabled (if dependentOn is in elementname group that is ignored and not disabled). These are the element names you&#039;ve used as the second argument in addElement or addGroup.&lt;br /&gt;
* dependentOn is the actual name of the element as it will appear in html. This can be different to the name used in addGroup particularly but also addElement where you&#039;re adding a complex element like a date_selector. Check the html of your page. You typically make the depedentOn a checkbox or select box.&lt;br /&gt;
* $condition will be &#039;notchecked&#039;, &#039;checked&#039;, &#039;noitemselected&#039;, &#039;eq&#039;, &#039;in&#039; or, if it is anything else, we test for &#039;neq&#039;.&lt;br /&gt;
** If $condition is &#039;eq&#039; or &#039;neq&#039; then we check the value of the dependentOn field and check for equality (==) or nonequality (!=) in js&lt;br /&gt;
** If $condition is &#039;checked&#039; or &#039;notchecked&#039; then we check to see if a checkbox is checked or not.&lt;br /&gt;
** If $condition is &#039;in&#039; then we check to see if a selected item is in the given list or not. (This was introduced in Moodle 2.7+)&lt;br /&gt;
** If $condition is &#039;noitemselected&#039; then we check to see whether nothing is selected in a dropdown list.&lt;br /&gt;
&lt;br /&gt;
Examples:&lt;br /&gt;
 // Disable my control unless a checkbox is checked.&lt;br /&gt;
 $mform-&amp;gt;disabledIf(&#039;mycontrol&#039;, &#039;somecheckbox&#039;);&lt;br /&gt;
 &lt;br /&gt;
 // Disable my control if a checkbox &#039;&#039;&#039;is&#039;&#039;&#039; checked.&lt;br /&gt;
 $mform-&amp;gt;disabledIf(&#039;mycontrol&#039;, &#039;somecheckbox&#039;, &#039;checked&#039;);&lt;br /&gt;
 &lt;br /&gt;
 // Disable my control when a dropdown has value 42.&lt;br /&gt;
 $mform-&amp;gt;disabledIf(&#039;mycontrol&#039;, &#039;someselect&#039;, &#039;eq&#039;, 42);&lt;br /&gt;
&lt;br /&gt;
 // Disable my control unless a dropdown has value 42.&lt;br /&gt;
 $mform-&amp;gt;disabledIf(&#039;mycontrol&#039;, &#039;someselect&#039;, &#039;neq&#039;, 42);&lt;br /&gt;
&lt;br /&gt;
The possible choices here are in the dependency manager in lib/form/form.js.&lt;br /&gt;
===A tricky case===&lt;br /&gt;
&lt;br /&gt;
You need to take care with disabledIf if you plan to use it with groups of checkboxes.&lt;br /&gt;
&lt;br /&gt;
Let&#039;s say you have a group of 5 checkboxes and you want to enable a depending item such as a drop down menu only when the first and the last checkboxes are selected.&lt;br /&gt;
&lt;br /&gt;
To fix ideas:&lt;br /&gt;
&lt;br /&gt;
If the selection in the checkboxes group is:&lt;br /&gt;
&lt;br /&gt;
 mycheck_01 == 1&lt;br /&gt;
 mycheck_02 == 0&lt;br /&gt;
 mycheck_03 == 0&lt;br /&gt;
 mycheck_04 == 0&lt;br /&gt;
 mycheck_05 == 1&lt;br /&gt;
&lt;br /&gt;
the depending item must be enabled while ANY OTHER COMBINATION must disable the drop down menu.&lt;br /&gt;
&lt;br /&gt;
The following code will, apparently, fail:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_01&#039;, &#039;neq&#039;, &#039;1&#039;);&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_02&#039;, &#039;neq&#039;, &#039;0&#039;);&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_03&#039;, &#039;neq&#039;, &#039;0&#039;);&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_04&#039;, &#039;neq&#039;, &#039;0&#039;);&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_05&#039;, &#039;neq&#039;, &#039;1&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In fact, once you get the drop down menu enabled, you are free to unselect mycheck_01 whilst still having the depending item enabled.&lt;br /&gt;
This apparent bug occurs because a non-checked checkbox behaves like a non existing mform element. So the js code will not find the element &amp;quot;mycheck_01&amp;quot; and will not apply the corresponding rule.&lt;br /&gt;
&lt;br /&gt;
A working solution for this kind of issue seems to be:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_01&#039;, &#039;notchecked&#039;);&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_02&#039;, &#039;checked&#039;);&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_03&#039;, &#039;checked&#039;);&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_04&#039;, &#039;checked&#039;);&lt;br /&gt;
$mform-&amp;gt;disabledIf(&#039;dropdownmenu&#039;, &#039;mycheck_05&#039;, &#039;notchecked&#039;);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
To see a failing example as the one described, try the attachments provided in MDL-38975. See also in MDL-38975 for the working solution in action with modifications suggested by Eloy.&lt;br /&gt;
&lt;br /&gt;
==hideIf==&lt;br /&gt;
{{Moodle 3.4}}&lt;br /&gt;
For any element or groups of element in a form you can conditionally hide the group or individual element depending on conditions.&lt;br /&gt;
This uses the same syntax as disabledIf just with hideIf instead.&lt;br /&gt;
&lt;br /&gt;
==setType==&lt;br /&gt;
&lt;br /&gt;
PARAM_* types are used to specify how a submitted variable should be cleaned. These should be used for get parameters such as id, course etc. which are used to load a page and also with setType(); method. Every form element should have a type specified except select, radio box and checkbox elements, these elements do a good job of cleaning themselves (only specified options are allowed as user input).&lt;br /&gt;
&lt;br /&gt;
===Most Commonly Used PARAM_* Types===&lt;br /&gt;
&lt;br /&gt;
These are the most commonly used PARAM_* types and their proper uses. More types can be seen in moodlelib.php starting around line 100.&lt;br /&gt;
&lt;br /&gt;
* PARAM_CLEAN is deprecated and you should try to use a more specific type.&lt;br /&gt;
* PARAM_TEXT should be used for cleaning data that is expected to contain multi-lang content. It will strip all html tags. But will still let tags for multilang support through.&lt;br /&gt;
* PARAM_NOTAGS should be used for cleaning data that is expected to be plain text. It will strip *all* html type tags. It will *not* let tags for multilang support through. This should be used for instance for email addresses where no multilang support is appropriate.&lt;br /&gt;
* PARAM_RAW means no cleaning whatsoever, it is used mostly for data from the html editor. Data from the editor is later cleaned before display using format_text() function. PARAM_RAW can also be used for data that is validated by some other way or printed by p() or s().&lt;br /&gt;
* PARAM_INT should be used for integers. PARAM_FLOAT is also available for decimal numbers but is not recommended for user input since it does not work for languages that use , as a decimal separator.&lt;br /&gt;
* PARAM_ACTION is an alias of PARAM_ALPHA and is used for hidden fields specifying form actions.&lt;br /&gt;
&lt;br /&gt;
==disable_form_change_checker==&lt;br /&gt;
&lt;br /&gt;
By default, any Moodle form will pop-up an &amp;quot;Are you sure?&amp;quot; alert if you make some changes and then try to leave the page without saving. Occasionally, that is undesirable, in which case you can call&lt;br /&gt;
&lt;br /&gt;
 $mform-&amp;gt;disable_form_change_checker()&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [http://www.midnighthax.com/quickform.php PEAR HTML QuickForm Getting Started Guide] by Keith Edmunds of Midnighthax.com&lt;br /&gt;
* [http://pear.php.net/manual/en/package.html.html-quickform.php PEAR::HTML_QuickForm manual]&lt;br /&gt;
&lt;br /&gt;
[[Category:Formslib]]&lt;br /&gt;
[[Category:Interfaces]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55909</id>
		<title>Moodle App Plugins Development Guide</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55909"/>
		<updated>2019-04-15T09:48:30Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Moodle plugins with mobile support */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
==Before 3.5==&lt;br /&gt;
&lt;br /&gt;
Since Moodle 3.1 Moodle plugins could be supported in the Mobile app, but only by writing an Angular JS/Ionic module, compiling it to a zip, and including that in your plugin. See [[Moodle Mobile Remote add-ons|Remote add-ons]] for details.&lt;br /&gt;
&lt;br /&gt;
In Moodle 3.5 the app switched to a new way to support plugins that was much easier for developers.&lt;br /&gt;
* This new way will allow developers to support plugins using PHP code, templates and Ionic markup (html components).&lt;br /&gt;
* The use of JavaScript is optional (but some type of advanced plugins may require it)&lt;br /&gt;
* Developers won’t need to set up a Mobile development environment, they will be able to test using the latest version of the official app (although setting up a local Mobile environment is recommended for complex plugins).&lt;br /&gt;
&lt;br /&gt;
This means that remote add-ons won’t be necessary anymore, and developers won’t have to learn Ionic 3 / Angular and set up a new mobile development environment to migrate them. Plugins using the old Remote add-ons mechanism will have to be migrated to the new simpler way (following this documentation)&lt;br /&gt;
&lt;br /&gt;
This new way is natively supported in Moodle 3.5. For previous versions you will need to install the Moodle Mobile Additional Features plugin.&lt;br /&gt;
&lt;br /&gt;
==How it works==&lt;br /&gt;
&lt;br /&gt;
The overall idea is to allow Moodle plugins to extend different areas in the app with &#039;&#039;just PHP server side&#039;&#039; code and Ionic 3 markup (custom html elements that are called components) using a set of custom Ionic directives and components.&lt;br /&gt;
&lt;br /&gt;
Developers will have to:&lt;br /&gt;
# Create a db/mobile.php file in their plugins. In this file developers will be able to indicate which areas of the app they want to extend, for example, adding a new option in the main menu, implementing an activity module not supported, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document.&lt;br /&gt;
# Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered (html). The template should use [https://ionicframework.com/docs/components/ Ionic components] so that it looks native (custom html elements) but it can be generated using mustache templates. &lt;br /&gt;
&lt;br /&gt;
Let’s clarify some points:&lt;br /&gt;
&lt;br /&gt;
* You don’t need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace.&lt;br /&gt;
* Those functions will be exported via the Web Service function tool_mobile_get_content&lt;br /&gt;
* As arguments of your functions you will always receive the userid, some relevant details of the app (app version, current language in the app, etc…) and some specific data depending on the type of plugin (courseid, cmid, …).&lt;br /&gt;
* We provide a list of custom Ionic components and directives (html tags) that will provide dynamic behaviour, like indicating that you are linking a file that can be downloaded, or to allow a transition to new pages into the app calling a specific function in the server, submit form data to the server  etc..&lt;br /&gt;
&lt;br /&gt;
==Types of plugins==&lt;br /&gt;
&lt;br /&gt;
We could classify all the plugins in 3 different types:&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_when_requested.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template of your plugin will be generated and downloaded when the user opens your plugin in the app. This means that your function will receive some context params. For example, if you&#039;re developing a course module plugin you will receive the courseid and the cmid (course module ID). You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_on_login.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template for your plugin will be downloaded when the user logins in the app and will be stored in the device. This means that your function will not receive any context params, and you need to return a generic template that will be built with JS data like the ones in the Mobile app. When the user opens a page that includes your plugin, your template will receive the required JS data and your template will be rendered. You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
You can always implement your whole plugin yourself using Javascript instead of using our API. In fact, this is required if you want to implement some features like capturing links in the Mobile app. You can see the list of delegates that only support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
==Step by step example==&lt;br /&gt;
&lt;br /&gt;
In this example, we are going to update an existing plugin ([https://github.com/markn86/moodle-mod_certificate Certificate activity module]) that currently uses a Remote add-on.&lt;br /&gt;
This is a simple activity module that displays the certificate issued for the current user along with the list of the dates of previously issued certificates. It also stores in the course log that the user viewed a certificate. This module also works offline: when the user downloads the course or activity, the data is pre-fetched and can be viewed offline.&lt;br /&gt;
&lt;br /&gt;
The example code can be downloaded from here (https://github.com/markn86/moodle-mod_certificate/commit/003fbac0d80fd96baf428255500980bf95a7a0d6)&lt;br /&gt;
&lt;br /&gt;
TIP: Make sure to ([https://docs.moodle.org/35/en/Developer_tools#Purge_all_caches purge all cache]) after making an edit to one of the following files for your changes to be taken into account.&lt;br /&gt;
&lt;br /&gt;
===Step 1. Update the db/mobile.php file===&lt;br /&gt;
In this case, we are updating an existing file but for new plugins, you should create this new file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$addons = [&lt;br /&gt;
    &#039;mod_certificate&#039; =&amp;gt; [ // Plugin identifier&lt;br /&gt;
        &#039;handlers&#039; =&amp;gt; [ // Different places where the plugin will display content.&lt;br /&gt;
            &#039;coursecertificate&#039; =&amp;gt; [ // Handler unique name (alphanumeric).&lt;br /&gt;
                &#039;displaydata&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;icon&#039; =&amp;gt; $CFG-&amp;gt;wwwroot . &#039;/mod/certificate/pix/icon.gif&#039;,&lt;br /&gt;
                    &#039;class&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
                ],&lt;br /&gt;
       &lt;br /&gt;
                &#039;delegate&#039; =&amp;gt; &#039;CoreCourseModuleDelegate&#039;, // Delegate (where to display the link to the plugin)&lt;br /&gt;
                &#039;method&#039; =&amp;gt; &#039;mobile_course_view&#039;, // Main function in \mod_certificate\output\mobile&lt;br /&gt;
                &#039;offlinefunctions&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;mobile_course_view&#039; =&amp;gt; [],&lt;br /&gt;
                    &#039;mobile_issues_view&#039; =&amp;gt; [],&lt;br /&gt;
                ]. // Function that needs to be downloaded for offline.&lt;br /&gt;
            ],&lt;br /&gt;
        ],&lt;br /&gt;
        &#039;lang&#039; =&amp;gt; [ // Language strings that are used in all the handlers.&lt;br /&gt;
            [&#039;pluginname&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;summaryofattempts&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;getcertificate&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;requiredtimenotmet&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;viewcertificateviews&#039;, &#039;certificate&#039;],&lt;br /&gt;
        ],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Plugin identifier:&lt;br /&gt;
: A unique name for the plugin, it can be anything (there’s no need to match the module name).&lt;br /&gt;
 &lt;br /&gt;
;Handlers  (Different places where the plugin will display content):&lt;br /&gt;
: A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).&lt;br /&gt;
&lt;br /&gt;
; Display data:&lt;br /&gt;
: This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields), in this case we are indicating the module icon.&lt;br /&gt;
	&lt;br /&gt;
; Delegate&lt;br /&gt;
: Where to display the link to the plugin, see the Delegates chapter in this documentation for all the possible options.&lt;br /&gt;
&lt;br /&gt;
; Method:&lt;br /&gt;
: This is the method in the Moodle \(component)\output\mobile class to be executed the first time the user clicks in the new option displayed in the app.	&lt;br /&gt;
&lt;br /&gt;
; Offlinefunctions&lt;br /&gt;
: These are the functions that need to be downloaded for offline usage. This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the mobile.php file. &lt;br /&gt;
: In our example, downloading for offline access will mean that we&#039;ll execute the functions for getting the certificate and issued certificates passing as parameters the current userid (and courseid when we are using the mod or course delegate). If we have the result of those functions stored in the app, we&#039;ll be able to display the certificate information even if the user is offline.&lt;br /&gt;
: Offline functions will be mostly used to display information for final users, any further interaction with the view won’t be supported offline (for example, trying to send information when the user is offline).&lt;br /&gt;
: You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently userid and courseid)&lt;br /&gt;
: Prefetching the module will also download all the files returned by the methods in these offline functions (in the &#039;&#039;files&#039;&#039; array).&lt;br /&gt;
: Note: If your functions use additional custom parameters (for example, if you implement multiple pages within a module&#039;s view function by using a &#039;page&#039; parameter in addition to the usual cmid, courseid, userid) then the app will not know which additional parameters to supply. In this case, do not list the function in offlinefunctions; instead, you will need to manually implement a [[#Module_prefetch_handler|module prefetch handler]].&lt;br /&gt;
&lt;br /&gt;
;Lang:&lt;br /&gt;
: &amp;lt;nowiki&amp;gt;The language pack string ids used in the plugin by all the handlers. Normally these will be strings from your own plugin, however, you can list any strings you need here (e.g. [&#039;cancel&#039;, &#039;moodle&#039;]). If you do this, be warned that in the app you will then need to refer to that string as {{ &#039;plugin.myplugin.cancel&#039; | translate }} (not {{ &#039;plugin.moodle.cancel&#039; | translate }})&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
: Please only include the strings you actually need. The Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform, and this will then be cached, so listing too many strings is very wasteful.&lt;br /&gt;
&lt;br /&gt;
There are additional attributes supported by the mobile.php list, see “Mobile.php supported options” section below.&lt;br /&gt;
&lt;br /&gt;
===Step 2. Creating the main function===&lt;br /&gt;
&lt;br /&gt;
The main function displays the current issued certificate (or several warnings if it’s not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates.&lt;br /&gt;
&lt;br /&gt;
All the functions must be created in the plugin or subsystem classes/output directory, the name of the class must be mobile.&lt;br /&gt;
&lt;br /&gt;
For this example (mod_certificate plugin) the namespace name will be mod_certificate\output.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
namespace mod_certificate\output;&lt;br /&gt;
&lt;br /&gt;
defined(&#039;MOODLE_INTERNAL&#039;) || die();&lt;br /&gt;
&lt;br /&gt;
use context_module;&lt;br /&gt;
use mod_certificate_external;&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Mobile output class for certificate&lt;br /&gt;
 *&lt;br /&gt;
 * @package    mod_certificate&lt;br /&gt;
 * @copyright  2018 Juan Leyva&lt;br /&gt;
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later&lt;br /&gt;
 */&lt;br /&gt;
class mobile {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate course view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_course_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Set timemodified for each certificate.&lt;br /&gt;
        foreach ($issues as $issue) {&lt;br /&gt;
            if (empty($issue-&amp;gt;timemodified)) {&lt;br /&gt;
                    $issue-&amp;gt;timemodified = $issue-&amp;gt;timecreated;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $showget = true;&lt;br /&gt;
        if ($certificate-&amp;gt;requiredtime &amp;amp;&amp;amp; !has_capability(&#039;mod/certificate:manage&#039;, $context)) {&lt;br /&gt;
            if (certificate_get_course_time($certificate-&amp;gt;course) &amp;lt; ($certificate-&amp;gt;requiredtime * 60)) {&lt;br /&gt;
                    $showget = false;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $certificate-&amp;gt;name = format_string($certificate-&amp;gt;name);&lt;br /&gt;
        list($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat) =&lt;br /&gt;
                        external_format_text($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat, $context-&amp;gt;id,&#039;mod_certificate&#039;, &#039;intro&#039;);&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;certificate&#039; =&amp;gt; $certificate,&lt;br /&gt;
            &#039;showget&#039; =&amp;gt; $showget &amp;amp;&amp;amp; count($issues) &amp;gt; 0,&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues,&lt;br /&gt;
            &#039;issue&#039; =&amp;gt; $issues[0],&lt;br /&gt;
            &#039;numissues&#039; =&amp;gt; count($issues),&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;files&#039; =&amp;gt; $issues,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Let’s go through the function code to analyse the different parts.&lt;br /&gt;
&lt;br /&gt;
;Function declaration: &lt;br /&gt;
: The function name is the same as the one used in the mobile.php file (method field). There is only one argument “$args” which is an array containing all the information sent by the mobile app (the courseid, userid, appid, appversionname, appversioncode, applang, appcustomurlscheme…)&lt;br /&gt;
&lt;br /&gt;
; Function implementation:&lt;br /&gt;
: In the first part of the function, we check permissions and capabilities (like a view.php script would do normally). Then we retrieve the certificate information that’s necessary to display the template.&lt;br /&gt;
&lt;br /&gt;
Finally, we return:&lt;br /&gt;
* The rendered template (notice that we could return more than one template but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some Javascript code.&lt;br /&gt;
* JavaScript: Empty, because we don’t need any in this case&lt;br /&gt;
* Other data: Empty as well, because we don’t need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way-data-bind to the template.&lt;br /&gt;
* Files: A list of files that the app should be able to download (for offline usage mostly)&lt;br /&gt;
&lt;br /&gt;
===Step 3. Creating the template for the main function===&lt;br /&gt;
&lt;br /&gt;
This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.&lt;br /&gt;
&lt;br /&gt;
In this template we’ll be using Ionic and custom directives and components available in the Mobile app.&lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with ion- are ionic components. Most of the time the component name is self-explanatory but you may refer to a detailed guide here: https://ionicframework.com/docs/components/ &lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with &#039;&#039;core-&#039;&#039; are custom components of the Mobile app.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_page.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;core-course-module-description description=&amp;quot;&amp;lt;% certificate.intro %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-course-module-description&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;ion-list-header&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-list-header&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
                    {{ &#039;plugin.mod_certificate.viewcertificateviews&#039; | translate: {$a: &amp;lt;% numissues %&amp;gt;} }}&lt;br /&gt;
                &amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;ion-icon name=&amp;quot;cloud-download&amp;quot; item-start&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
                {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
            &amp;lt;/button&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%^showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;p&amp;gt;{{ &#039;plugin.mod_certificate.requiredtimenotmet&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;!-- Call log WS when the template is loaded. --&amp;gt;&lt;br /&gt;
        &amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Then we display the module description using &amp;lt;code&amp;gt;&amp;lt;core-course-module-description&amp;lt;/code&amp;gt; that is a component used to include the course module description.&lt;br /&gt;
&lt;br /&gt;
For displaying the certificate information we create a list of elements, adding a header on top.&lt;br /&gt;
The following line &amp;lt;code&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/code&amp;gt; indicates that the Mobile app will translate the &#039;&#039;summaryofattempts&#039;&#039; string id (here we could’ve used mustache translation but it is usually better to delegate the strings translations to the app). The string id has this format: &lt;br /&gt;
&lt;br /&gt;
“plugin” + plugin identifier (from mobile.php) +  string id (the string must be indicated in the lang field in mobile.php). &lt;br /&gt;
&lt;br /&gt;
Then we display a button to transition to another page if there are certificates issued. The attribute (directive) &amp;lt;code&amp;gt;core-site-plugins-new-content&amp;lt;/code&amp;gt; indicates that if the user clicks the button, we need to call the function “mobile_issues_view” in the component “mod_certificate” passing as arguments the cmid and courseid. The content returned by this function will be displayed in a new page (see Step 4 for the code of this new page).&lt;br /&gt;
&lt;br /&gt;
Just after this button we display another one but this time for downloading an issued certificate. The &amp;lt;code&amp;gt;core-course-download-module-main-file&amp;lt;/code&amp;gt; directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available in offline.&lt;br /&gt;
&lt;br /&gt;
Finally, just before the ion-list is closed, we use the &amp;lt;code&amp;gt;core-site-plugins-call-ws-on-load&amp;lt;/code&amp;gt; directive to indicate that once the page is loaded, we need to call to a Web Service function in the server, in this case we are calling the &#039;&#039;mod_certificate_view_certificate&#039;&#039; that will log that the user viewed this page.&lt;br /&gt;
&lt;br /&gt;
As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes.&lt;br /&gt;
&lt;br /&gt;
===Step 4. Adding an additional page===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Partial file contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate issues view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_issues_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = [&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_issues&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This function for the new page was added just after the mobile_course_view function, the code is quite similar: Capabilities checks, retrieves the information required for the template and returns the template rendered.&lt;br /&gt;
&lt;br /&gt;
The code of the mustache template is also very simple:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_issues.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &amp;lt;%timecreated%&amp;gt; | coreToLocaleString }}&amp;lt;/p&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;&amp;lt;%grade%&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Here we are creating an ionic list that will display a new item in the list per each issued certificated.&lt;br /&gt;
&lt;br /&gt;
For the issued certificated we’ll display the time when it was created (using the app filter &#039;&#039;coreToLocaleString&#039;&#039;). We are also displaying the grade displayed in the certificate (if any).&lt;br /&gt;
&lt;br /&gt;
===Step 5. Plugin webservices, if included===&lt;br /&gt;
&lt;br /&gt;
If your plugin uses its own web services, they will also need to be enabled for mobile access in your db/services.php file.&lt;br /&gt;
&lt;br /&gt;
The following line &amp;lt;code&amp;gt;&#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&amp;lt;/code&amp;gt; should be included in each webservice definition.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/db/services.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$functions = [&lt;br /&gt;
&lt;br /&gt;
    &#039;mod_certificate_get_certificates_by_courses&#039; =&amp;gt; [&lt;br /&gt;
        &#039;classname&#039;     =&amp;gt; &#039;mod_certificate_external&#039;,&lt;br /&gt;
        &#039;methodname&#039;    =&amp;gt; &#039;get_certificates_by_courses&#039;,&lt;br /&gt;
        &#039;description&#039;   =&amp;gt; &#039;Returns a list of certificate instances...&#039;,&lt;br /&gt;
        &#039;type&#039;          =&amp;gt; &#039;read&#039;,&lt;br /&gt;
        &#039;capabilities&#039;  =&amp;gt; &#039;mod/certificate:view&#039;,&lt;br /&gt;
        &#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
	&lt;br /&gt;
This extra services definition is the reason why you will need to have the local_mobile plugin installed for Moodle versions 3.4 and lower, so that your Moodle site will have all the additional webservices included to deal with all these mobile access calls. This is explained further in the [https://docs.moodle.org/dev/Mobile_support_for_plugins#Moodle_version_requirements Moodle version requirements section] below.&lt;br /&gt;
&lt;br /&gt;
==Getting started==&lt;br /&gt;
&lt;br /&gt;
The first and most important thing to know is that you don’t need a local mobile environment, you can just use the Chrome or Chromium browser to add mobile support to your plugins!&lt;br /&gt;
&lt;br /&gt;
Open this URL (with Chrome or Chromium browser): https://mobileapp.moodledemo.net/ and you will see a web version of the mobile app completely functional (except for some native features). This URL is updated with the latest integration version of the app.&lt;br /&gt;
&lt;br /&gt;
Please test that your site works correctly in the web version before starting any development.&lt;br /&gt;
&lt;br /&gt;
===Moodle version requirements===&lt;br /&gt;
&lt;br /&gt;
If your Moodle version is lower than 3.5 you will need to install the [https://docs.moodle.org/en/Moodle_Mobile_additional_features Moodle Mobile additional features plugin]. &lt;br /&gt;
&lt;br /&gt;
Please use this development version for now: https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE (if your Moodle version is 3.2, 3.3 or 3.4) you will have to use the specific branch for your version but applying manually the [https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE last commit from the 3.1 branch] (the one with number MOBILE-2362).&lt;br /&gt;
&lt;br /&gt;
Also, when installing the Moodle Mobile Additional features plugin you must follow the installation instructions so the service is set up properly.&lt;br /&gt;
&lt;br /&gt;
Remember to update your plugin documentation to reflect that this plugin is mandatory for Mobile support. We don’t recommend to indicate in your plugin version.php a dependency to local_mobile though.&lt;br /&gt;
&lt;br /&gt;
===Development workflow===&lt;br /&gt;
&lt;br /&gt;
First of all, we recommend creating a simple &#039;&#039;mobile.php&#039;&#039; for displaying a new main menu option (even if your plugin won’t be in the main menu, just to verify that you are able to extend the app plugins). Then open the webapp (https://mobileapp.moodledemo.net/) or refresh the browser if it was already open. Check that you can correctly  see the new menu option you included.&lt;br /&gt;
&lt;br /&gt;
Then, develop the main function of the app returning a “Hello world” or basic code (without using templates) to see that everything works together. After adding the classes/output/mobile.php file it is very important to “Purge all caches” to avoid problems with the auto-loading cache.&lt;br /&gt;
&lt;br /&gt;
It is important to remember that:&lt;br /&gt;
* Any change in the mobile.php file will require you to refresh the web app page in the browser (remember to disable the cache in the Chrome developer options).&lt;br /&gt;
* Any change in an existing template or function won’t require to refresh the browser page. In most cases you should just do a PTR (Pull down To Refresh) in the page that displays the view returned by the function. Be aware that PTR will work only when using the “device” emulation in the browser (see following section).&lt;br /&gt;
&lt;br /&gt;
===Testing and debugging===&lt;br /&gt;
&lt;br /&gt;
To learn how to debug with the web version of the app, please read the following documents:&lt;br /&gt;
* [[Moodle Mobile debugging WS requests]] AND&lt;br /&gt;
* [[Moodle Mobile development using Chrome or Chromium]] (please, omit the installation section)&lt;br /&gt;
&lt;br /&gt;
For plugins using the Javascript API you may develop making use of the console.log function to add trace messages in your code that will be displayed in the browser console.&lt;br /&gt;
&lt;br /&gt;
Within the app, make sure to turn on the option: &#039;&#039;&#039;App settings&#039;&#039;&#039; / &#039;&#039;&#039;General&#039;&#039;&#039; / &#039;&#039;&#039;Display debug messages&#039;&#039;&#039;. This means popup errors from the app will show more information.&lt;br /&gt;
&lt;br /&gt;
==Mobile.php supported options==&lt;br /&gt;
&lt;br /&gt;
In the Step by Step section we learned about some of the existing options for handlers configuration. This is the full list of supported options:&lt;br /&gt;
&lt;br /&gt;
===Common options===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;delegate&#039;&#039;&#039; (mandatory): Name of the delegate to register the handler in.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (mandatory): The function to call to retrieve the main page content.&lt;br /&gt;
* &#039;&#039;&#039;init&#039;&#039;&#039; (optional): A function to call to retrieve the initialization JS and the &amp;quot;restrict&amp;quot; to apply to the whole handler. It can also return templates that can be used from the Javascript of the init method or the Javascript of the handler’s method.&lt;br /&gt;
* &#039;&#039;&#039;restricttocurrentuser&#039;&#039;&#039; (optional) Only used if the delegate has a isEnabledForUser function. If true, the handler will only be shown for current user. For more info about displaying the plugin only for certain users, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;restricttoenrolledcourses&#039;&#039;&#039; (optional): Only used if the delegate has a isEnabledForCourse function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;styles&#039;&#039;&#039; (optional): An array with two properties: &#039;&#039;url&#039;&#039; and &#039;&#039;version&#039;&#039;. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It&#039;s recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number everytime you change the CSS file.&lt;br /&gt;
* &#039;&#039;&#039;moodlecomponent&#039;&#039;&#039; (optional): If your plugin supports a component in the app different than the one defined by your plugin, you can use this property to specify it. For example, you can create a local plugin to support a certain course format, activity, etc. The component of your plugin in Moodle would be &#039;&#039;local_whatever&#039;&#039;, but in &amp;quot;moodlecomponent&amp;quot; you can specify that this handler will implement &#039;&#039;format_whatever&#039;&#039; or &#039;&#039;mod_whatever&#039;&#039;. This property was introduced in the version 3.6.1 of the app.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseOptionsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreMainMenuDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the &amp;quot;More&amp;quot; tab, they cannot be displayed as tabs in the bottom bar.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseModuleDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): icon, class.&lt;br /&gt;
* &#039;&#039;&#039;offlinefunctions&#039;&#039;&#039;: (optional) List of functions to call when prefetching the module. It can be a get_content method or a WS. You can filter the params received by the WS. By default, WS will receive these params: courseid, cmid, userid. Other valid values that will be added if they are present in the list of params: courseids (it will receive a list with the courses the user is enrolled in), component + &#039;id&#039; (e.g. certificateid).&lt;br /&gt;
* &#039;&#039;&#039;downloadbutton&#039;&#039;&#039;: (optional) Whether to display download button in the module. If not defined, the button will be shown if there is any offlinefunction.&lt;br /&gt;
* &#039;&#039;&#039;isresource&#039;&#039;&#039;: (optional) Whether the module is a resource or an activity. Only used if there is any offlinefunction. If your module relies on the &amp;quot;contents&amp;quot; field, then it should be true.&lt;br /&gt;
* &#039;&#039;&#039;updatesnames&#039;&#039;&#039;: (optional) Only used if there is any offlinefunction. A Regular Expression to check if there&#039;s any update in the module. It will be compared to the result of &#039;&#039;core_course_check_updates&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;displayopeninbrowser&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Open in browser&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayOpenInBrowser = false;&lt;br /&gt;
* &#039;&#039;&#039;displaydescription&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Description&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayDescription = false;&lt;br /&gt;
* &#039;&#039;&#039;displayrefresh&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Refresh&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayRefresh = false;&lt;br /&gt;
* &#039;&#039;&#039;displayprefetch&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: this.displayPrefetch = false;&lt;br /&gt;
* &#039;&#039;&#039;displaysize&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: this.displaySize = false;&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseFormatDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;canviewallsections&#039;&#039;&#039;: (optional) Whether the course format allows seeing all sections in a single page. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displayenabledownload&#039;&#039;&#039;: (optional) Whether the option to enable section/module download should be displayed. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displaysectionselector&#039;&#039;&#039;: (optional) Whether the default section selector should be displayed. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreUserDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;type&#039;&#039;&#039;: The type of the addon. Values accepted: &#039;newpage&#039; (default) or  &#039;communication&#039;. &lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreSettingsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for AddonMessageOutputDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
==Delegates==&lt;br /&gt;
&lt;br /&gt;
The delegates can be classified by type of plugin. For more info about type of plugins, please see the See [[Mobile_support_for_plugins#Types_of_plugins|Types of plugins]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
====CoreMainMenuDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app). &lt;br /&gt;
&lt;br /&gt;
====CoreCourseOptionsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate).&lt;br /&gt;
&lt;br /&gt;
====CoreCourseModuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting activity modules or resources.&lt;br /&gt;
&lt;br /&gt;
====CoreUserDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add additional options in the user profile page in the app.&lt;br /&gt;
&lt;br /&gt;
====CoreCourseFormatDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting course formats.&lt;br /&gt;
&lt;br /&gt;
====CoreSettingsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to add a new option in the settings page.&lt;br /&gt;
&lt;br /&gt;
====AddonMessageOutputDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a message output plugin.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question types.&lt;br /&gt;
https://docs.moodle.org/dev/Creating_mobile_question_types&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionBehaviourDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question behaviours.&lt;br /&gt;
&lt;br /&gt;
====CoreUserProfileFieldDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting user profile fields.&lt;br /&gt;
&lt;br /&gt;
====AddonModQuizAccessRuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a quiz access rule.&lt;br /&gt;
&lt;br /&gt;
====AddonModAssignSubmissionDelegate and AddonModAssignFeedbackDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use these delegates to support assign submission or feedback plugins.&lt;br /&gt;
&lt;br /&gt;
====AddonWorkshopAssessmentStrategyDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a workshop assessment strategy plugin.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
These delegates require JavaScript to be supported. See [[Mobile_support_for_plugins#Initialization|Initialization]] for more information.&lt;br /&gt;
&lt;br /&gt;
* CoreContentLinksDelegate&lt;br /&gt;
* CoreCourseModulePrefetchDelegate&lt;br /&gt;
* CoreFileUploaderDelegate&lt;br /&gt;
* CorePluginFileDelegate&lt;br /&gt;
&lt;br /&gt;
==Available components and directives==&lt;br /&gt;
&lt;br /&gt;
===Difference between component and directives===&lt;br /&gt;
&lt;br /&gt;
A component (represented as an HTML tag) is used to add custom elements to the app.&lt;br /&gt;
Example of components are: ion-list, ion-item, core-search-box&lt;br /&gt;
&lt;br /&gt;
A directive (represented as an HTML attribute) allows you to extend a piece of HTML with additional information or functionality.&lt;br /&gt;
Example of directives are: core-auto-focus, *ngIf, ng-repeat&lt;br /&gt;
&lt;br /&gt;
The Mobile app uses Angular, Ionic and custom components and directives, for a full reference of:&lt;br /&gt;
* Angular directives, please check: https://angular.io/api?type=directive&lt;br /&gt;
* Ionic components, please check: https://ionicframework.com/docs/&lt;br /&gt;
&lt;br /&gt;
===Custom core components and directives===&lt;br /&gt;
&lt;br /&gt;
These are some useful custom components and directives (only available in the mobile app). Please notice that this isn’t the full list of components and directives of the app, it’s just an extract of the most common ones.&lt;br /&gt;
&lt;br /&gt;
====core-format-text====&lt;br /&gt;
&lt;br /&gt;
This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive.&lt;br /&gt;
&lt;br /&gt;
This directive automatically applies core-external-content and core-link to all the links and embedded media.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;text&#039;&#039;&#039; (string): The text to format.&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* &#039;&#039;&#039;adaptImg&#039;&#039;&#039; (boolean): Optional. Whether to adapt images to screen width. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;clean&#039;&#039;&#039; (boolean): Optional. Whether all the HTML tags should be removed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;singleLine&#039;&#039;&#039; (boolean): Optional. Whether new lines should be removed (all text in single line). Only if clean=true. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;maxHeight&#039;&#039;&#039; (number): Optional. Max height in pixels to render the content box. It should be 50 at least to make sense. Using this parameter will force display: block to calculate height better. If you want to avoid this use class=&amp;quot;inline&amp;quot; at the same time to use display: inline-block.&lt;br /&gt;
* &#039;&#039;&#039;fullOnClick&#039;&#039;&#039; (boolean): Optional. Whether it should open a new page with the full contents on click. Only if maxHeight is set and the content has been collapsed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;fullTitle&#039;&#039;&#039; (string): Optional. Title to use in full view. Defaults to &amp;quot;Description&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-format-text text=&amp;quot;&amp;lt;% cm.description %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-format-text&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-link====&lt;br /&gt;
&lt;br /&gt;
Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app).&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;capture&#039;&#039;&#039; (boolean): Optional, default false. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser).&lt;br /&gt;
* &#039;&#039;&#039;inApp&#039;&#039;&#039; (boolean): Optional, default false. True to open in embedded browser, false to open in system browser.&lt;br /&gt;
* &#039;&#039;&#039;autoLogin&#039;&#039;&#039; (string): Optional, default &amp;quot;check&amp;quot;. If the link should be open with auto-login. Accepts the following values:&lt;br /&gt;
** &amp;quot;yes&amp;quot; -&amp;gt; Always auto-login.&lt;br /&gt;
** &amp;quot;no&amp;quot; -&amp;gt; Never auto-login.&lt;br /&gt;
** &amp;quot;check&amp;quot; -&amp;gt; Auto-login only if it points to the current site. Default value.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a href=&amp;quot;&amp;lt;% cm.url %&amp;gt;&amp;quot; core-link&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-external-content====&lt;br /&gt;
&lt;br /&gt;
Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline. &lt;br /&gt;
&lt;br /&gt;
If a file is downloaded, its URL will be replaced by the local file URL.&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;img src=&amp;quot;&amp;lt;% event.iconurl %&amp;gt;&amp;quot; core-external-content component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% event.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-user-link====&lt;br /&gt;
&lt;br /&gt;
Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;userId&#039;&#039;&#039; (number): User id to open the profile.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): Optional. Course id to show the user info related to that course.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a ion-item core-user-link userId=&amp;quot;&amp;lt;% userid %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-file====&lt;br /&gt;
&lt;br /&gt;
Component to handle a remote file. It shows the file name, icon (depending on mimetype) and a button to download/refresh it. The user can identify if the file is downloaded or not based on the button.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* file (object): The file. Must have a property &#039;filename&#039; and a &#039;fileurl&#039; or &#039;url&#039;&lt;br /&gt;
* component (string): Optional. Component the file belongs to.&lt;br /&gt;
* componentId (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* canDelete (boolean): Optional. Whether file can be deleted.&lt;br /&gt;
* alwaysDownload (boolean): Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they&#039;re outdated or not.&lt;br /&gt;
* canDownload (boolean): Optional. Whether file can be downloaded. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-file [file]=&amp;quot;{fileurl: &#039;&amp;lt;% issue.url %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.name %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, filesize: &#039;&amp;lt;% issue.size %&amp;gt;&#039;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-file&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-download-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened.&lt;br /&gt;
&lt;br /&gt;
It is usually recommended to use the core-file component since it also displays the state of the file.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;core-download-file&#039;&#039;&#039; (object): The file to download.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage: a button to download a file.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button [core-download-file]=&amp;quot;{fileurl: &amp;lt;% issue.url %&amp;gt;, timemodified: &amp;lt;% issue.timemodified %&amp;gt;, filesize: &amp;lt;% issue.size %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.download | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-course-download-module-main-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and opening the main file of a module.&lt;br /&gt;
&lt;br /&gt;
When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like mod_resource.&lt;br /&gt;
&lt;br /&gt;
This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;module&#039;&#039;&#039; (object): Optional. The module object. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;moduleId&#039;&#039;&#039; (number): Optional. The module ID. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): The course ID the module belongs to.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component. If not defined, moduleId.&lt;br /&gt;
* &#039;&#039;&#039;files&#039;&#039;&#039; (object[]): Optional. List of files of the module. If not provided, use module.contents.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-navbar-buttons====&lt;br /&gt;
&lt;br /&gt;
Component to add buttons to the app&#039;s header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page.&lt;br /&gt;
&lt;br /&gt;
If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first &amp;lt;ion-buttons&amp;gt; found in the header.&lt;br /&gt;
&lt;br /&gt;
You can use the [hidden] input to hide all the inner buttons if a certain condition is met.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons end&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button icon-only (click)=&amp;quot;action()&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;ion-icon name=&amp;quot;funnel&amp;quot;&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can also use this to add options to the context menu. Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons&amp;gt;&lt;br /&gt;
    &amp;lt;core-context-menu&amp;gt;&lt;br /&gt;
        &amp;lt;core-context-menu-item [priority]=&amp;quot;500&amp;quot; [content]=&amp;quot;&#039;Nice boat&#039;&amp;quot; (action)=&amp;quot;boatFunction()&amp;quot; [iconAction]=&amp;quot;&#039;boat&#039;&amp;quot;&amp;gt;&amp;lt;/core-context-menu-item&amp;gt;&lt;br /&gt;
    &amp;lt;/core-context-menu&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that it is not currently possible to remove or modify options from the context menu without using a nasty hack.&lt;br /&gt;
&lt;br /&gt;
===Specific component and directives for plugins===&lt;br /&gt;
&lt;br /&gt;
These are component and directives created specifically for supporting Moodle plugins.&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the new &#039;&#039;get_content&#039;&#039; WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to go to a new content page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to load new content in current page using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view.&lt;br /&gt;
&lt;br /&gt;
If you want to load a new content when the WS call is done, please see core-site-plugins-call-ws-new-content.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;successMessage&#039;&#039;&#039; (string): Message to show on success. If not supplied, no message. If supplied but empty, default message (“Success”).&lt;br /&gt;
* &#039;&#039;&#039;goBackOnSuccess&#039;&#039;&#039; (boolean): Whether to go back if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;refreshOnSuccess&#039;&#039;&#039; (boolean): Whether to refresh the current view if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server without using cache, displaying default messages and refreshing on success:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage successMessage refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server using cache without confirming, going back on success and using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; goBackOnSuccess=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;certificateViewed($event)&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.certificateViewed = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked and load a new content passing the WS result as args. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t need to load some new content when done, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;jsData&#039;&#039;&#039; (any): JS variables to pass to the new page so they can be used in the template or JS. If true is supplied instead of an object, all initial variables from current page will be copied. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;newContentPreSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server without using cache, showing default confirm and displaying a new page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server using cache, without confirm, displaying new content in same page and using &#039;&#039;userid&#039;&#039; from &#039;&#039;otherdata&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-on-load====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services.&lt;br /&gt;
&lt;br /&gt;
If you want to call a WS when the user clicks on a certain element, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Note that this will cause an error to appear on each page load if the user is offline in v3.5.1 and older, the bug was fixed in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Advanced features==&lt;br /&gt;
&lt;br /&gt;
===Display the plugin only if certain conditions are met===&lt;br /&gt;
&lt;br /&gt;
You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the &amp;quot;init&amp;quot; method (for more info, please see the [[Mobile_support_for_plugins#Initialization|Initialization]] section ahead).&lt;br /&gt;
&lt;br /&gt;
All the init methods are called as soon as your plugin is retrieved. If you don&#039;t want your plugin to be displayed for the current user, then you should return an exception in this init method. It&#039;s recommended to include a message explaining why the plugin isn&#039;t available for the current user, this exception will be logged in the Javascript console.&lt;br /&gt;
&lt;br /&gt;
On the other hand, you might want to display a plugin only for certain courses (&#039;&#039;CoreCourseOptionsDelegate&#039;&#039;) or only if the user is viewing certain users&#039; profiles (&#039;&#039;CoreUserDelegate&#039;&#039;). This can be achieved with the init method too.&lt;br /&gt;
&lt;br /&gt;
In the init method you can return a &amp;quot;restrict&amp;quot; property with two fields in it: &#039;&#039;courses&#039;&#039; and &#039;&#039;users&#039;&#039;. If you return a list of courses IDs in this restrict property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users&#039; profiles.&lt;br /&gt;
&lt;br /&gt;
===Using “otherdata”===&lt;br /&gt;
&lt;br /&gt;
The values returned by the functions in otherdata are added to a variable so they can be used both in Javascript and in templates. The otherdata returned by a init call is added to a variable named INIT_OTHERDATA, while the otherdata returned by a &#039;&#039;get_content&#039;&#039; WS call is added to a variable named CONTENT_OTHERDATA.&lt;br /&gt;
&lt;br /&gt;
The otherdata returned by a init call will be passed to the JS and template of all the get_content calls in that handler. The otherdata returned by a get_content call will only be passed to the JS and template returned by that get_content call.&lt;br /&gt;
&lt;br /&gt;
This means that, in your Javascript, you can access and use these data like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
this.CONTENT_OTHERDATA.myVar&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
And in the template you could use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
{{ CONTENT_OTHERDATA.myVar }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;myVar&#039;&#039; is the name we put to one of our variables, it can be the name you want. In the example above, this is the otherdata returned by the PHP method:&lt;br /&gt;
&lt;br /&gt;
array(&#039;myVar&#039; =&amp;gt; &#039;Initial value&#039;)&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
In our plugin we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain WebService. This can be done using otherdata.&lt;br /&gt;
&lt;br /&gt;
We will return the initial value of the input in the otherdata of our PHP method:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;myVar&#039; =&amp;gt; &#039;My initial value&#039;),&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then in the template we will use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-item text-wrap&amp;gt;&lt;br /&gt;
    &amp;lt;ion-label stacked&amp;gt;{{ &#039;plugin.mod_certificate.textlabel | translate }}&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;text&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.myVar&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [useOtherDataForWS]=&amp;quot;[&#039;myVar&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
        {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we are creating an input text and we use &#039;&#039;[(ngModel)]&#039;&#039; to use the value in &#039;&#039;myVar&#039;&#039; as the initial value and to store the changes in the same &#039;&#039;myVar&#039;&#039; variable. This means that the initial value of the input will be “My initial value”, and if the user changes the value of the input these changes will be applied to the &#039;&#039;myVar&#039;&#039; variable. This is called 2-way data binding in Angular.&lt;br /&gt;
&lt;br /&gt;
Then we add a button to send this data to a WS, and for that we use the directive core-site-plugins-call-ws. We use the &#039;&#039;useOtherDataForWS&#039;&#039; attribute to specify which variable from &#039;&#039;otherdata&#039;&#039; we want to send to our WebService. So if the user enters “A new value” in the input and then clicks the button, it will call the WebService &#039;&#039;mod_certificate_my_webservice&#039;&#039; and will send as a param: myVar -&amp;gt; “A new value”.&lt;br /&gt;
&lt;br /&gt;
We can achieve the same result using the &#039;&#039;params&#039;&#039; attribute of the core-site-plugins-call-ws directive instead of using &#039;&#039;useOtherDataForWS&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [params]=&amp;quot;{myVar: CONTENT_OTHERDATA.myVar}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
The WebService call will be exactly the same with both buttons.&lt;br /&gt;
&lt;br /&gt;
Please notice that this example could be done without using otherdata too, using the “&#039;&#039;form&#039;&#039;” input of the &#039;&#039;core-site-plugins-call-ws directive&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
===Running JS code after a content template has loaded===&lt;br /&gt;
&lt;br /&gt;
When you return JavaScript code from a handler function using the &#039;javascript&#039; array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM. &lt;br /&gt;
&lt;br /&gt;
If your code needs to run after the DOM has been updated, you can use setTimeout to call it. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
return [&lt;br /&gt;
    &#039;template&#039; =&amp;gt; [ ... ],&lt;br /&gt;
    &#039;javascript&#039; =&amp;gt; &#039;setTimeout(function() { console.log(&amp;quot;DOM is available now&amp;quot;); });&#039;,&lt;br /&gt;
    &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
    &#039;files&#039; =&amp;gt; []&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Note: If you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an init template, so that it does not get loaded again with each page of content.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
===JS functions visible in the templates===&lt;br /&gt;
&lt;br /&gt;
The app provides some Javascript functions that can be used from the templates to update, refresh or view content. These are the functions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;openContent(title: string, args: any, component?: string, method?: string)&#039;&#039;&#039;: Open a new page to display some new content. You need to specify the &#039;&#039;title&#039;&#039; of the new page and the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
* &#039;&#039;&#039;refreshContent(showSpinner = true)&#039;&#039;&#039;: Refresh the current content. By default it will display a spinner while refreshing, if you don&#039;t want it to be displayed you should pass false as a parameter.&lt;br /&gt;
* &#039;&#039;&#039;updateContent(args: any, component?: string, method?: string)&#039;&#039;&#039;: Refresh the current content using different params. You need to specify the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Group selector=====&lt;br /&gt;
&lt;br /&gt;
Imagine we have an activity that uses groups and we want to let the user select which group he wants to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting.&lt;br /&gt;
&lt;br /&gt;
To do so, we&#039;ll use a drop down to select the group. When the user selects a group using this drop down we&#039;ll update the page content to display the new group.&lt;br /&gt;
&lt;br /&gt;
The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular&#039;s &#039;&#039;ngModel&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
======Using plain HTML======&lt;br /&gt;
&lt;br /&gt;
We need to add a &amp;quot;&#039;&#039;selected&#039;&#039;&amp;quot; attribute to the option that needs to be selected. To do so, we need to pre-caclulate the selected option in the PHP code:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
        // Detect which group is selected.&lt;br /&gt;
        foreach ($groups as $gid=&amp;gt;$group) {&lt;br /&gt;
            $group-&amp;gt;selected = $gid === $groupid;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid,&lt;br /&gt;
            &#039;groups&#039; =&amp;gt; $groups&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the code above, we&#039;re retrieving the groups the user can see and then we&#039;re adding a &amp;quot;selected&amp;quot; bool to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template.&lt;br /&gt;
&lt;br /&gt;
In the template, we display the drop down like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: $event})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot; &amp;lt;%#selected%&amp;gt;selected&amp;lt;%/selected%&amp;gt; &amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;ionChange&#039;&#039; function will be called everytime the user selects a different group with the drop down. We&#039;re using the function &#039;&#039;updateContent&#039;&#039; to update the current view using the new group. &#039;&#039;$event&#039;&#039; is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work.&lt;br /&gt;
&lt;br /&gt;
======Using ngModel======&lt;br /&gt;
&lt;br /&gt;
ngModel is an Angular directive that allows storing the value of a certain input/select in a Javascript variable, and also the opposite way: tell the input/select which value to set. The main problem is that we cannot initialize a Javascript variable from the template (Angular doesn&#039;t have &#039;&#039;ng-init&#039;&#039; like in AngularJS), so we&#039;ll use &amp;quot;otherdata&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
In the PHP function we&#039;ll return the group that needs to be selected in the &#039;&#039;otherdata&#039;&#039; array:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
         ...&lt;br /&gt;
&lt;br /&gt;
         return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; array(&lt;br /&gt;
                &#039;group&#039; =&amp;gt; $groupid&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we don&#039;t need to iterate over the groups array like in the plain HTML example. However, now we&#039;re returning the groupid in the &amp;quot;otherdata&amp;quot; array. As it&#039;s explained in the [[Mobile_support_for_plugins#Using_.E2.80.9Cotherdata.E2.80.9D|Using &amp;quot;otherdata&amp;quot;]] section, this &amp;quot;otherdata&amp;quot; is visible in the templates inside a variable named &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. So in the template we&#039;ll use this variable like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.group&amp;quot; (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: CONTENT_OTHERDATA.group})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Use the rich text editor===&lt;br /&gt;
&lt;br /&gt;
The rich text editor included in the app requires a FormControl to work. You can use the library FormBuilder to create this control (or to create a whole FormGroup if you prefer).&lt;br /&gt;
&lt;br /&gt;
With the following Javascript you&#039;ll be able to create a FormControl:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.control = this.FormBuilder.control(this.CONTENT_OTHERDATA.rte);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we&#039;re using a value returned in OTHERDATA as the initial value of the rich text editor, but you can use whatever you want.&lt;br /&gt;
&lt;br /&gt;
Then you need to pass this control to the rich text editor in your template:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;core-rich-text-editor item-content [control]=&amp;quot;control&amp;quot; placeholder=&amp;quot;Enter your text here&amp;quot; name=&amp;quot;rte_answer&amp;quot;&amp;gt;&amp;lt;/core-rich-text-editor&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, there are several ways to send the value in the rich text editor to a WebService to save it. This is one of the simplest options:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_webservice&amp;quot; [params]=&amp;quot;{rte: control.value}&amp;quot; ....&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re passing the value of the rich text editor as a parameter to our WebService.&lt;br /&gt;
&lt;br /&gt;
===Initialization===&lt;br /&gt;
&lt;br /&gt;
All handlers can specify a “&#039;&#039;init&#039;&#039;” method in the mobile.php file. This method is meant to return some JavaScript code that needs to be executed as soon as the plugin is retrieved.&lt;br /&gt;
&lt;br /&gt;
When the app retrieves all the handlers, the first thing it will do is call the &#039;&#039;tool_mobile_get_content&#039;&#039; WebService with the init method. This WS call will only receive the default args.&lt;br /&gt;
&lt;br /&gt;
The app will immediately execute the JavaScript code returned by this WS call. This JavaScript can be used to manually register your handlers in the delegates you want, without having to rely on the default handlers built based on the mobile.php data.&lt;br /&gt;
&lt;br /&gt;
The templates returned by this init method will be added to a INIT_TEMPLATES variable that will be passed to all the Javascript code of that handler. This means that the Javascript returned by the init method or the “main” method can access any of the templates HTML like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.INIT_TEMPLATES[‘main’];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
In this case, “main” is the ID of the template we want to use.&lt;br /&gt;
&lt;br /&gt;
The same happens with the &#039;&#039;otherdata&#039;&#039; returned by this init method, it is added to a INIT_OTHERDATA variable.&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;restrict&#039;&#039; field returned by this init call will be used to determine if your handler is enabled or not. For example, if your handler is for the delegate &#039;&#039;CoreCourseOptionsDelegate&#039;&#039; and you return a list of courseids in restrict-&amp;gt;courses, then your handler will only be enabled in the courses you returned. This only applies to the “default” handlers, if you register your own handler using the Javascript code then you should check yourself if the handler is enabled.&lt;br /&gt;
&lt;br /&gt;
Finally, if you return an object in this init Javascript code, all the properties of that object will be passed to all the Javascript code of that handler so you can use them when the code is run. For example, if your init Javascript code does something like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    MyAddonClass: new MyAddonClass()&lt;br /&gt;
};&lt;br /&gt;
result:&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then, for the rest of Javascript code of your handler (e.g. for the “main” method) you can use this variable like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.MyAddonClass&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Module link handler=====&lt;br /&gt;
&lt;br /&gt;
A link handler allows you to decide what to do when a link with a certain URL is clicked. This is useful, for example, to open your module when a link to the module is clicked. In this example we’ll create a link handler to detect links to a certificate module using a init JavaScript:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function AddonModCertificateModuleLinkHandler() {&lt;br /&gt;
    that.CoreContentLinksModuleIndexHandler.call(this, that.CoreCourseHelperProvider, &#039;mmaModCertificate&#039;, &#039;certificate&#039;);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateLinkHandler&amp;quot;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype = Object.create(this.CoreContentLinksModuleIndexHandler.prototype);&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype.constructor = AddonModCertificateModuleLinkHandler;&lt;br /&gt;
&lt;br /&gt;
this.CoreContentLinksDelegate.registerHandler(new AddonModCertificateModuleLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Advanced link handler=====&lt;br /&gt;
Link handlers have some advanced features that allow you to change how links behave under different conditions.&lt;br /&gt;
======Patterns======&lt;br /&gt;
You can define a Regular Expression pattern to match certain links.  This will apply the handler only to links that match the pattern.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.pattern = RegExp(&#039;\/mod\/foo\/specialpage.php&#039;);&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Priority======&lt;br /&gt;
Multiple link handlers may apply to a given link.  You can define the order of precedence by setting the priority - the handler with the highest priority will be used.&lt;br /&gt;
All default handlers have a priority of 0, so 1 or higher will override the default.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Multiple actions======&lt;br /&gt;
Once a link has been matched, the handler&#039;s getActions() method determines what the link should do.  This method has access to the URL and its parameters.&lt;br /&gt;
Different actions can be returned depending on different conditions.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {&lt;br /&gt;
    return [{&lt;br /&gt;
        action: function(siteId, navCtrl) {&lt;br /&gt;
            // The actual behaviour of the link goes here.&lt;br /&gt;
        },&lt;br /&gt;
        sites: [...]&lt;br /&gt;
    }, {&lt;br /&gt;
        ...&lt;br /&gt;
    }];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Once handlers have been matched for a link, the actions will be fetched for all the matching handlers, in priorty order.  The first &amp;quot;valid&amp;quot; action will be used to open the link.&lt;br /&gt;
If your handler is matched with a link, but a condition assessed in the getActions() function means you want to revert to the next highest priorty handler, you can &amp;quot;invalidate&amp;quot;&lt;br /&gt;
your action by settings its sites propety to an empty array.&lt;br /&gt;
======Complex example======&lt;br /&gt;
This will match all URLs containing /mod/foo/, and force those with an id parameter that&#039;s not in the &amp;quot;supportedModFoos&amp;quot; array to open in the user&#039;s browser, rather than the app.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
var supportedModFoos = [...];&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    this.pattern = new RegExp(&#039;\/mod\/foo\/&#039;);&lt;br /&gt;
    this.name = &amp;quot;AddonModFooLinkHandler&amp;quot;;&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
}&lt;br /&gt;
AddonModFooLinkHandler.prototype = Object.create(that.CoreContentLinksHandlerBase.prototype);&lt;br /&gt;
AddonModFooLinkHandler.prototype.constructor = AddonModFooLinkHandler;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {     &lt;br /&gt;
    var action = {&lt;br /&gt;
        action: function() {&lt;br /&gt;
            that.CoreUtilsProvider.openInBrowser(url);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) {&lt;br /&gt;
        action.sites = [];&lt;br /&gt;
    }&lt;br /&gt;
    return [action];&lt;br /&gt;
};&lt;br /&gt;
that.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Module prefetch handler=====&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;CoreCourseModuleDelegate&#039;&#039; handler allows you to define a list of &#039;&#039;offlinefunctions&#039;&#039; to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain WS calls (pass the result of a WS call to the next one), and this cannot be done using &#039;&#039;offlinefunctions&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to create a prefetch handler using init JS:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
// Create a class that &amp;quot;inherits&amp;quot; from CoreCourseActivityPrefetchHandlerBase.&lt;br /&gt;
function AddonModCertificateModulePrefetchHandler() {&lt;br /&gt;
    that.CoreCourseActivityPrefetchHandlerBase.call(this, that.TranslateService, that.CoreAppProvider, that.CoreUtilsProvider,&lt;br /&gt;
            that.CoreCourseProvider, that.CoreFilepoolProvider, that.CoreSitesProvider, that.CoreDomUtilsProvider);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateModulePrefetchHandler&amp;quot;;&lt;br /&gt;
    this.modName = &amp;quot;certificate&amp;quot;;&lt;br /&gt;
    this.component = &amp;quot;mod_certificate&amp;quot;; // This must match the plugin identifier from db/mobile.php, otherwise the download link in the context menu will not update correctly.&lt;br /&gt;
    this.updatesNames = /^configuration$|^.*files$/;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype = Object.create(this.CoreCourseActivityPrefetchHandlerBase.prototype);&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.constructor = AddonModCertificateModulePrefetchHandler;&lt;br /&gt;
&lt;br /&gt;
// Override the prefetch call.&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.prefetch = function(module, courseId, single, dirPath) {&lt;br /&gt;
    return this.prefetchPackage(module, courseId, single, prefetchCertificate);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function prefetchCertificate(module, courseId, single, siteId) {&lt;br /&gt;
    // Perform all the WS calls.&lt;br /&gt;
    // You can access most of the app providers using that.ClassName. E.g. that.CoreWSProvider.call().&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same cmid. The app will not automatically work with this situation - it will call the offline function with the standard arguments only, so you won&#039;t be able to prefetch all the possible parameters. &lt;br /&gt;
&lt;br /&gt;
To deal with this, you need to implement a web service in your Moodle component that returns the list of possible extra arguments, and then you can call this web service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module (showing only the actual prefetch function - the rest of the code is as above) where there are multiple values of a custom &#039;section&#039; parameter for the mobile function &#039;mobile_document_view&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function prefetchOucontent(module, courseId, single, siteId) {&lt;br /&gt;
    var component = &#039;mod_oucontent&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Get the site, first.&lt;br /&gt;
    return that.CoreSitesProvider.getSite(siteId).then(function(site) {&lt;br /&gt;
        // Read the list of pages in this document using a web service.&lt;br /&gt;
        return site.read(&#039;mod_oucontent_get_page_list&#039;, {&#039;cmid&#039;: module.id}).then(function(response) {&lt;br /&gt;
            var promises = [];&lt;br /&gt;
&lt;br /&gt;
            // For each page, read and process the page - this is a copy of logic in the app at&lt;br /&gt;
            // siteplugins.ts (prefetchFunctions), but modified to add the custom argument.&lt;br /&gt;
            for(var i = 0; i &amp;lt; response.length; i++) {&lt;br /&gt;
                var args = {&lt;br /&gt;
                    courseid: courseId,&lt;br /&gt;
                    cmid: module.id,&lt;br /&gt;
                    userid: site.getUserId()&lt;br /&gt;
                };&lt;br /&gt;
                if (response[i] !== &#039;&#039;) {&lt;br /&gt;
                    args.section = response[i];&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                promises.push(that.CoreSitePluginsProvider.getContent(&lt;br /&gt;
                        component, &#039;mobile_document_view&#039;, args).then(&lt;br /&gt;
                        function(result) {&lt;br /&gt;
                            var subPromises = [];&lt;br /&gt;
                            if (result.files &amp;amp;&amp;amp; result.files.length) {&lt;br /&gt;
                                subPromises.push(that.CoreFilepoolProvider.downloadOrPrefetchFiles(&lt;br /&gt;
                                        site.id, result.files, true, false, component, module.id));&lt;br /&gt;
                            }&lt;br /&gt;
                            return Promise.all(subPromises);&lt;br /&gt;
                        }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            return Promise.all(promises);&lt;br /&gt;
        });&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Single activity course format=====&lt;br /&gt;
&lt;br /&gt;
In the following example, the value of INIT_TEMPLATES[&amp;quot;main&amp;quot;] is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;core-dynamic-component [component]=&amp;quot;componentClass&amp;quot; [data]=&amp;quot;data&amp;quot;&amp;gt;&amp;lt;/core-dynamic-component&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This template is returned by the init method. And this is the JavaScript code returned by the init method:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function getAddonSingleActivityFormatComponent() {&lt;br /&gt;
    function AddonSingleActivityFormatComponent() {&lt;br /&gt;
        this.data = {};&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.constructor = AddonSingleActivityFormatComponent;&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.ngOnChanges = function(changes) {&lt;br /&gt;
        var self = this;&lt;br /&gt;
&lt;br /&gt;
        if (this.course &amp;amp;&amp;amp; this.sections &amp;amp;&amp;amp; this.sections.length) {&lt;br /&gt;
            var module = this.sections[0] &amp;amp;&amp;amp; this.sections[0].modules &amp;amp;&amp;amp; this.sections[0].modules[0];&lt;br /&gt;
            if (module &amp;amp;&amp;amp; !this.componentClass) {&lt;br /&gt;
                that.CoreCourseModuleDelegate.getMainComponent(that.Injector, this.course, module).then((component) =&amp;gt; {&lt;br /&gt;
                    self.componentClass = component || that.CoreCourseUnsupportedModuleComponent;&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.data.courseId = this.course.id;&lt;br /&gt;
            this.data.module = module;&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.doRefresh = function(refresher, done) {&lt;br /&gt;
        return Promise.resolve(this.dynamicComponent.callComponentFunction(&amp;quot;doRefresh&amp;quot;, [refresher, done]));&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
return AddonSingleActivityFormatComponent;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function AddonSingleActivityFormatHandler() {&lt;br /&gt;
    this.name = &amp;quot;singleactivity&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.constructor = AddonSingleActivityFormatHandler;&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.isEnabled = function() {&lt;br /&gt;
    return true;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.canViewAllSections = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseTitle = function(course, sections) {&lt;br /&gt;
    if (sections &amp;amp;&amp;amp; sections[0] &amp;amp;&amp;amp; sections[0].modules &amp;amp;&amp;amp; sections[0].modules[0]) {&lt;br /&gt;
        return sections[0].modules[0].name;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return course.fullname || &amp;quot;&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displayEnableDownload = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displaySectionSelector = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseFormatComponent = function(injector, course) {&lt;br /&gt;
    that.Injector = injector || that.Injector;&lt;br /&gt;
&lt;br /&gt;
    return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES[&amp;quot;main&amp;quot;], getAddonSingleActivityFormatComponent(), injector);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using the JavaScript API===&lt;br /&gt;
&lt;br /&gt;
The Javascript API is partly supported right now, only the delegates specified in the section [[Mobile_support_for_plugins#Templates_downloaded_on_login_and_rendered_using_JS_data_2|Templates downloaded on login and rendered using JS data]] supports it now. This API allows you to override any of the functions of the default handler. &lt;br /&gt;
&lt;br /&gt;
The “method” specified in a handler registered in the &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; will be called immediately after the init method, and the Javascript returned by this method will be run. If this Javascript code returns an object with certain functions, these function will override the ones in the default handler.&lt;br /&gt;
&lt;br /&gt;
For example, if the Javascript returned by the method returns something like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The the &#039;&#039;getData&#039;&#039; function of the default handler will be overridden by the returned getData function.&lt;br /&gt;
&lt;br /&gt;
The default handler for &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; only has 2 functions: &#039;&#039;getComponent&#039;&#039; and &#039;&#039;getData&#039;&#039;. In addition, the JavaScript code can return an extra function named &#039;&#039;componentInit&#039;&#039; that will be executed when the component returned by &#039;&#039;getComponent&#039;&#039; is initialized.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to support the text user profile field using this API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
var result = {&lt;br /&gt;
    componentInit: function() {&lt;br /&gt;
        if (this.field &amp;amp;&amp;amp; this.edit &amp;amp;&amp;amp; this.form) {&lt;br /&gt;
            this.field.modelName = &amp;quot;profile_field_&amp;quot; + this.field.shortname;&lt;br /&gt;
&lt;br /&gt;
            if (this.field.param2) {&lt;br /&gt;
                this.field.maxlength = parseInt(this.field.param2, 10) || &amp;quot;&amp;quot;;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? &amp;quot;password&amp;quot; : &amp;quot;text&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
            var formData = {&lt;br /&gt;
                value: this.field.defaultdata,&lt;br /&gt;
                disabled: this.disabled&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            this.form.addControl(this.field.modelName, that.FormBuilder.control(formData, this.field.required &amp;amp;&amp;amp; !this.field.locked ? that.Validators.required : null));&lt;br /&gt;
        }&lt;br /&gt;
    },&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
        var name = &amp;quot;profile_field_&amp;quot; + field.shortname;&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            type: &amp;quot;text&amp;quot;,&lt;br /&gt;
            name: name,&lt;br /&gt;
            value: that.CoreTextUtilsProvider.cleanTags(formValues[name])&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Translate dynamic strings===&lt;br /&gt;
&lt;br /&gt;
If you wish to have an element that displays a localised string based on value from your template you can doing something like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content translate&amp;gt;&lt;br /&gt;
        plugin.mod_myactivity.&amp;lt;% status %&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This could save you from having to write something like when only one value should be displayed:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
&amp;lt;ion-card&amp;gt;&lt;br /&gt;
    &amp;lt;ion-card-content&amp;gt;&lt;br /&gt;
        &amp;lt;%#isedting%&amp;gt;{{ &#039;plugin.mod_myactivity.editing&#039; | translate }}&amp;lt;%/isediting%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isopen%&amp;gt;{{ &#039;plugin.mod_myactivity.open&#039; | translate }}&amp;lt;%/isopen%&amp;gt;&lt;br /&gt;
        &amp;lt;%#isclosed%&amp;gt;{{ &#039;plugin.mod_myactivity.closed&#039; | translate }}&amp;lt;%/isclosed%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-card-content&amp;gt;&lt;br /&gt;
&amp;lt;/ion-card&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using strings with dates===&lt;br /&gt;
&lt;br /&gt;
If you have a string that you wish to pass a formatted date for example in the Moodle language file you have:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$string[&#039;strwithdate&#039;] = &#039;This string includes a date of {$a-&amp;gt;date} in the middle of it.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can localise the string correctly in your template using something like the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
{{ &#039;plugin.mod_myactivity.strwithdate&#039; | translate: {$a: { date: &amp;lt;% timestamp %&amp;gt; * 1000 | coreFormatDate: &amp;quot;dffulldate&amp;quot; } } }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A Unix timestamp must be multiplied by 1000 as the Mobile App expects millisecond timestamps, where as Unix timestamps are in seconds.&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
&lt;br /&gt;
=== Invalid response received ===&lt;br /&gt;
&lt;br /&gt;
You might receive this error when using the &amp;quot;core-site-plugins-call-ws&amp;quot; directive or similar. By default, the app expects all WebService calls to return an object, if your WebService returns another type (string, bool, ...) then you need to specify it using the preSets attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{typeExpected: &#039;boolean&#039;}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
In a similar way, if your WebService returns null you need to tell the app not to expect any result using the preSets:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{responseExpected: false}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS ===&lt;br /&gt;
&lt;br /&gt;
Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, ion-radio, ion-checkbox and ion-select don&#039;t use HTML inputs, they simulate them, so the directive isn&#039;t going to find their data and so it won&#039;t be sent to the WebService.&lt;br /&gt;
&lt;br /&gt;
There are 2 workarounds to fix this problem. It seems that the next major release of Ionic framework does use HTML inputs, so these are temporary solutions.&lt;br /&gt;
&lt;br /&gt;
==== Sending the data manually ====&lt;br /&gt;
&lt;br /&gt;
The first solution is to send the missing params manually using the &amp;quot;&#039;&#039;params&#039;&#039;&amp;quot; property. We will use &#039;&#039;ngModel&#039;&#039; to store the input value in a variable, and this variable will be passed to the params. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a template like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group name=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, responses: responses}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Basically, you need to add &#039;&#039;ngModel&#039;&#039; to the affected element (in this case, the &#039;&#039;radio-group&#039;&#039;). You can put whatever name you want as the value, we used &amp;quot;responses&amp;quot;. With this, everytime the user selects a radio button the value will be stored in a variable named &amp;quot;responses&amp;quot;. Then, in the button we are passing this variable to the params of the WebService.&lt;br /&gt;
&lt;br /&gt;
Please notice that the &amp;quot;form&amp;quot; attribute has priority over &amp;quot;params&amp;quot;, so if you have an input with name=&amp;quot;responses&amp;quot; it will override what you&#039;re manually passing to params.&lt;br /&gt;
&lt;br /&gt;
==== Using a hidden input ====&lt;br /&gt;
&lt;br /&gt;
Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use &#039;&#039;ngModel&#039;&#039; to synchronize your ion-radio/ion-checkbox/ion-select with the new hidden input. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a radio button like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot; [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;hidden&amp;quot; [ngModel]=&amp;quot;responses&amp;quot; name=&amp;quot;responses&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we&#039;re using a variable named &amp;quot;responses&amp;quot; to synchronize the data between the &#039;&#039;radio-group&#039;&#039; and the hidden input. You can use whatever name you want.&lt;br /&gt;
&lt;br /&gt;
=== I can&#039;t return an object or array in otherdata ===&lt;br /&gt;
&lt;br /&gt;
If you try to return an object or an array in any field inside &#039;&#039;otherdata&#039;&#039;, the WebService call will fail with the following error:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Scalar type expected, array or object received&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Each field in &#039;&#039;otherdata&#039;&#039; must be a string, number or boolean, it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($data))&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The app will automatically parse this JSON and convert it back into an array or object.&lt;br /&gt;
&lt;br /&gt;
==Examples==&lt;br /&gt;
&lt;br /&gt;
===Accepting dynamic names in a WebService===&lt;br /&gt;
&lt;br /&gt;
We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new WebService that we have created.&lt;br /&gt;
&lt;br /&gt;
The first issue we find is that the WebService needs to define the names of the parameters received, but in this case they&#039;re dynamic. The solution is to accept an array of objects with name and value. So in the &#039;&#039;_parameters()&#039;&#039; function of our new WebService, we will add this parameter:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;data&#039; =&amp;gt; new external_multiple_structure(&lt;br /&gt;
     new external_single_structure(&lt;br /&gt;
        array(&lt;br /&gt;
            &#039;name&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data name&#039;),&lt;br /&gt;
            &#039;value&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data value&#039;),&lt;br /&gt;
        )&lt;br /&gt;
    ),&lt;br /&gt;
    &#039;The data to be saved&#039;, VALUE_DEFAULT, array()&lt;br /&gt;
)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now we need to adapt our form to send the data as the WebService requires it. In our template, we have a button with the directive &#039;&#039;core-site-plugins-call-ws&#039;&#039; that will send the form data to our WebService. To make this work we will have to pass the parameters manually, without using the &amp;quot;&#039;&#039;form&#039;&#039;&amp;quot; attribute, because we need to format the data before it is sent.&lt;br /&gt;
&lt;br /&gt;
Since we will send the params manually and we want it all to be sent in the same array, we will use &#039;&#039;ngModel&#039;&#039; to store the input data into a variable that we&#039;ll call &amp;quot;data&amp;quot;, but you can use the name you want. This &amp;quot;data&amp;quot; will be an object that will hold the input data with the format &amp;quot;name-&amp;gt;value&amp;quot;. For example, if I have an input with name &amp;quot;a1&amp;quot; and value &amp;quot;My answer&amp;quot;, the data object will be:&lt;br /&gt;
&lt;br /&gt;
{a1: &amp;quot;My answer&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
So we need to add &#039;&#039;ngModel&#039;&#039; to all the inputs whose values need to be sent to the &amp;quot;data&amp;quot; WS param. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&amp;lt;ion-input name=&amp;quot;&amp;lt;% name %&amp;gt;&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.data[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re using &#039;&#039;CONTENT_OTHERDATA&#039;&#039; to store the data. We do it like this because we&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the form, setting the values the user has already stored. If you don&#039;t need to initialize the form, then you can use the variable &amp;quot;dataObject&amp;quot;, an empty object that the Mobile app creates for you: [(ngModel)]=&amp;quot;dataObject[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&lt;br /&gt;
&lt;br /&gt;
The Mobile app has a function that allows you to convert this data object into an array like the one the WS expects: &#039;&#039;objectToArrayOfObjects&#039;&#039;. So in our button we&#039;ll use this function to format the data before it&#039;s sent:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_ws_name&amp;quot;&lt;br /&gt;
    [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, data: CoreUtilsProvider.objectToArrayOfObjects(CONTENT_OTHERDATA.data, &#039;name&#039;, &#039;value&#039;)}&amp;quot;&lt;br /&gt;
    successMessage&lt;br /&gt;
    refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see in the example above, we&#039;re specifying that the keys of the &amp;quot;data&amp;quot; object need to be stored in a property named &amp;quot;name&amp;quot;, and the values need to be stored in a property named &amp;quot;value&amp;quot;. If your WebService expects different names you need to change the parameters of the function &#039;&#039;objectToArrayOfObjects&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
If you open your plugin now in the Mobile app it will display an error in the Javascript console. The reason is that the variable &amp;quot;data&amp;quot; doesn&#039;t exist inside &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. As it is explained in previous sections, &#039;&#039;CONTENT_OTHERDATA&#039;&#039; holds the data that you return in &#039;&#039;otherdata&#039;&#039; for your method. We&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the values to be displayed in the form.&lt;br /&gt;
&lt;br /&gt;
If the user hasn&#039;t answered the form yet, we can initialize the &amp;quot;data&amp;quot; object as an empty object. Please remember that we cannot return arrays or objects in &#039;&#039;otherdata&#039;&#039;, so we&#039;ll return a JSON string.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; &#039;{}&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$userdata = get_user_responses(); // It will held the data in a format name-&amp;gt;value. Example: array(&#039;a1&#039; =&amp;gt; &#039;My value&#039;).&lt;br /&gt;
...&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($userdata))&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our WebService in array format.&lt;br /&gt;
&lt;br /&gt;
==Moodle plugins with mobile support==&lt;br /&gt;
&lt;br /&gt;
* Group choice: [https://moodle.org/plugins/mod_choicegroup Moodle plugins directory entry] and [https://github.com/ndunand/moodle-mod_choicegroup code in github].&lt;br /&gt;
* Custom certificate: [https://moodle.org/plugins/mod_customcert Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_customcert code in github].&lt;br /&gt;
* Gapfill question type: [https://moodle.org/plugins/qtype_gapfill Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_gapfill in github].&lt;br /&gt;
* Wordselect question type: [https://moodle.org/plugins/qtype_wordselect Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_wordselect in github].&lt;br /&gt;
* RegExp question type: [https://moodle.org/plugins/qtype_regexp Moodle plugins directory entry] and [https://github.com/rezeau/moodle-qtype_regexp in github].&lt;br /&gt;
* Certificate: [https://moodle.org/plugins/mod_certificate Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_certificate in github].&lt;br /&gt;
* Attendance [https://moodle.org/plugins/mod_attendance Moodle plugins directory entry] and [https://github.com/danmarsden/moodle-mod_attendance in github].&lt;br /&gt;
* ForumNG (unfinished support) [https://moodle.org/plugins/mod_forumng Moodle plugins directory entry] and [https://github.com/moodleou/moodle-mod_forumng in github].&lt;br /&gt;
* News block [https://github.com/moodleou/moodle-block_news in github].&lt;br /&gt;
&lt;br /&gt;
See the complete list in the plugins database [https://moodle.org/plugins/browse.php?list=award&amp;amp;id=6 here] (it may contain some outdated plugins)&lt;br /&gt;
[[Category:Mobile]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=55768</id>
		<title>Running acceptance test</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Running_acceptance_test&amp;diff=55768"/>
		<updated>2019-03-21T13:23:51Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Advance usage */ Added documentation for the new timeout feature.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Short version ==&lt;br /&gt;
&lt;br /&gt;
...or how I got it to work on Ubuntu and some of the problems encountered. &lt;br /&gt;
&lt;br /&gt;
You need a bunch of browsers and terminal windows open to do this.&lt;br /&gt;
&lt;br /&gt;
==== 1. Background ====  &lt;br /&gt;
&lt;br /&gt;
# I am using the desktop version of Ubuntu 17.04 so there are no issues about running this software in headless mode. Running in headless mode was not tested.&lt;br /&gt;
# Moodle is version 3.3 and is a fully installed and working version using the &#039;standard&#039; Ubuntu LAMP stack.&lt;br /&gt;
&lt;br /&gt;
==== 2. Set up Selenium ====&lt;br /&gt;
&lt;br /&gt;
# Download the Selenium Standalone Server from [http://www.seleniumhq.org/download/ http://www.seleniumhq.org/download/]. It&#039;s a single JAR file, put it anywhere handy.&lt;br /&gt;
# Download the Chrome driver from [https://sites.google.com/a/chromium.org/chromedriver/ https://sites.google.com/a/chromium.org/chromedriver/] (Forget trying to use Firefox, the latest version is not compatible)&lt;br /&gt;
# Unzip the driver (it&#039;s a single file) and copy to /usr/local/bin (should work anywhere on the path)&lt;br /&gt;
# If not installed already, &#039;&amp;lt;tt&amp;gt;sudo apt install default-jre&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Start Selenium - &#039;&amp;lt;tt&amp;gt;java -jar /path/to/your/selenium/server/selenium-server-standalone-N.NN.N.jar -port 4444&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# check it works, access &#039;localhost:4444/wd/hub/&#039; in your browser and check you can create a new Chrome session.&lt;br /&gt;
&lt;br /&gt;
If running headless or the above doesn&#039;t work (&amp;quot;Selenium server is not running&amp;quot; when running the behat tests). Try the following&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;Xvfb -ac :99 -screen 0 1280x1024x16 &amp;amp;&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# Then immediately, &#039;&amp;lt;tt&amp;gt;export DISPLAY=:99&amp;lt;/tt&amp;gt;&#039;&lt;br /&gt;
# The run the Selenium command as above&lt;br /&gt;
&lt;br /&gt;
==== 3. Set up Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Create a new &#039;dataroot&#039; area for files especially for behat adjusting permissions accordingly. &lt;br /&gt;
# If not there already, add Section 11 from config-dist.php to your config.php file and review the settings. &lt;br /&gt;
# $CFG-&amp;gt;behat_wwwroot needs to point to your Moodle site yet be different from the &#039;normal&#039; wwwroot (e.g. if you used localhost for wwwroot use 127.0.0.1 for the behat_wwwroot). Whatever you choose, make sure it works. &lt;br /&gt;
# $CFG-&amp;gt;behat_dataroot should point to the directory you created above&lt;br /&gt;
# $CFG-&amp;gt;behat_prefix should be fine. &lt;br /&gt;
# Set up $CFG-&amp;gt;behat_profiles to select Chrome as the browser...&lt;br /&gt;
&lt;br /&gt;
    $CFG-&amp;gt;behat_profiles = [&lt;br /&gt;
        &#039;default&#039; =&amp;gt; [&lt;br /&gt;
            &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
                &#039;extensions&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;Behat\MinkExtension&#039; =&amp;gt; [&lt;br /&gt;
                        &#039;selenium2&#039; =&amp;gt; [&lt;br /&gt;
                            &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
                        ]&lt;br /&gt;
                    ]&lt;br /&gt;
                ]&lt;br /&gt;
            ]&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
==== 4. Configure Behat for Moodle ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;php admin/tool/behat/cli/init.php&amp;lt;/tt&amp;gt;&#039; (from the root of your Moodle install). This installs all the required software and creates the test version of Moodle. &lt;br /&gt;
&lt;br /&gt;
==== 5. Run Behat tests ====&lt;br /&gt;
&lt;br /&gt;
# Run &#039;&amp;lt;tt&amp;gt;vendor/bin/behat&amp;lt;/tt&amp;gt;&#039;. If you don&#039;t want to run all the tests add &#039;&amp;lt;tt&amp;gt;--tags=&amp;quot;@something&amp;quot;&amp;lt;/tt&amp;gt;&#039; where the @something refers to the tags at the top of most feature files. Use comma-separated list like &#039;&amp;lt;tt&amp;gt;@some_thing,@some_thing_else&amp;lt;/tt&amp;gt;&#039; to run tests from multiple areas. See upstream documentation on Gherkin filters for advanced syntax and more complex examples.&lt;br /&gt;
# After some initial setup dots should start to go by. It&#039;s a while before Selenium is first accessed. On the Linux desktop a new Chrome window appears and the testing process &#039;remote control&#039; starts (hopefully!)&lt;br /&gt;
&lt;br /&gt;
== Prerequisite ==&lt;br /&gt;
Before initializing acceptance test environment for running behat, you should ensure:&lt;br /&gt;
# [[Acceptance_testing#Requirements Meet min. system requirements for running tests]]&lt;br /&gt;
# [[Acceptance_testing#Installation Have set min. config variable in config.php for behat]]&lt;br /&gt;
# [[Acceptance_testing#Installation Downloaded composer dependencies]]&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
Acceptance tests (also known as behat), use [http://www.seleniumhq.org/download/ Selenium server] and can be run as:&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; In single run, only one behat run is executed. So all features are executed in this single run.&lt;br /&gt;
# &#039;&#039;&#039;Parallel runs:&#039;&#039;&#039; (Since Moodle 3.0) Parallel runs allow dev&#039;s to execute multiple behat runs together. This was introduced to get acceptance tests results faster. To achieve this:&lt;br /&gt;
#* Features are divided between multiple behat runs&lt;br /&gt;
#* Symlinks behatrun{x} (x being the run process number), are created pointing to moodle directory, so site for run 1 is accessible via https://localhost/moodle/behatrun1&lt;br /&gt;
#* Process number is included as suffix to $CFG-&amp;gt;behat_prefix.&lt;br /&gt;
#* Process number is suffixed to $CFG-&amp;gt;behat_dataroot.&lt;br /&gt;
&lt;br /&gt;
== Step 1: Initialise acceptance test environment ==&lt;br /&gt;
Before running acceptance tests, environment needs to be initialised for acceptance testing.&lt;br /&gt;
&lt;br /&gt;
=== Single run ===&lt;br /&gt;
For initialising acceptance tests for single run, above command is sufficient.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For initialising acceptance tests for parallel runs, you can use one of the following options&lt;br /&gt;
# &#039;&#039;&#039;-j or --parallel&#039;&#039;&#039; (required) Number of parallel behat run to initialise&lt;br /&gt;
# &#039;&#039;&#039;-m or --maxruns&#039;&#039;&#039;  (optional) Max parallel site which should be initialised at one time. If your system is slow, then you can initialise sites in chucks.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun&#039;&#039;&#039; (optional) Initialise site to run specified run from. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;--torun&#039;&#039;&#039; (optional) Initialise site to run specified run till. Used for running acceptance tests on different vms&lt;br /&gt;
# &#039;&#039;&#039;-o or --optimize-runs&#039;&#039;&#039; (optional) This option will split features with specified tags in all parallel runs, so they are executed first when parallel run gets executed.&lt;br /&gt;
# &#039;&#039;&#039;-a or --add-core-features-to-theme&#039;&#039;&#039; (optional) Since Moodle 3.2. Use this option to add all core features to specified themes (comma separated list of themes)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
// Below command will initialise moodle to run 2 parallel tests.&lt;br /&gt;
php admin/tool/behat/cli/init.php --parallel=2&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 2: Running acceptance test environment ==&lt;br /&gt;
=== Single run ===&lt;br /&gt;
Run either of the following commands. For more options &#039;&#039;&#039;vendor/bin/behat --help&#039;&#039;&#039; or http://docs.behat.org/guides/6.cli.html&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
vendor/bin/behat --config /path/to/your/CFG_behat_dataroot/behatrun/behat/behat.yml&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Parallel runs ===&lt;br /&gt;
For running parallel runs, use following command&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/run.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Following optional options are available for custom run:&lt;br /&gt;
# &#039;&#039;&#039;--feature&#039;&#039;&#039; Only execute specified feature file (Absolute path of feature file).&lt;br /&gt;
# &#039;&#039;&#039;--suite&#039;&#039;&#039; Features for specified theme will be executed.&lt;br /&gt;
# &#039;&#039;&#039;--replace&#039;&#039;&#039; Replace args string with run process number, useful for output and reruns.&lt;br /&gt;
# &#039;&#039;&#039;--fromrun&#039;&#039;&#039; Execute run starting from (Used for parallel runs on different vms)&lt;br /&gt;
# &#039;&#039;&#039;--torun&#039;&#039;&#039; Execute run till (Used for parallel runs on different vms)&lt;br /&gt;
# &#039;&#039;&#039;-a or --add-core-features-to-theme&#039;&#039;&#039; (optional) Since Moodle 3.2. Use this option to add all core features to specified theme&#039;s (comma separated list)&lt;br /&gt;
# Behat options can be passed for filtering features/scenarios:&lt;br /&gt;
#* In case you don&#039;t want to run Javascript tests, use the Behat tags option to skip them, &#039;&#039;&#039;--tags=&amp;quot;~@javascript&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific scenario, use the Behat name option to run it, &#039;&#039;&#039;--name=&amp;quot;Filter user accounts by role and cohort&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
#* In case you want to run specific feature file, use the Behat feature option to run it, &#039;&#039;&#039;--feature=&amp;quot;/PATH/TO/MOODLE/admin/tests/behat/filter_users.feature&amp;quot;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Common options for running tests ===&lt;br /&gt;
==== Tests filters ====&lt;br /&gt;
With the &#039;&#039;&#039;--tags&#039;&#039;&#039; or the &#039;&#039;&#039;-name&#039;&#039;&#039; Behat options you can filter which tests are going to run or which ones are going to be skipped. There are a few tags that you might be interested in:&lt;br /&gt;
* &#039;&#039;&#039;@javascript&#039;&#039;&#039;: All the tests that runs in a browser using Javascript; they require Selenium to be running, otherwise an exception will be thrown.&lt;br /&gt;
* &#039;&#039;&#039;@_file_upload&#039;&#039;&#039;: All the tests that involves file uploading or any OS feature that is not 100% part of the browser. They should only be executed when Selenium is running in the same machine where the tests are running.&lt;br /&gt;
* &#039;&#039;&#039;@_alert&#039;&#039;&#039;: All the tests that involves Javascript dialogs (alerts, confirms...) are using a feature that is OS-dependant and out of the browser scope, so they should be tag appropriately as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_window&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; window&#039;&#039;&#039; step should be tagged as not all browsers manage them properly.&lt;br /&gt;
* &#039;&#039;&#039;@_switch_iframe&#039;&#039;&#039;: All the tests that are using the &#039;&#039;&#039;I switch to &amp;quot;NAME&amp;quot; window&#039;&#039;&#039; steps should be tagged as it is an advanced feature and some browsers may have problems dealing with them&lt;br /&gt;
* &#039;&#039;&#039;@_cross_browser&#039;&#039;&#039;: All the tests that should run against multiple combinations of browsers + OS in a regular basis. The features that are sensitive to different combinations of OS and browsers should be tagges as @_cross_browser.&lt;br /&gt;
* &#039;&#039;&#039;@componentname&#039;&#039;&#039;: Moodle features uses the [https://docs.moodle.org/dev/Frankenstyle Frankenstyle] component name to tag the features according to the Moodle subsystem they belong to.&lt;br /&gt;
&lt;br /&gt;
==== Output formats ====&lt;br /&gt;
Since Moodle 3.1 option for output is:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=pretty --out=/path/to/pretty.txt --format=moodle_progress --out=std&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Before Moodle 3.1 option for output was:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
--format=&#039;moodle_progress,pretty&#039; --out=&#039;,/path/to/pretty.txt&#039;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Following output formats are supported:&lt;br /&gt;
# &#039;&#039;&#039;progress&#039;&#039;&#039;: Prints one character per step.&lt;br /&gt;
# &#039;&#039;&#039;pretty&#039;&#039;&#039;: Prints the feature as is.&lt;br /&gt;
# &#039;&#039;&#039;junit&#039;&#039;&#039;: Outputs the failures in JUnit compatible files.&lt;br /&gt;
# &#039;&#039;&#039;moodle_progress&#039;&#039;&#039;: Prints Moodle branch information and dots for each step.&lt;br /&gt;
# &#039;&#039;&#039;moodle_list&#039;&#039;&#039;: List all scenarios.&lt;br /&gt;
# &#039;&#039;&#039;moodle_stepcount&#039;&#039;&#039;: List all features with total steps in each feature file. Used for parallel run.&lt;br /&gt;
# &#039;&#039;&#039;moodle_screenshot&#039;&#039;&#039;: (since Moodle 3.1) Take screenshot and core dump of each step. With following options you can dump either or both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;image&amp;quot;}&#039;**: will dump image only&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;}&#039;**: will dump html only.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html,image&amp;quot;}&#039;**: will dump both.&lt;br /&gt;
## --format-settings &#039;{&amp;quot;formats&amp;quot;: &amp;quot;html&amp;quot;, &amp;quot;dir_permissions&amp;quot;: &amp;quot;0777&amp;quot;}&#039;**&lt;br /&gt;
If you want to see the failures immediately (rather than waiting ~3 hours for all the tests to finish) then either use the -v option to output a bit more information, or change the output format using --format. Format &#039;pretty&#039; (&#039;&#039;&#039;-f pretty&#039;&#039;&#039;) is sufficient for most cases, as it outputs each step outcomes in the command line making easier to see the progress.&lt;br /&gt;
&lt;br /&gt;
== Advance usage ==&lt;br /&gt;
=== Rerun failed scenarios ===&lt;br /&gt;
With slow systems or parallel run you might see some random failures, to rerun only failed scenarios (to eliminate random failures), use --rerun option&lt;br /&gt;
# &#039;&#039;&#039;Single run:&#039;&#039;&#039; --run=&amp;quot;absolute_path_to_empty_file&amp;quot; (Behat will record failed scenarios in this file, and when run again only failed scenarios will be run)&lt;br /&gt;
# &#039;&#039;&#039;Parallel run:&#039;&#039;&#039; --rerun=&amp;quot;absolute_path_to_empty_file_{runprocess}.txt --replace=&amp;quot;{runprocess}&amp;quot; ({runprocess} will be replaced with the process number for recording fails in the specific run process).&lt;br /&gt;
&#039;&#039;&#039;Since Moodle 3.1 --rerun option don&#039;t accept any value, as it is handled internally by behat&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Running behat with specified theme (Since Moodle 3.2) ===&lt;br /&gt;
You can run behat with any theme installed. To execute behat with specified theme use &#039;&#039;&#039;--suite={THEME_NAME}&#039;&#039;&#039; option, while running behat. By default the features in theme behat folder will be executed for the suite. But if you want to run all core features with the specific theme then initialise acceptance test with -a option. For example, &#039;&#039;&#039;-a {THEME_NAME}&#039;&#039;&#039; e.g.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php -a clean&lt;br /&gt;
vendor/bin/behat --suite=clean --tags=&amp;quot;@enrol_foobar&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That is a core theme but it will work with custom theme. No = or quotes needed around the theme name.&lt;br /&gt;
&lt;br /&gt;
Make sure that &amp;lt;tt&amp;gt;$CFG-&amp;gt;theme&amp;lt;/tt&amp;gt; is &#039;&#039;&#039;not set&#039;&#039;&#039; in your config.php.&lt;br /&gt;
&lt;br /&gt;
==== Override behat core context for theme suite ====&lt;br /&gt;
To override behat step definitions so as to run behat with specified theme, you should create a contexts within &#039;&#039;&#039;/theme/{MYTHEME}/tests/behat/&#039;&#039;&#039; with prefix behat_theme_{MYTHEME}_ and suffixed with the context being overridden. For example, if you want to override behat_mod_forum context, then you should create a class /theme/{MYTHEME}/tests/behat/mod_forum/behat_theme_{MYTHEME}_behat_mod_forum.php&lt;br /&gt;
&lt;br /&gt;
==== Blacklist behat context or features to run in theme suite ====&lt;br /&gt;
To blacklist contexts/ features to be executed by theme suite you should create a /theme/{MYTHEME}/tests/behat/blacklist.json file with following format. Following will not use step_definitions from  behat_grade and behat_navigation while running theme suite. Also, scenarios in auth/tests/behat/login.feature and grade/tests/behat/grade_hidden_items.feature won&#039;t be executed with theme suite.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;contexts&amp;quot;: [&lt;br /&gt;
    &amp;quot;behat_grade&amp;quot;,&lt;br /&gt;
    &amp;quot;behat_navigation&amp;quot;,&lt;br /&gt;
  ],&lt;br /&gt;
  &amp;quot;features&amp;quot;: [&lt;br /&gt;
    &amp;quot;auth/tests/behat/login.feature&amp;quot;,&lt;br /&gt;
    &amp;quot;grade/tests/behat/grade_hidden_items.feature&amp;quot;,&lt;br /&gt;
   ]&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
==== Override core behat selectors ====&lt;br /&gt;
To override behat selectors in specific theme, you should create a class behat_theme_{MYTHEME}_behat_selectors in /theme/{MYTHEME}/tests/behat/behat_theme_{MYTHEME}_behat_selectors.php extending behat_selectors.&lt;br /&gt;
&lt;br /&gt;
=== Use php built in web server ===&lt;br /&gt;
You can use php built-in-web server for executing behat runs. To do so:&lt;br /&gt;
# Open a command line interface and &#039;&#039;&#039;cd /to/your/moodle/dirroot&#039;&#039;&#039;&lt;br /&gt;
# &#039;&#039;&#039;php -S localhost:8000&#039;&#039;&#039; (This is the test site URL that moodle uses by default, if you want to use another one you can override it in config.php with $CFG-&amp;gt;behat_wwwroot attribute; more info in https://docs.moodle.org/dev/Acceptance_testing#Advanced_usage or config-dist.php)&lt;br /&gt;
# Update $CFG-&amp;gt;behat_wwwroot = localhost:8000; in config.php&lt;br /&gt;
&lt;br /&gt;
=== Define custom options for parallel runs ===&lt;br /&gt;
You can set following custom config options for parallel runs via $CFG-&amp;gt;behat_parallel_run. It&#039;s an array of options where 1st array is for 1st run and so on.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
       array (&lt;br /&gt;
           &#039;dbtype&#039; =&amp;gt; &#039;mysqli&#039;,&lt;br /&gt;
           &#039;dblibrary&#039; =&amp;gt; &#039;native&#039;,&lt;br /&gt;
           &#039;dbhost&#039; =&amp;gt; &#039;localhost&#039;,&lt;br /&gt;
           &#039;dbname&#039; =&amp;gt; &#039;moodletest&#039;,&lt;br /&gt;
           &#039;dbuser&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;dbpass&#039; =&amp;gt; &#039;moodle&#039;,&lt;br /&gt;
           &#039;behat_prefix&#039; =&amp;gt; &#039;mdl_&#039;,&lt;br /&gt;
           &#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;,&lt;br /&gt;
           &#039;behat_wwwroot&#039; =&amp;gt; &#039;http://127.0.0.1/moodle&#039;,&lt;br /&gt;
           &#039;behat_dataroot&#039; =&amp;gt; &#039;/home/example/bht_moodledata&#039;&lt;br /&gt;
       )&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To set different selenium servers for parallel runs, you can use following. NOTE: Running parallel (headless) runs on different selenium servers avoid random focus failures.&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
    $CFG-&amp;gt;behat_parallel_run = array (&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4444/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4445/wd/hub&#039;),&lt;br /&gt;
        array (&#039;wd_host&#039; =&amp;gt; &#039;http://127.0.0.1:4446/wd/hub&#039;),&lt;br /&gt;
    );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Write new tests and behat methods ===&lt;br /&gt;
&lt;br /&gt;
If you want to write tests for your own integration, you can do so by creating new tests with format .feature. Follow instructions in [[Writing_acceptance_tests|this page]] to write new tests. &lt;br /&gt;
&lt;br /&gt;
It is also possible to add new steps the moodle behat integration. In order to do so, you will have to create a new .php class with the prefix &#039;&#039;&#039;behat_&#039;&#039;&#039;. Copy the format from &#039;&#039;&#039;lib\behat\behat_base.php&#039;&#039;&#039;, but set your class to extend the behat_base class instead of the MinkExtension. You can define new behat steps by declaring functions with the appropriate heading. &lt;br /&gt;
&lt;br /&gt;
You will not be  able to use these steps and features right away. Check [[Running_acceptance_test#New_step_definitions_or_features_are_not_executed|this section]] for instructions on how to update the behat integration. &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
For further information on how to create new steps definitions, check [[Acceptance testing/Custom acceptance steps]].&lt;br /&gt;
&lt;br /&gt;
=== Running acceptance tests with different browser ===&lt;br /&gt;
By default behat will run with Firefox browser through Selenium. By adding the following code to your config.php you can change the selected browser that is run when behat is invoked.  You will need to run php admin/tool/behat/cli/init.php for changes to take effect. Then use --profile=&#039;chrome&#039;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_profiles = array(&lt;br /&gt;
   &#039;chrome&#039; =&amp;gt; array(&lt;br /&gt;
       &#039;browser&#039; =&amp;gt; &#039;chrome&#039;,&lt;br /&gt;
       &#039;tags&#039; =&amp;gt; &#039;@javascript&#039;,&lt;br /&gt;
   )&lt;br /&gt;
);&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
[[Acceptance_testing/Browsers|More info about alternative browsers]]&lt;br /&gt;
&lt;br /&gt;
=== Start multiple selenium servers ===&lt;br /&gt;
From command line Start selenium servers at different ports (say 4444, 4445, 4446 for 3 parallel runs)&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4444 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4445 &amp;amp;&lt;br /&gt;
java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port 4446&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternative way of running three Selenium servers in parallel:&lt;br /&gt;
&lt;br /&gt;
 $ printf %d\\n {4444..4446} | xargs -n 1 -P 3 java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar -port&lt;br /&gt;
&lt;br /&gt;
=== Using Docker to start selenium server ===&lt;br /&gt;
==== What is Docker ====&lt;br /&gt;
Docker is a app container,  it&#039;s a kind of virtual machine, but only for one app, service,  so you can download&lt;br /&gt;
a docker image and run a selenium server without worry in how to configure selenium in your machine, one for chrome, others for firefox, you either don&#039;t need to install the browsers in your machine&lt;br /&gt;
To install docker follow this link; https://docs.docker.com/engine/installation/&lt;br /&gt;
&lt;br /&gt;
==== Selenium docker images ====&lt;br /&gt;
There is many docker images available,  for many browser, the complete list is in https://hub.docker.com/u/selenium/&lt;br /&gt;
for moodle you can use standalone version.&lt;br /&gt;
You can download  specific selenium version too,  for example,  for firefox,  moodle recommend selenium 2.53.1, see: [https://docs.moodle.org/dev/Acceptance_testing/Browsers/Working_combinations_of_OS%2BBrowser%2Bselenium What version do I need?]&lt;br /&gt;
&lt;br /&gt;
so  the command will be:&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
docker run -d -p4444:4444 selenium/standalone-firefox:2.53.1-beryllium&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
to see all available version click in tags.   For firefox you can find at: https://hub.docker.com/r/selenium/standalone-firefox/tags/&lt;br /&gt;
&lt;br /&gt;
==== Change config.php file ====&lt;br /&gt;
In config.php file you must change the $CFG-&amp;gt;behat_wwwroot=   to your network card (NIC) ip address,  you can&#039;t use &lt;br /&gt;
localhost , 127.0.0.1, ...  or selenium docker server  will fail&lt;br /&gt;
&lt;br /&gt;
=== Increasing timeouts ===&lt;br /&gt;
&lt;br /&gt;
You may see errors such as:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
Javascript code and/or AJAX requests are not ready after 10 seconds. &lt;br /&gt;
There is a Javascript error or the code is extremely slow.&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Sometimes this indicates a genuine problem with the code, but if you are using a slow computer, it could just mean that the browser was not yet ready. You may find that the test works if you run it again. If you get this error frequently, it might be useful to increase the timeout.&lt;br /&gt;
&lt;br /&gt;
It is possible to increase this timeout by adding a line in your config.php. (Requires Moodle versions 3.5 (from 3.5.6), 3.6 (from 3.6.4), or 3.7+.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_increasetimeout = 2;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will increase all the timeouts by a factor of 2; if that isn&#039;t sufficient, you could use 3. &lt;br /&gt;
&lt;br /&gt;
Increasing timeouts will make tests run a bit slower (because there are points where Behat waits up to a timeout to make sure something doesn&#039;t happen) so don&#039;t do this unless you need to.&lt;br /&gt;
&lt;br /&gt;
== NOTE ==&lt;br /&gt;
# Start the Selenium server (in case you want to run tests that involves Javascript)&lt;br /&gt;
#* (See http://www.installationpage.com/selenium/how-to-run-selenium-headless-firefox-in-ubuntu/ for running &#039;headless&#039; Firefox and xvfm in a server environment)&lt;br /&gt;
#* Open another command line interface and &#039;&#039;&#039;java -jar /path/to/your/selenium/server/selenium-server-standalone-2.NN.N.jar&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Trouble shooting ===&lt;br /&gt;
=== New step definitions or features are not executed === &lt;br /&gt;
If you are adding new tests or steps definitions update the tests list&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --enable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039; For parallel runs, all options for initialising parallel runs are valid &#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Tests are failing ===&lt;br /&gt;
If you followed all the steps and you receive an unknown weird error probably your system&#039;s Firefox version is not compatible with the Selenium version you are running.  Please refer Working combinations to ensure you have correct [[Acceptance_testing/Browsers#Working_combinations_of_OS.2BBrowser.2Bselenium]] of them to run acceptance test.&lt;br /&gt;
&lt;br /&gt;
=== Disable acceptance test environment ===&lt;br /&gt;
if you want to prevent access to test environment&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/util.php --disable&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Note that if you have the HTTP_PROXY environment variable set, which you may have had to do to run composer, then you also need to set NO_PROXY=localhost.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== External links ==&lt;br /&gt;
* Vagrant profile with Moodle and Behat preconfigured: https://github.com/mackensen/moodle-hat&lt;br /&gt;
* Docker containers for Moodle Developers and Behat: https://github.com/moodlehq/moodle-docker&lt;br /&gt;
* Docker environment with Behat preconfigured : https://github.com/tobiga/docker_moodle_environment&lt;br /&gt;
&lt;br /&gt;
[[Category:Quality Assurance]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing/Browsers/Working_combinations_of_OS%2BBrowser%2Bselenium&amp;diff=55709</id>
		<title>Acceptance testing/Browsers/Working combinations of OS+Browser+selenium</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing/Browsers/Working_combinations_of_OS%2BBrowser%2Bselenium&amp;diff=55709"/>
		<updated>2019-03-14T16:03:23Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Moodle 3.4 and up */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Working combinations of OS+Browser+selenium =&lt;br /&gt;
As OS, Browsers and Selenium keeps updating, some combination might fail on different Moodle releases.&lt;br /&gt;
&lt;br /&gt;
Following combinations have been tested at the time of release of Moodle version and will be supported for that combination.&lt;br /&gt;
&lt;br /&gt;
== Moodle 3.4 and up ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|OS&lt;br /&gt;
|Browser&lt;br /&gt;
|Selenium Server&lt;br /&gt;
|Chrome Driver&lt;br /&gt;
|IE Driver&lt;br /&gt;
|Notes&lt;br /&gt;
|-&lt;br /&gt;
|style=&amp;quot;white-space: nowrap;&amp;quot;|Linux - Debian Stretch&lt;br /&gt;
|style=&amp;quot;white-space: nowrap;&amp;quot;|Firefox 47.0.1&lt;br /&gt;
| [https://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar 3.141.59]&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
| Requires special behat config (to be added soon)&lt;br /&gt;
|-&lt;br /&gt;
| Linux - Debian Stretch&lt;br /&gt;
| Chrome 66&lt;br /&gt;
| [https://selenium-release.storage.googleapis.com/3.11/selenium-server-standalone-3.11.0.jar 3.11.0]&lt;br /&gt;
| [http://chromedriver.storage.googleapis.com/index.html?path=2.38/ 2.38]&lt;br /&gt;
| N/A&lt;br /&gt;
| Any other valid combination of browser, selenium and chromedriver should work ok, normally. Here there is a [https://github.com/SeleniumHQ/docker-selenium/releases good list] of them, as reference.&lt;br /&gt;
|-&lt;br /&gt;
| MacOS X&lt;br /&gt;
| Firefox 47.0.1&lt;br /&gt;
| [https://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar 3.141.59]&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
| Requires special behat config (to be added soon)&lt;br /&gt;
|-&lt;br /&gt;
| MacOS X&lt;br /&gt;
| Chrome 72&lt;br /&gt;
| [https://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar 3.141.59]&lt;br /&gt;
| [http://chromedriver.storage.googleapis.com/index.html?path=2.46/ 2.46]&lt;br /&gt;
| N/A&lt;br /&gt;
| Any other valid combination of browser, selenium and chromedriver should work ok normally. Here there is a [https://github.com/SeleniumHQ/docker-selenium/releases good list] of them, as reference.&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;6&amp;quot;|General note: Many of the combinations below, for Moodle 3.1 and up, should continue working acceptably well for Moodle 3.4 and up. Just the ones above are actively being used now (CI infrastructure, developers...), hence, verified to be running ok. Feel free to add any other working combination!&lt;br /&gt;
|-&lt;br /&gt;
| Windows&lt;br /&gt;
| Chrome 72&lt;br /&gt;
| [https://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar 3.141.59]&lt;br /&gt;
| [https://chromedriver.storage.googleapis.com/index.html?path=72.0.3626.69/ 72.0.3626.69]&lt;br /&gt;
| N/A&lt;br /&gt;
| Any other valid combination of browser, selenium and chromedriver should work ok normally. Here there is a [https://github.com/SeleniumHQ/docker-selenium/releases good list] of them, as reference.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Moodle 3.2 and 3.3 ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|OS&lt;br /&gt;
|Browser&lt;br /&gt;
|Selenium Server&lt;br /&gt;
|Chrome Driver&lt;br /&gt;
|IE Driver&lt;br /&gt;
|-&lt;br /&gt;
| Linux - Ubuntu 16.04&lt;br /&gt;
| Firefox 47.0.1&lt;br /&gt;
| [http://selenium-release.storage.googleapis.com/2.53/selenium-server-standalone-2.53.1.jar 2.53.1]&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|Linux - Ubuntu 16.04&lt;br /&gt;
|[https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F403380%2Fchrome-linux.zip?generation=1467337264475000&amp;amp;alt=media Chrome 53.0]&lt;br /&gt;
| [http://selenium-release.storage.googleapis.com/index.html?path=3.0/ 3.0.1]&lt;br /&gt;
| [http://chromedriver.storage.googleapis.com/index.html?path=2.25/ 2.25]&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| Linux - Ubuntu 16.04&lt;br /&gt;
| Phantomjs 2.1.1&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| Windows 7/10&lt;br /&gt;
| Firefox 47.0.1&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| Windows 7/10&lt;br /&gt;
| Chrome v53.0&lt;br /&gt;
| [http://selenium-release.storage.googleapis.com/index.html?path=3.0/ 3.0.1]&lt;br /&gt;
| [http://chromedriver.storage.googleapis.com/index.html?path=2.25/ 2.25]&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| MacOS X&lt;br /&gt;
| Firefox 47.0.1&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| MacOS X&lt;br /&gt;
| [http://www.slimjet.com/chrome/google-chrome-old-version.php Chrome v53.0]&lt;br /&gt;
| [http://selenium-release.storage.googleapis.com/index.html?path=3.0/ 3.0.1]&lt;br /&gt;
| [http://chromedriver.storage.googleapis.com/index.html?path=2.25/ 2.25]&lt;br /&gt;
| N/A&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Moodle 3.1 ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|OS&lt;br /&gt;
|Browser&lt;br /&gt;
|Selenium Server&lt;br /&gt;
|Chrome Driver&lt;br /&gt;
|IE Driver&lt;br /&gt;
|-&lt;br /&gt;
| Linux - Ubuntu 16.04&lt;br /&gt;
| [https://download.mozilla.org/?product=firefox-47.0.1-SSL&amp;amp;os=linux64&amp;amp;lang=en-GB Firefox 47.0.1]&lt;br /&gt;
| [http://selenium-release.storage.googleapis.com/2.53/selenium-server-standalone-2.53.1.jar 2.53.1]&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|Linux - Ubuntu 16.04&lt;br /&gt;
|[https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F386249%2Fchrome-linux.zip?generation=1460160957434000&amp;amp;alt=media Chrome 51.0]&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| 2.22&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| Linux - Ubuntu 16.04&lt;br /&gt;
| Phantomjs 2.1.1&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| Windows 7/10&lt;br /&gt;
| Firefox 47.0.1&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| Windows 7/10&lt;br /&gt;
| Chrome v51.0&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| 2.22&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| MacOS X&lt;br /&gt;
| Firefox 47.0.1&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| MacOS X&lt;br /&gt;
| Chrome v51.0&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| 2.22&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
| MacOS X&lt;br /&gt;
| PhantomJS 2.1.1&lt;br /&gt;
| 2.53.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Moodle 3.0 and lower ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|OS&lt;br /&gt;
|Browser&lt;br /&gt;
|Selenium Server&lt;br /&gt;
|Chrome Driver&lt;br /&gt;
|IE Driver&lt;br /&gt;
|-&lt;br /&gt;
|Linux - Ubuntu 14.10&lt;br /&gt;
|Firefox 42.0&lt;br /&gt;
|2.47.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|Linux - Ubuntu 14.10&lt;br /&gt;
|Chrome 46.0&lt;br /&gt;
|2.47.1&lt;br /&gt;
| 2.19.346067&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|Linux - Ubuntu 14.10&lt;br /&gt;
|Phantomjs 2.0.0&lt;br /&gt;
|2.47.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|Windows 7/10&lt;br /&gt;
|Firefox 41.0&lt;br /&gt;
|2.47.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|Windows 7/10&lt;br /&gt;
|Chrome 47.0&lt;br /&gt;
|2.47.1&lt;br /&gt;
| 2.20&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|MacOS X&lt;br /&gt;
|Firefox 41.0&lt;br /&gt;
|2.47.1&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|MacOS X&lt;br /&gt;
|Chrome 46.0&lt;br /&gt;
|2.47.1&lt;br /&gt;
| 2.20&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|MacOS X&lt;br /&gt;
|Chrome 48.0&lt;br /&gt;
|2.51.0&lt;br /&gt;
| 2.21&lt;br /&gt;
| N/A&lt;br /&gt;
|-&lt;br /&gt;
|MacOS X&lt;br /&gt;
|PhantomJS - 2.0.0 &amp;amp; 2.1.1&lt;br /&gt;
|2.48.2&lt;br /&gt;
| N/A&lt;br /&gt;
| N/A&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
[[Category:Behat]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55586</id>
		<title>Moodle App Plugins Development Guide</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Moodle_App_Plugins_Development_Guide&amp;diff=55586"/>
		<updated>2019-02-14T15:33:43Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Testing and debugging */ Added a note to remind everyone to turn on display debug messages&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
{{Moodle Mobile 3.5}}&lt;br /&gt;
&lt;br /&gt;
==Before 3.5==&lt;br /&gt;
&lt;br /&gt;
Since Moodle 3.1 Moodle plugins could be supported in the Mobile app, but only by writing an Angular JS/Ionic module, compiling it to a zip, and including that in your plugin. See [[Moodle Mobile Remote add-ons|Remote add-ons]] for details.&lt;br /&gt;
&lt;br /&gt;
In Moodle 3.5 the app switched to a new way to suport plugins that was much easier for developers.&lt;br /&gt;
* This new way will allow developers to support plugins using PHP code, templates and Ionic markup (html components).&lt;br /&gt;
* The use of JavaScript is optional (but some type of advanced plugins may require it)&lt;br /&gt;
* Developers won’t need to set up a Mobile development environment, they will be able to test using the latest version of the official app (although setting up a local Mobile environment is recommended for complex plugins).&lt;br /&gt;
&lt;br /&gt;
This means that remote add-ons won’t be necessary anymore, and developers won’t have to learn Ionic 3 / Angular and set up a new mobile development environment to migrate them. Plugins using the old Remote add-ons mechanism will have to be migrated to the new simpler way (following this documentation)&lt;br /&gt;
&lt;br /&gt;
This new way is natively supported in Moodle 3.5. For previous versions you will need to install the Moodle Mobile Additional Features plugin.&lt;br /&gt;
&lt;br /&gt;
==How it works==&lt;br /&gt;
&lt;br /&gt;
The overall idea is to allow Moodle plugins to extend different areas in the app with &#039;&#039;just PHP server side&#039;&#039; code and Ionic 3 markup (custom html elements that are called components) using a set of custom Ionic directives and components.&lt;br /&gt;
&lt;br /&gt;
Developers will have to:&lt;br /&gt;
# Create a db/mobile.php file in their plugins. In this file developers will be able to indicate which areas of the app they want to extend, for example, adding a new option in the main menu, implementing an activity module not supported, including a new option in the course menu, including a new option in the user profile, etc. All the areas supported are described further in this document.&lt;br /&gt;
# Create new functions in a reserved namespace that will return the content of the new options. The content should be returned rendered (html). The template should use [https://ionicframework.com/docs/components/ Ionic components] so that it looks native (custom html elements) but it can be generated using mustache templates. &lt;br /&gt;
&lt;br /&gt;
Let’s clarify some points:&lt;br /&gt;
&lt;br /&gt;
* You don’t need to create new Web Service functions (although you will be able to use them for advanced features). You just need plain php functions that will be placed in a reserved namespace.&lt;br /&gt;
* Those functions will be exported via the Web Service function tool_mobile_get_content&lt;br /&gt;
* As arguments of your functions you will always receive the userid, some relevant details of the app (app version, current language in the app, etc…) and some specific data depending on the type of plugin (courseid, cmid, …).&lt;br /&gt;
* We provide a list of custom Ionic components and directives (html tags) that will provide dynamic behaviour, like indicating that you are linking a file that can be downloaded, or to allow a transition to new pages into the app calling a specific function in the server, submit form data to the server  etc..&lt;br /&gt;
&lt;br /&gt;
==Types of plugins==&lt;br /&gt;
&lt;br /&gt;
We could classify all the plugins in 3 different types:&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_when_requested.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template of your plugin will be generated and downloaded when the user opens your plugin in the app. This means that your function will receive some context params. For example, if you&#039;re developing a course module plugin you will receive the courseid and the cmid (course module ID). You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
[[File:Templates_downloaded_on_login.png|thumb]]&lt;br /&gt;
&lt;br /&gt;
With this type of plugin, the template for your plugin will be downloaded when the user logins in the app and will be stored in the device. This means that your function will not receive any context params, and you need to return a generic template that will be built with JS data like the ones in the Mobile app. When the user opens a page that includes your plugin, your template will receive the required JS data and your template will be rendered. You can see the list of delegates that support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
You can always implement your whole plugin yourself using Javascript instead of using our API. In fact, this is required if you want to implement some features like capturing links in the Mobile app. You can see the list of delegates that only support this type of plugin in the [[Mobile_support_for_plugins#Delegates|Delegates]] section.&lt;br /&gt;
&lt;br /&gt;
==Step by step example==&lt;br /&gt;
&lt;br /&gt;
In this example, we are going to update an existing plugin ([https://github.com/markn86/moodle-mod_certificate Certificate activity module]) that currently uses a Remote add-on.&lt;br /&gt;
This is a simple activity module that displays the certificate issued for the current user along with the list of the dates of previously issued certificates. It also stores in the course log that the user viewed a certificate. This module also works offline: when the user downloads the course or activity, the data is pre-fetched and can be viewed offline.&lt;br /&gt;
&lt;br /&gt;
The example code can be downloaded from here (https://github.com/markn86/moodle-mod_certificate/commit/003fbac0d80fd96baf428255500980bf95a7a0d6)&lt;br /&gt;
&lt;br /&gt;
TIP: Make sure to ([https://docs.moodle.org/35/en/Developer_tools#Purge_all_caches purge all cache]) after making an edit to one of the following files for your changes to be taken into account.&lt;br /&gt;
&lt;br /&gt;
===Step 1. Update the db/mobile.php file===&lt;br /&gt;
In this case, we are updating an existing file but for new plugins, you should create this new file.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$addons = [&lt;br /&gt;
    &#039;mod_certificate&#039; =&amp;gt; [ // Plugin identifier&lt;br /&gt;
        &#039;handlers&#039; =&amp;gt; [ // Different places where the plugin will display content.&lt;br /&gt;
            &#039;coursecertificate&#039; =&amp;gt; [ // Handler unique name (alphanumeric).&lt;br /&gt;
                &#039;displaydata&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;icon&#039; =&amp;gt; $CFG-&amp;gt;wwwroot . &#039;/mod/certificate/pix/icon.gif&#039;,&lt;br /&gt;
                    &#039;class&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
                ],&lt;br /&gt;
       &lt;br /&gt;
                &#039;delegate&#039; =&amp;gt; &#039;CoreCourseModuleDelegate&#039;, // Delegate (where to display the link to the plugin)&lt;br /&gt;
                &#039;method&#039; =&amp;gt; &#039;mobile_course_view&#039;, // Main function in \mod_certificate\output\mobile&lt;br /&gt;
                &#039;offlinefunctions&#039; =&amp;gt; [&lt;br /&gt;
                    &#039;mobile_course_view&#039; =&amp;gt; [],&lt;br /&gt;
                    &#039;mobile_issues_view&#039; =&amp;gt; [],&lt;br /&gt;
                ]. // Function that needs to be downloaded for offline.&lt;br /&gt;
            ],&lt;br /&gt;
        ],&lt;br /&gt;
        &#039;lang&#039; =&amp;gt; [ // Language strings that are used in all the handlers.&lt;br /&gt;
            [&#039;pluginname&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;summaryofattempts&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;getcertificate&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;requiredtimenotmet&#039;, &#039;certificate&#039;],&lt;br /&gt;
            [&#039;viewcertificateviews&#039;, &#039;certificate&#039;],&lt;br /&gt;
        ],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;Plugin identifier:&lt;br /&gt;
: A unique name for the plugin, it can be anything (there’s no need to match the module name).&lt;br /&gt;
 &lt;br /&gt;
;Handlers  (Different places where the plugin will display content):&lt;br /&gt;
: A plugin can be displayed in different views in the app. Each view should have a unique name inside the plugin scope (alphanumeric).&lt;br /&gt;
&lt;br /&gt;
; Display data:&lt;br /&gt;
: This is only needed for certain types of plugins. Also, depending on the type of delegate it may require additional (or less fields), in this case we are indicating the module icon.&lt;br /&gt;
	&lt;br /&gt;
; Delegate&lt;br /&gt;
: Where to display the link to the plugin, see the Delegates chapter in this documentation for all the possible options.&lt;br /&gt;
&lt;br /&gt;
; Method:&lt;br /&gt;
: This is the method in the Moodle \(component)\output\mobile class to be executed the first time the user clicks in the new option displayed in the app.	&lt;br /&gt;
&lt;br /&gt;
; Offlinefunctions&lt;br /&gt;
: These are the functions that need to be downloaded for offline usage. This is the list of functions that need to be called and stored when the user downloads a course for offline usage. Please note that you can add functions here that are not even listed in the mobile.php file. &lt;br /&gt;
: In our example, downloading for offline access will mean that we&#039;ll execute the functions for getting the certificate and issued certificates passing as parameters the current userid (and courseid when we are using the mod or course delegate). If we have the result of those functions stored in the app, we&#039;ll be able to display the certificate information even if the user is offline.&lt;br /&gt;
: Offline functions will be mostly used to display information for final users, any further interaction with the view won’t be supported offline (for example, trying to send information when the user is offline).&lt;br /&gt;
: You can indicate here other Web Services functions, indicating the parameters that they might need from a defined subset (currently userid and courseid)&lt;br /&gt;
: Prefetching the module will also download all the files returned by the methods in these offline functions (in the &#039;&#039;files&#039;&#039; array).&lt;br /&gt;
: Note: If your functions use additional custom parameters (for example, if you implement multiple pages within a module&#039;s view function by using a &#039;page&#039; parameter in addition to the usual cmid, courseid, userid) then the app will not know which additional parameters to supply. In this case, do not list the function in offlinefunctions; instead, you will need to manually implement a [[#Module_prefetch_handler|module prefetch handler]].&lt;br /&gt;
&lt;br /&gt;
;Lang:&lt;br /&gt;
: The language pack string ids used in the plugin by all the handlers. Please note that you should avoid adding all the plugin string ids (including those unused) because the Web Service that returns the plugin information will include the translation of each string id for every language installed in the platform.&lt;br /&gt;
&lt;br /&gt;
There are additional attributes supported by the mobile.php list, see “Mobile.php supported options” section below.&lt;br /&gt;
&lt;br /&gt;
===Step 2. Creating the main function===&lt;br /&gt;
&lt;br /&gt;
The main function displays the current issued certificate (or several warnings if it’s not possible to issue a certificate). It also displays a link to view the dates of previously issued certificates.&lt;br /&gt;
&lt;br /&gt;
All the functions must be created in the plugin or subsystem classes/output directory, the name of the class must be mobile.&lt;br /&gt;
&lt;br /&gt;
For this example (mod_certificate plugin) the namespace name will be mod_certificate\output.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
namespace mod_certificate\output;&lt;br /&gt;
&lt;br /&gt;
defined(&#039;MOODLE_INTERNAL&#039;) || die();&lt;br /&gt;
&lt;br /&gt;
use context_module;&lt;br /&gt;
use mod_certificate_external;&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Mobile output class for certificate&lt;br /&gt;
 *&lt;br /&gt;
 * @package    mod_certificate&lt;br /&gt;
 * @copyright  2018 Juan Leyva&lt;br /&gt;
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later&lt;br /&gt;
 */&lt;br /&gt;
class mobile {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate course view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_course_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Set timemodified for each certificate.&lt;br /&gt;
        foreach ($issues as $issue) {&lt;br /&gt;
            if (empty($issue-&amp;gt;timemodified)) {&lt;br /&gt;
                    $issue-&amp;gt;timemodified = $issue-&amp;gt;timecreated;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $showget = true;&lt;br /&gt;
        if ($certificate-&amp;gt;requiredtime &amp;amp;&amp;amp; !has_capability(&#039;mod/certificate:manage&#039;, $context)) {&lt;br /&gt;
            if (certificate_get_course_time($certificate-&amp;gt;course) &amp;lt; ($certificate-&amp;gt;requiredtime * 60)) {&lt;br /&gt;
                    $showget = false;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $certificate-&amp;gt;name = format_string($certificate-&amp;gt;name);&lt;br /&gt;
        list($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat) =&lt;br /&gt;
                        external_format_text($certificate-&amp;gt;intro, $certificate-&amp;gt;introformat, $context-&amp;gt;id,&#039;mod_certificate&#039;, &#039;intro&#039;);&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;certificate&#039; =&amp;gt; $certificate,&lt;br /&gt;
            &#039;showget&#039; =&amp;gt; $showget &amp;amp;&amp;amp; count($issues) &amp;gt; 0,&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues,&lt;br /&gt;
            &#039;issue&#039; =&amp;gt; $issues[0],&lt;br /&gt;
            &#039;numissues&#039; =&amp;gt; count($issues),&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;files&#039; =&amp;gt; $issues,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Let’s go through the function code to analyse the different parts.&lt;br /&gt;
&lt;br /&gt;
;Function declaration: &lt;br /&gt;
: The function name is the same as the one used in the mobile.php file (method field). There is only one argument “$args” which is an array containing all the information sent by the mobile app (the courseid, userid, appid, appversionname, appversioncode, applang, appcustomurlscheme…)&lt;br /&gt;
&lt;br /&gt;
; Function implementation:&lt;br /&gt;
: In the first part of the function, we check permissions and capabilities (like a view.php script would do normally). Then we retrieve the certificate information that’s necessary to display the template.&lt;br /&gt;
&lt;br /&gt;
Finally, we return:&lt;br /&gt;
* The rendered template (notice that we could return more than one template but we usually would only need one). By default the app will always render the first template received, the rest of the templates can be used if the plugin defines some Javascript code.&lt;br /&gt;
* JavaScript: Empty, because we don’t need any in this case&lt;br /&gt;
* Other data: Empty as well, because we don’t need any additional data to be used by directives or components in the template. This field will be published as an object supporting 2-way-data-bind to the template.&lt;br /&gt;
* Files: A list of files that the app should be able to download (for offline usage mostly)&lt;br /&gt;
&lt;br /&gt;
===Step 3. Creating the template for the main function===&lt;br /&gt;
&lt;br /&gt;
This is the most important part of your plugin because it contains the code that will be rendered on the mobile app.&lt;br /&gt;
&lt;br /&gt;
In this template we’ll be using Ionic and custom directives and components available in the Mobile app.&lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with ion- are ionic components. Most of the time the component name is self-explanatory but you may refer to a detailed guide here: https://ionicframework.com/docs/components/ &lt;br /&gt;
&lt;br /&gt;
All the HTML attributes starting with &#039;&#039;core-&#039;&#039; are custom components of the Mobile app.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_page.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;core-course-module-description description=&amp;quot;&amp;lt;% certificate.intro %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-course-module-description&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;ion-list-header&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-list-header&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
                    {{ &#039;plugin.mod_certificate.viewcertificateviews&#039; | translate: {$a: &amp;lt;% numissues %&amp;gt;} }}&lt;br /&gt;
                &amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%#showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;ion-icon name=&amp;quot;cloud-download&amp;quot; item-start&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
                {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
            &amp;lt;/button&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;%^showget%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-item&amp;gt;&lt;br /&gt;
            &amp;lt;p&amp;gt;{{ &#039;plugin.mod_certificate.requiredtimenotmet&#039; | translate }}&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/showget%&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;!-- Call log WS when the template is loaded. --&amp;gt;&lt;br /&gt;
        &amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Then we display the module description using &amp;lt;code&amp;gt;&amp;lt;core-course-module-description&amp;lt;/code&amp;gt; that is a component used to include the course module description.&lt;br /&gt;
&lt;br /&gt;
For displaying the certificate information we create a list of elements, adding a header on top.&lt;br /&gt;
The following line &amp;lt;code&amp;gt;{{ &#039;plugin.mod_certificate.summaryofattempts&#039; | translate }}&amp;lt;/code&amp;gt; indicates that the Mobile app will translate the &#039;&#039;summaryofattempts&#039;&#039; string id (here we could’ve used mustache translation but it is usually better to delegate the strings translations to the app). The string id has this format: &lt;br /&gt;
&lt;br /&gt;
“plugin” + plugin identifier (from mobile.php) +  string id (the string must be indicated in the lang field in mobile.php). &lt;br /&gt;
&lt;br /&gt;
Then we display a button to transition to another page if there are certificates issued. The attribute (directive) &amp;lt;code&amp;gt;core-site-plugins-new-content&amp;lt;/code&amp;gt; indicates that if the user clicks the button, we need to call the function “mobile_issues_view” in the component “mod_certificate” passing as arguments the cmid and courseid. The content returned by this function will be displayed in a new page (see Step 4 for the code of this new page).&lt;br /&gt;
&lt;br /&gt;
Just after this button we display another one but this time for downloading an issued certificate. The &amp;lt;code&amp;gt;core-course-download-module-main-file&amp;lt;/code&amp;gt; directive indicates that clicking this button is for downloading the whole activity and opening the main file. This means that, when the user clicks this button, the whole certificate activity will be available in offline.&lt;br /&gt;
&lt;br /&gt;
Finally, just before the ion-list is closed, we use the &amp;lt;code&amp;gt;core-site-plugins-call-ws-on-load&amp;lt;/code&amp;gt; directive to indicate that once the page is loaded, we need to call to a Web Service function in the server, in this case we are calling the &#039;&#039;mod_certificate_view_certificate&#039;&#039; that will log that the user viewed this page.&lt;br /&gt;
&lt;br /&gt;
As you can see, no JavaScript was necessary at all. We used plain HTML elements and attributes that did all the complex dynamic logic (like calling a Web Service) behind the scenes.&lt;br /&gt;
&lt;br /&gt;
===Step 4. Adding an additional page===&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Partial file contents: mod/certificate/classes/output/mobile.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Returns the certificate issues view for the mobile app.&lt;br /&gt;
     * @param  array $args Arguments from tool_mobile_get_content WS&lt;br /&gt;
     *&lt;br /&gt;
     * @return array       HTML, javascript and otherdata&lt;br /&gt;
     */&lt;br /&gt;
    public static function mobile_issues_view($args) {&lt;br /&gt;
        global $OUTPUT, $USER, $DB;&lt;br /&gt;
&lt;br /&gt;
        $args = (object) $args;&lt;br /&gt;
        $cm = get_coursemodule_from_id(&#039;certificate&#039;, $args-&amp;gt;cmid);&lt;br /&gt;
&lt;br /&gt;
        // Capabilities check.&lt;br /&gt;
        require_login($args-&amp;gt;courseid , false , $cm, true, true);&lt;br /&gt;
&lt;br /&gt;
        $context = context_module::instance($cm-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
        require_capability (&#039;mod/certificate:view&#039;, $context);&lt;br /&gt;
        if ($args-&amp;gt;userid != $USER-&amp;gt;id) {&lt;br /&gt;
            require_capability(&#039;mod/certificate:manage&#039;, $context);&lt;br /&gt;
        }&lt;br /&gt;
        $certificate = $DB-&amp;gt;get_record(&#039;certificate&#039;, array(&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance));&lt;br /&gt;
&lt;br /&gt;
        // Get certificates from external (taking care of exceptions).&lt;br /&gt;
        try {&lt;br /&gt;
            $issued = mod_certificate_external::issue_certificate($cm-&amp;gt;instance);&lt;br /&gt;
            $certificates = mod_certificate_external::get_issued_certificates($cm-&amp;gt;instance);&lt;br /&gt;
            $issues = array_values($certificates[&#039;issues&#039;]); // Make it mustache compatible.&lt;br /&gt;
        } catch (Exception $e) {&lt;br /&gt;
            $issues = array();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = [&lt;br /&gt;
            &#039;issues&#039; =&amp;gt; $issues&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        return [&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; [&lt;br /&gt;
                [&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_issues&#039;, $data),&lt;br /&gt;
                ],&lt;br /&gt;
            ],&lt;br /&gt;
            &#039;javascript&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
        ];&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This function for the new page was added just after the mobile_course_view function, the code is quite similar: Capabilities checks, retrieves the information required for the template and returns the template rendered.&lt;br /&gt;
&lt;br /&gt;
The code of the mustache template is also very simple:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/templates/mobile_view_issues.mustache&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code xml&amp;gt;&lt;br /&gt;
{{=&amp;lt;% %&amp;gt;=}}&lt;br /&gt;
&amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;ion-list&amp;gt;&lt;br /&gt;
        &amp;lt;%#issues%&amp;gt;&lt;br /&gt;
            &amp;lt;ion-item&amp;gt;&lt;br /&gt;
                &amp;lt;p class=&amp;quot;item-heading&amp;quot;&amp;gt;{{ &amp;lt;%timecreated%&amp;gt; | coreToLocaleString }}&amp;lt;/p&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;&amp;lt;%grade%&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;%/issues%&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As we did in the previous template, in the first line of the template we switch delimiters to avoid conflicting with Ionic delimiters (that are curly brackets like mustache). &lt;br /&gt;
&lt;br /&gt;
Here we are creating an ionic list that will display a new item in the list per each issued certificated.&lt;br /&gt;
&lt;br /&gt;
For the issued certificated we’ll display the time when it was created (using the app filter &#039;&#039;coreToLocaleString&#039;&#039;). We are also displaying the grade displayed in the certificate (if any).&lt;br /&gt;
&lt;br /&gt;
===Step 5. Plugin webservices, if included===&lt;br /&gt;
&lt;br /&gt;
If your plugin uses its own web services, they will also need to be enabled for mobile access in your db/services.php file.&lt;br /&gt;
&lt;br /&gt;
The following line &amp;lt;code&amp;gt;&#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&amp;lt;/code&amp;gt; should be included in each webservice definition.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;File contents: mod/certificate/db/services.php&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$functions = [&lt;br /&gt;
&lt;br /&gt;
    &#039;mod_certificate_get_certificates_by_courses&#039; =&amp;gt; [&lt;br /&gt;
        &#039;classname&#039;     =&amp;gt; &#039;mod_certificate_external&#039;,&lt;br /&gt;
        &#039;methodname&#039;    =&amp;gt; &#039;get_certificates_by_courses&#039;,&lt;br /&gt;
        &#039;description&#039;   =&amp;gt; &#039;Returns a list of certificate instances...&#039;,&lt;br /&gt;
        &#039;type&#039;          =&amp;gt; &#039;read&#039;,&lt;br /&gt;
        &#039;capabilities&#039;  =&amp;gt; &#039;mod/certificate:view&#039;,&lt;br /&gt;
        &#039;services&#039;      =&amp;gt; [MOODLE_OFFICIAL_MOBILE_SERVICE, &#039;local_mobile&#039;],&lt;br /&gt;
    ],&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
	&lt;br /&gt;
This extra services definition is the reason why you will need to have the local_mobile plugin installed for Moodle versions 3.4 and lower, so that your Moodle site will have all the additional webservices included to deal with all these mobile access calls. This is explained further in the [https://docs.moodle.org/dev/Mobile_support_for_plugins#Moodle_version_requirements Moodle version requirements section] below.&lt;br /&gt;
&lt;br /&gt;
==Getting started==&lt;br /&gt;
&lt;br /&gt;
The first and most important thing to know is that you don’t need a local mobile environment, you can just use the Chrome or Chromium browser to add mobile support to your plugins!&lt;br /&gt;
&lt;br /&gt;
Open this URL (with Chrome or Chromium browser): https://mobileapp.moodledemo.net/ and you will see a web version of the mobile app completely functional (except for some native features). This URL is updated with the latest integration version of the app.&lt;br /&gt;
&lt;br /&gt;
Please test that your site works correctly in the web version before starting any development.&lt;br /&gt;
&lt;br /&gt;
===Moodle version requirements===&lt;br /&gt;
&lt;br /&gt;
If your Moodle version is lower than 3.5 you will need to install the [https://docs.moodle.org/en/Moodle_Mobile_additional_features Moodle Mobile additional features plugin]. &lt;br /&gt;
&lt;br /&gt;
Please use this development version for now: https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE (if your Moodle version is 3.2, 3.3 or 3.4) you will have to use the specific branch for your version but applying manually the [https://github.com/moodlehq/moodle-local_mobile/commits/MOODLE_31_STABLE last commit from the 3.1 branch] (the one with number MOBILE-2362).&lt;br /&gt;
&lt;br /&gt;
Also, when installing the Moodle Mobile Additional features plugin you must follow the installation instructions so the service is set up properly.&lt;br /&gt;
&lt;br /&gt;
Remember to update your plugin documentation to reflect that this plugin is mandatory for Mobile support. We don’t recommend to indicate in your plugin version.php a dependency to local_mobile though.&lt;br /&gt;
&lt;br /&gt;
===Development workflow===&lt;br /&gt;
&lt;br /&gt;
First of all, we recommend creating a simple &#039;&#039;mobile.php&#039;&#039; for displaying a new main menu option (even if your plugin won’t be in the main menu, just to verify that you are able to extend the app plugins). Then open the webapp (https://mobileapp.moodledemo.net/) or refresh the browser if it was already open. Check that you can correctly  see the new menu option you included.&lt;br /&gt;
&lt;br /&gt;
Then, develop the main function of the app returning a “Hello world” or basic code (without using templates) to see that everything works together. After adding the classes/output/mobile.php file it is very important to “Purge all caches” to avoid problems with the auto-loading cache.&lt;br /&gt;
&lt;br /&gt;
It is important to remember that:&lt;br /&gt;
* Any change in the mobile.php file will require you to refresh the web app page in the browser (remember to disable the cache in the Chrome developer options).&lt;br /&gt;
* Any change in an existing template or function won’t require to refresh the browser page. In most cases you should just do a PTR (Pull down To Refresh) in the page that displays the view returned by the function. Be aware that PTR will work only when using the “device” emulation in the browser (see following section).&lt;br /&gt;
&lt;br /&gt;
===Testing and debugging===&lt;br /&gt;
&lt;br /&gt;
To learn how to debug with the web version of the app, please read the following documents:&lt;br /&gt;
* [[Moodle Mobile debugging WS requests]] AND&lt;br /&gt;
* [[Moodle Mobile development using Chrome or Chromium]] (please, omit the installation section)&lt;br /&gt;
&lt;br /&gt;
For plugins using the Javascript API you may develop making use of the console.log function to add trace messages in your code that will be displayed in the browser console.&lt;br /&gt;
&lt;br /&gt;
Within the app, make sure to turn on the option: &#039;&#039;&#039;App settings&#039;&#039;&#039; / &#039;&#039;&#039;General&#039;&#039;&#039; / &#039;&#039;&#039;Display debug messages&#039;&#039;&#039;. This means popup errors from the app will show more information.&lt;br /&gt;
&lt;br /&gt;
==Mobile.php supported options==&lt;br /&gt;
&lt;br /&gt;
In the Step by Step section we learned about some of the existing options for handlers configuration. This is the full list of supported options:&lt;br /&gt;
&lt;br /&gt;
===Common options===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;delegate&#039;&#039;&#039; (mandatory): Name of the delegate to register the handler in.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (mandatory): The function to call to retrieve the main page content.&lt;br /&gt;
* &#039;&#039;&#039;init&#039;&#039;&#039; (optional): A function to call to retrieve the initialization JS and the &amp;quot;restrict&amp;quot; to apply to the whole handler. It can also return templates that can be used from the Javascript of the init method or the Javascript of the handler’s method.&lt;br /&gt;
* &#039;&#039;&#039;restricttocurrentuser&#039;&#039;&#039; (optional) Only used if the delegate has a isEnabledForUser function. If true, the handler will only be shown for current user. For more info about displaying the plugin only for certain users, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;restricttoenrolledcourses&#039;&#039;&#039; (optional): Only used if the delegate has a isEnabledForCourse function. If true or not defined, the handler will only be shown for courses the user is enrolled in. For more info about displaying the plugin only for certain courses, please see [[Mobile_support_for_plugins#Display_the_plugin_only_if_certain_conditions_are_met|Display the plugin only if certain conditions are met]].&lt;br /&gt;
* &#039;&#039;&#039;styles&#039;&#039;&#039; (optional): An array with two properties: &#039;&#039;url&#039;&#039; and &#039;&#039;version&#039;&#039;. The URL should point to a CSS file, either using an absolute URL or a relative URL. This file will be downloaded and applied by the app. It&#039;s recommended to include styles that will only affect your plugin templates. The version number is used to determine if the file needs to be downloaded again, you should change the version number everytime you change the CSS file.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseOptionsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreMainMenuDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. Main Menu plugins are always displayed in the &amp;quot;More&amp;quot; tab, they cannot be displayed as tabs in the bottom bar.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseModuleDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): icon, class.&lt;br /&gt;
* &#039;&#039;&#039;offlinefunctions&#039;&#039;&#039;: (optional) List of functions to call when prefetching the module. It can be a get_content method or a WS. You can filter the params received by the WS. By default, WS will receive these params: courseid, cmid, userid. Other valid values that will be added if they are present in the list of params: courseids (it will receive a list with the courses the user is enrolled in), component + &#039;id&#039; (e.g. certificateid).&lt;br /&gt;
* &#039;&#039;&#039;downloadbutton&#039;&#039;&#039;: (optional) Whether to display download button in the module. If not defined, the button will be shown if there is any offlinefunction.&lt;br /&gt;
* &#039;&#039;&#039;isresource&#039;&#039;&#039;: (optional) Whether the module is a resource or an activity. Only used if there is any offlinefunction. If your module relies on the &amp;quot;contents&amp;quot; field, then it should be true.&lt;br /&gt;
* &#039;&#039;&#039;updatesnames&#039;&#039;&#039;: (optional) Only used if there is any offlinefunction. A Regular Expression to check if there&#039;s any update in the module. It will be compared to the result of &#039;&#039;core_course_check_updates&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;displayopeninbrowser&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Open in browser&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayOpenInBrowser = false;&lt;br /&gt;
* &#039;&#039;&#039;displaydescription&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Description&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayDescription = false;&lt;br /&gt;
* &#039;&#039;&#039;displayrefresh&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the &amp;quot;Refresh&amp;quot; option in the top-right menu. This can be done in JavaScript too: this.displayRefresh = false;&lt;br /&gt;
* &#039;&#039;&#039;displayprefetch&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the download option in the top-right menu. This can be done in JavaScript too: this.displayPrefetch = false;&lt;br /&gt;
* &#039;&#039;&#039;displaysize&#039;&#039;&#039;: (optional) Supported from the 3.6 version of the app. Whether the module should display the downloaded size in the top-right menu. This can be done in JavaScript too: this.displaySize = false;&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreCourseFormatDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;canviewallsections&#039;&#039;&#039;: (optional) Whether the course format allows seeing all sections in a single page. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displayenabledownload&#039;&#039;&#039;: (optional) Whether the option to enable section/module download should be displayed. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;displaysectionselector&#039;&#039;&#039;: (optional) Whether the default section selector should be displayed. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
===Options only for CoreUserDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;type&#039;&#039;&#039;: The type of the addon. Values accepted: &#039;newpage&#039; (default) or  &#039;communication&#039;. &lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for CoreSettingsDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon, class.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
===Options only for AddonMessageOutputDelegate===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;displaydata&#039;&#039;&#039; (mandatory): title, icon.&lt;br /&gt;
* &#039;&#039;&#039;priority&#039;&#039;&#039; (optional): Priority of the handler. Higher priority is displayed first. &lt;br /&gt;
&lt;br /&gt;
==Delegates==&lt;br /&gt;
&lt;br /&gt;
The delegates can be classified by type of plugin. For more info about type of plugins, please see the See [[Mobile_support_for_plugins#Types_of_plugins|Types of plugins]] section.&lt;br /&gt;
&lt;br /&gt;
===Templates generated and downloaded when the user opens the plugins===&lt;br /&gt;
&lt;br /&gt;
====CoreMainMenuDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new items to the main menu (currently displayed at the bottom of the app). &lt;br /&gt;
&lt;br /&gt;
====CoreCourseOptionsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add new options in a course (Participants or Grades are examples of this type of delegate).&lt;br /&gt;
&lt;br /&gt;
====CoreCourseModuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting activity modules or resources.&lt;br /&gt;
&lt;br /&gt;
====CoreUserDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate when you want to add additional options in the user profile page in the app.&lt;br /&gt;
&lt;br /&gt;
====CoreCourseFormatDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting course formats.&lt;br /&gt;
&lt;br /&gt;
====CoreSettingsDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to add a new option in the settings page.&lt;br /&gt;
&lt;br /&gt;
====AddonMessageOutputDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a message output plugin.&lt;br /&gt;
&lt;br /&gt;
===Templates downloaded on login and rendered using JS data===&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question types.&lt;br /&gt;
https://docs.moodle.org/dev/Creating_mobile_question_types&lt;br /&gt;
&lt;br /&gt;
====CoreQuestionBehaviourDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting question behaviours.&lt;br /&gt;
&lt;br /&gt;
====CoreUserProfileFieldDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate for supporting user profile fields.&lt;br /&gt;
&lt;br /&gt;
====AddonModQuizAccessRuleDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a quiz access rule.&lt;br /&gt;
&lt;br /&gt;
====AddonModAssignSubmissionDelegate and AddonModAssignFeedbackDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use these delegates to support assign submission or feedback plugins.&lt;br /&gt;
&lt;br /&gt;
====AddonWorkshopAssessmentStrategyDelegate====&lt;br /&gt;
&lt;br /&gt;
You must use this delegate to support a workshop assessment strategy plugin.&lt;br /&gt;
&lt;br /&gt;
===Pure Javascript plugins===&lt;br /&gt;
&lt;br /&gt;
These delegates require JavaScript to be supported. See [[Mobile_support_for_plugins#Initialization|Initialization]] for more information.&lt;br /&gt;
&lt;br /&gt;
* CoreContentLinksDelegate&lt;br /&gt;
* CoreCourseModulePrefetchDelegate&lt;br /&gt;
* CoreFileUploaderDelegate&lt;br /&gt;
* CorePluginFileDelegate&lt;br /&gt;
&lt;br /&gt;
==Available components and directives==&lt;br /&gt;
&lt;br /&gt;
===Difference between component and directives===&lt;br /&gt;
&lt;br /&gt;
A component (represented as an HTML tag) is used to add custom elements to the app.&lt;br /&gt;
Example of components are: ion-list, ion-item, core-search-box&lt;br /&gt;
&lt;br /&gt;
A directive (represented as an HTML attribute) allows you to extend a piece of HTML with additional information or functionality.&lt;br /&gt;
Example of directives are: core-auto-focus, *ngIf, ng-repeat&lt;br /&gt;
&lt;br /&gt;
The Mobile app uses Angular, Ionic and custom components and directives, for a full reference of:&lt;br /&gt;
* Angular directives, please check: https://angular.io/api?type=directive&lt;br /&gt;
* Ionic components, please check: https://ionicframework.com/docs/&lt;br /&gt;
&lt;br /&gt;
===Custom core components and directives===&lt;br /&gt;
&lt;br /&gt;
These are some useful custom components and directives (only available in the mobile app). Please notice that this isn’t the full list of components and directives of the app, it’s just an extract of the most common ones.&lt;br /&gt;
&lt;br /&gt;
====core-format-text====&lt;br /&gt;
&lt;br /&gt;
This directive formats the text and adds some directives needed for the app to work as it should. For example, it treats all links and all the embedded media so they work fine in the app. If some content in your template includes links or embedded media, please use this directive.&lt;br /&gt;
&lt;br /&gt;
This directive automatically applies core-external-content and core-link to all the links and embedded media.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;text&#039;&#039;&#039; (string): The text to format.&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* &#039;&#039;&#039;adaptImg&#039;&#039;&#039; (boolean): Optional. Whether to adapt images to screen width. Defaults to true.&lt;br /&gt;
* &#039;&#039;&#039;clean&#039;&#039;&#039; (boolean): Optional. Whether all the HTML tags should be removed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;singleLine&#039;&#039;&#039; (boolean): Optional. Whether new lines should be removed (all text in single line). Only if clean=true. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;maxHeight&#039;&#039;&#039; (number): Optional. Max height in pixels to render the content box. It should be 50 at least to make sense. Using this parameter will force display: block to calculate height better. If you want to avoid this use class=&amp;quot;inline&amp;quot; at the same time to use display: inline-block.&lt;br /&gt;
* &#039;&#039;&#039;fullOnClick&#039;&#039;&#039; (boolean): Optional. Whether it should open a new page with the full contents on click. Only if maxHeight is set and the content has been collapsed. Defaults to false.&lt;br /&gt;
* &#039;&#039;&#039;fullTitle&#039;&#039;&#039; (string): Optional. Title to use in full view. Defaults to &amp;quot;Description&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-format-text text=&amp;quot;&amp;lt;% cm.description %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-format-text&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-link====&lt;br /&gt;
&lt;br /&gt;
Directive to handle a link. It performs several checks, like checking if the link needs to be opened in the app, and opens the link as it should (without overriding the app).&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;capture&#039;&#039;&#039; (boolean): Optional. Whether the link needs to be captured by the app (check if the link can be handled by the app instead of opening it in a browser).&lt;br /&gt;
* &#039;&#039;&#039;inApp&#039;&#039;&#039; (boolean): Optional. True to open in embedded browser, false to open in system browser.&lt;br /&gt;
* &#039;&#039;&#039;autoLogin&#039;&#039;&#039; (string): Optional. If the link should be open with auto-login. Accepts the following values:&lt;br /&gt;
** &amp;quot;yes&amp;quot; -&amp;gt; Always auto-login.&lt;br /&gt;
** &amp;quot;no&amp;quot; -&amp;gt; Never auto-login.&lt;br /&gt;
** &amp;quot;check&amp;quot; -&amp;gt; Auto-login only if it points to the current site. Default value.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a href=&amp;quot;&amp;lt;% cm.url %&amp;gt;&amp;quot; core-link&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-external-content====&lt;br /&gt;
&lt;br /&gt;
Directive to handle links to files and embedded files. This directive should be used in any link to a file or any embedded file that you want to have available when the app is offline. &lt;br /&gt;
&lt;br /&gt;
If a file is downloaded, its URL will be replaced by the local file URL.&lt;br /&gt;
&lt;br /&gt;
This directive is automatically applied to all the links and media inside core-format-text.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;siteId&#039;&#039;&#039; (string): Optional. Site ID to use. If not defined, current site.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to use when downloading embedded files.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;img src=&amp;quot;&amp;lt;% event.iconurl %&amp;gt;&amp;quot; core-external-content component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% event.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-user-link====&lt;br /&gt;
&lt;br /&gt;
Directive to go to user profile on click. When the user clicks the element where this directive is attached, the right user profile will be opened.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;userId&#039;&#039;&#039; (number): User id to open the profile.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): Optional. Course id to show the user info related to that course.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;a ion-item core-user-link userId=&amp;quot;&amp;lt;% userid %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-file====&lt;br /&gt;
&lt;br /&gt;
Component to handle a remote file. It shows the file name, icon (depending on mimetype) and a button to download/refresh it. The user can identify if the file is downloaded or not based on the button.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* file (object): The file. Must have a property &#039;filename&#039; and a &#039;fileurl&#039; or &#039;url&#039;&lt;br /&gt;
* component (string): Optional. Component the file belongs to.&lt;br /&gt;
* componentId (string|number): Optional. ID to use in conjunction with the component.&lt;br /&gt;
* canDelete (boolean): Optional. Whether file can be deleted.&lt;br /&gt;
* alwaysDownload (boolean): Optional. Whether it should always display the refresh button when the file is downloaded. Use it for files that you cannot determine if they&#039;re outdated or not.&lt;br /&gt;
* canDownload (boolean): Optional. Whether file can be downloaded. Defaults to true.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-file [file]=&amp;quot;{fileurl: &#039;&amp;lt;% issue.url %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.name %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, filesize: &#039;&amp;lt;% issue.size %&amp;gt;&#039;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;/core-file&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-download-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and open a file. When the item with this directive is clicked, the file will be downloaded (if needed) and opened.&lt;br /&gt;
&lt;br /&gt;
It is usually recommended to use the core-file component since it also displays the state of the file.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;core-download-file&#039;&#039;&#039; (object): The file to download.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component.&lt;br /&gt;
&lt;br /&gt;
Example usage: a button to download a file.&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button [core-download-file]=&amp;quot;{fileurl: &amp;lt;% issue.url %&amp;gt;, timemodified: &amp;lt;% issue.timemodified %&amp;gt;, filesize: &amp;lt;% issue.size %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; componentId=&amp;quot;&amp;lt;% cm.id %&amp;gt;&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.download | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-course-download-module-main-file====&lt;br /&gt;
&lt;br /&gt;
Directive to allow downloading and opening the main file of a module.&lt;br /&gt;
&lt;br /&gt;
When the item with this directive is clicked, the whole module will be downloaded (if needed) and its main file opened. This is meant for modules like mod_resource.&lt;br /&gt;
&lt;br /&gt;
This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;module&#039;&#039;&#039; (object): Optional. The module object. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;moduleId&#039;&#039;&#039; (number): Optional. The module ID. Required if module is not supplied.&lt;br /&gt;
* &#039;&#039;&#039;courseId&#039;&#039;&#039; (number): The course ID the module belongs to.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): Optional. Component to link the file to.&lt;br /&gt;
* &#039;&#039;&#039;componentId&#039;&#039;&#039; (string|number): Optional. Component ID to use in conjunction with the component. If not defined, moduleId.&lt;br /&gt;
* &#039;&#039;&#039;files&#039;&#039;&#039; (object[]): Optional. List of files of the module. If not provided, use module.contents.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block core-course-download-module-main-file moduleId=&amp;quot;&amp;lt;% cmid %&amp;gt;&amp;quot; courseId=&amp;quot;&amp;lt;% certificate.course %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; [files]=&amp;quot;[{fileurl: &#039;&amp;lt;% issue.fileurl %&amp;gt;&#039;, filename: &#039;&amp;lt;% issue.filename %&amp;gt;&#039;, timemodified: &#039;&amp;lt;% issue.timemodified %&amp;gt;&#039;, mimetype: &#039;&amp;lt;% issue.mimetype %&amp;gt;&#039;}]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getcertificate&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-navbar-buttons====&lt;br /&gt;
&lt;br /&gt;
Component to add buttons to the app&#039;s header without having to place them inside the header itself. Using this component in a site plugin will allow adding buttons to the header of the current page.&lt;br /&gt;
&lt;br /&gt;
If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that position. If no start/end is specified, then the buttons will be added to the first &amp;lt;ion-buttons&amp;gt; found in the header.&lt;br /&gt;
&lt;br /&gt;
You can use the [hidden] input to hide all the inner buttons if a certain condition is met.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons end&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button icon-only (click)=&amp;quot;action()&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;ion-icon name=&amp;quot;funnel&amp;quot;&amp;gt;&amp;lt;/ion-icon&amp;gt;&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can also use this to add options to the context menu. Example usage:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;core-navbar-buttons&amp;gt;&lt;br /&gt;
    &amp;lt;core-context-menu&amp;gt;&lt;br /&gt;
        &amp;lt;core-context-menu-item [priority]=&amp;quot;500&amp;quot; [content]=&amp;quot;&#039;Nice boat&#039;&amp;quot; (action)=&amp;quot;boatFunction()&amp;quot; [iconAction]=&amp;quot;&#039;boat&#039;&amp;quot;&amp;gt;&amp;lt;/core-context-menu-item&amp;gt;&lt;br /&gt;
    &amp;lt;/core-context-menu&amp;gt;&lt;br /&gt;
&amp;lt;/core-navbar-buttons&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that it is not currently possible to remove or modify options from the context menu without using a nasty hack.&lt;br /&gt;
&lt;br /&gt;
===Specific component and directives for plugins===&lt;br /&gt;
&lt;br /&gt;
These are component and directives created specifically for supporting Moodle plugins.&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to display a new content when clicked. This new content can be displayed in a new page or in the current page (only if the current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the new &#039;&#039;get_content&#039;&#039; WS call. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to go to a new content page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to load new content in current page using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-new-content component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.viewissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the provided data: display a message, go back or refresh current view.&lt;br /&gt;
&lt;br /&gt;
If you want to load a new content when the WS call is done, please see core-site-plugins-call-ws-new-content.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;successMessage&#039;&#039;&#039; (string): Message to show on success. If not supplied, no message. If supplied but empty, default message (“Success”).&lt;br /&gt;
* &#039;&#039;&#039;goBackOnSuccess&#039;&#039;&#039; (boolean): Whether to go back if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;refreshOnSuccess&#039;&#039;&#039; (boolean): Whether to refresh the current view if the WS call is successful.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server without using cache, displaying default messages and refreshing on success:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage successMessage refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to send some data to the server using cache without confirming, going back on success and using userid from otherdata:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; goBackOnSuccess=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;certificateViewed($event)&amp;quot;&amp;gt;&lt;br /&gt;
     {{ &#039;plugin.mod_certificate.senddata&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.certificateViewed = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-new-content====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS when the element is clicked and load a new content passing the WS result as args. This new content can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content).&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t need to load some new content when done, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Data that can be passed to the directive:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;confirmMessage&#039;&#039;&#039; (string): Message to confirm the action when the user clicks the element. If not supplied, no confirmation. If supplied but empty, default message (&amp;quot;Are you sure?&amp;quot;).&lt;br /&gt;
* &#039;&#039;&#039;showError&#039;&#039;&#039; (boolean): Whether to show an error message if the WS call fails. Defaults to true. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;component&#039;&#039;&#039; (string): The component of the new content.&lt;br /&gt;
* &#039;&#039;&#039;method&#039;&#039;&#039; (string): The method to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;args&#039;&#039;&#039; (object): The params to get the new content.&lt;br /&gt;
* &#039;&#039;&#039;title&#039;&#039;&#039; (string): The title to display with the new content. Only if samePage=false.&lt;br /&gt;
* &#039;&#039;&#039;samePage&#039;&#039;&#039; (boolean): Whether to display the content in same page or open a new one. Defaults to new page.&lt;br /&gt;
* &#039;&#039;&#039;useOtherData&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the args for the new &#039;&#039;get_content&#039;&#039; call. The format is the same as in &#039;&#039;useOtherDataForWS&#039;&#039;.&lt;br /&gt;
* &#039;&#039;&#039;jsData&#039;&#039;&#039; (any): JS variables to pass to the new page so they can be used in the template or JS. If true is supplied instead of an object, all initial variables from current page will be copied. This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;newContentPreSets&#039;&#039;&#039; (object): Extra options for the WS call of the new content: whether to use cache or not, etc. This field was added in v3.6.0.&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usages:&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server without using cache, showing default confirm and displaying a new page:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; confirmMessage title=&amp;quot;&amp;lt;% certificate.name %&amp;gt;&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A button to get some data from the server using cache, without confirm, displaying new content in same page and using &#039;&#039;userid&#039;&#039; from &#039;&#039;otherdata&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Same example as the previous one but implementing a custom JS code to run on success:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button core-site-plugins-call-ws-new-content name=&amp;quot;mod_certificate_get_issued_certificates&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; component=&amp;quot;mod_certificate&amp;quot; method=&amp;quot;mobile_issues_view&amp;quot; [args]=&amp;quot;{cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;}&amp;quot; samePage=&amp;quot;true&amp;quot; [useOtherData]=&amp;quot;[&#039;userid&#039;]&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.getissued&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====core-site-plugins-call-ws-on-load====&lt;br /&gt;
&lt;br /&gt;
Directive to call a WS as soon as the template is loaded. This directive is meant for actions to do in the background, like calling logging Web Services.&lt;br /&gt;
&lt;br /&gt;
If you want to call a WS when the user clicks on a certain element, please see core-site-plugins-call-ws.&lt;br /&gt;
&lt;br /&gt;
Note that this will cause an error to appear on each page load if the user is offline in v3.5.1 and older, the bug was fixed in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;name&#039;&#039;&#039; (string): The name of the WS to call.&lt;br /&gt;
* &#039;&#039;&#039;params&#039;&#039;&#039; (object): The params for the WS call.&lt;br /&gt;
* &#039;&#039;&#039;preSets&#039;&#039;&#039; (object): Extra options for the WS call: whether to use cache or not, etc.&lt;br /&gt;
* &#039;&#039;&#039;useOtherDataForWS&#039;&#039;&#039; (any): Whether to include &#039;&#039;otherdata&#039;&#039; (from the &#039;&#039;get_content&#039;&#039; WS call) in the params for the WS call. If not supplied, no other data will be added. If supplied but empty (null, false or empty string) all the &#039;&#039;otherdata&#039;&#039; will be added. If it’s an array, it will only copy the properties whose names are in the array.&lt;br /&gt;
* &#039;&#039;&#039;form&#039;&#039;&#039; (string): ID or name to identify a form in the template. The form will be obtained from &#039;&#039;document.forms&#039;&#039;. If supplied and form is found, the form data will be retrieved and sent to the WS. If your form contains an ion-radio, ion-checkbox or ion-select, please see [[Mobile_support_for_plugins#Values_of_ion-radio.2C_ion-checkbox_or_ion-select_aren.27t_sent_to_my_WS|Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS]].&lt;br /&gt;
* &#039;&#039;&#039;onSuccess&#039;&#039;&#039; (Function): A function to call when the WS call is successful (HTTP call successful and no exception returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onError&#039;&#039;&#039; (Function): A function to call when the WS call fails (HTTP call fails or an exception is returned). This field was added in v3.5.2.&lt;br /&gt;
* &#039;&#039;&#039;onDone&#039;&#039;&#039; (Function): A function to call when the WS call finishes (either success or fail). This field was added in v3.5.2.&lt;br /&gt;
&lt;br /&gt;
Example usage:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;span core-site-plugins-call-ws-on-load name=&amp;quot;mod_certificate_view_certificate&amp;quot; [params]=&amp;quot;{certificateid: &amp;lt;% certificate.id %&amp;gt;}&amp;quot; [preSets]=&amp;quot;{getFromCache: 0, saveToCache: 0}&amp;quot; (onSuccess)=&amp;quot;callDone($event)&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.callDone = function(result) {&lt;br /&gt;
    // Code to run when the WS call is successful.&lt;br /&gt;
};&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Advanced features==&lt;br /&gt;
&lt;br /&gt;
===Display the plugin only if certain conditions are met===&lt;br /&gt;
&lt;br /&gt;
You might want to display your plugin in the mobile app only if certain dynamic conditions are met, so the plugin would be displayed only for some users. This can be achieved using the &amp;quot;init&amp;quot; method (for more info, please see the [[Mobile_support_for_plugins#Initialization|Initialization]] section ahead).&lt;br /&gt;
&lt;br /&gt;
All the init methods are called as soon as your plugin is retrieved. If you don&#039;t want your plugin to be displayed for the current user, then you should return an exception in this init method. It&#039;s recommended to include a message explaining why the plugin isn&#039;t available for the current user, this exception will be logged in the Javascript console.&lt;br /&gt;
&lt;br /&gt;
On the other hand, you might want to display a plugin only for certain courses (&#039;&#039;CoreCourseOptionsDelegate&#039;&#039;) or only if the user is viewing certain users&#039; profiles (&#039;&#039;CoreUserDelegate&#039;&#039;). This can be achieved with the init method too.&lt;br /&gt;
&lt;br /&gt;
In the init method you can return a &amp;quot;restrict&amp;quot; property with two fields in it: &#039;&#039;courses&#039;&#039; and &#039;&#039;users&#039;&#039;. If you return a list of courses IDs in this restrict property, then your plugin will only be displayed when the user views any of those courses. In the same way, if you return a list of user IDs then your plugin will only be displayed when the user views any of those users&#039; profiles.&lt;br /&gt;
&lt;br /&gt;
===Using “otherdata”===&lt;br /&gt;
&lt;br /&gt;
The values returned by the functions in otherdata are added to a variable so they can be used both in Javascript and in templates. The otherdata returned by a init call is added to a variable named INIT_OTHERDATA, while the otherdata returned by a &#039;&#039;get_content&#039;&#039; WS call is added to a variable named CONTENT_OTHERDATA.&lt;br /&gt;
&lt;br /&gt;
The otherdata returned by a init call will be passed to the JS and template of all the get_content calls in that handler. The otherdata returned by a get_content call will only be passed to the JS and template returned by that get_content call.&lt;br /&gt;
&lt;br /&gt;
This means that, in your Javascript, you can access and use these data like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
this.CONTENT_OTHERDATA.myVar&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
And in the template you could use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
{{ CONTENT_OTHERDATA.myVar }}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&#039;&#039;myVar&#039;&#039; is the name we put to one of our variables, it can be the name you want. In the example above, this is the otherdata returned by the PHP method:&lt;br /&gt;
&lt;br /&gt;
array(&#039;myVar&#039; =&amp;gt; &#039;Initial value&#039;)&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
In our plugin we want to display an input text with a certain initial value. When the user clicks a button, we want the value in the input to be sent to a certain WebService. This can be done using otherdata.&lt;br /&gt;
&lt;br /&gt;
We will return the initial value of the input in the otherdata of our PHP method:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;myVar&#039; =&amp;gt; &#039;My initial value&#039;),&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then in the template we will use it like this:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-item text-wrap&amp;gt;&lt;br /&gt;
    &amp;lt;ion-label stacked&amp;gt;{{ &#039;plugin.mod_certificate.textlabel | translate }}&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;text&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.myVar&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;ion-item&amp;gt;&lt;br /&gt;
    &amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [useOtherDataForWS]=&amp;quot;[&#039;myVar&#039;]&amp;quot;&amp;gt;&lt;br /&gt;
        {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
    &amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we are creating an input text and we use &#039;&#039;[(ngModel)]&#039;&#039; to use the value in &#039;&#039;myVar&#039;&#039; as the initial value and to store the changes in the same &#039;&#039;myVar&#039;&#039; variable. This means that the initial value of the input will be “My initial value”, and if the user changes the value of the input these changes will be applied to the &#039;&#039;myVar&#039;&#039; variable. This is called 2-way data binding in Angular.&lt;br /&gt;
&lt;br /&gt;
Then we add a button to send this data to a WS, and for that we use the directive core-site-plugins-call-ws. We use the &#039;&#039;useOtherDataForWS&#039;&#039; attribute to specify which variable from &#039;&#039;otherdata&#039;&#039; we want to send to our WebService. So if the user enters “A new value” in the input and then clicks the button, it will call the WebService &#039;&#039;mod_certificate_my_webservice&#039;&#039; and will send as a param: myVar -&amp;gt; “A new value”.&lt;br /&gt;
&lt;br /&gt;
We can achieve the same result using the &#039;&#039;params&#039;&#039; attribute of the core-site-plugins-call-ws directive instead of using &#039;&#039;useOtherDataForWS&#039;&#039;:&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block color=&amp;quot;light&amp;quot; core-site-plugins-call-ws name=&amp;quot;mod_certificate_my_webservice&amp;quot; [params]=&amp;quot;{myVar: CONTENT_OTHERDATA.myVar}&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mod_certificate.send | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
The WebService call will be exactly the same with both buttons.&lt;br /&gt;
&lt;br /&gt;
Please notice that this example could be done without using otherdata too, using the “&#039;&#039;form&#039;&#039;” input of the &#039;&#039;core-site-plugins-call-ws directive&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
===Running JS code after a content template has loaded===&lt;br /&gt;
&lt;br /&gt;
When you return JavaScript code from a handler function using the &#039;javascript&#039; array key, this code is executed immediately after the web service call returns, which may be before the returned template has been rendered into the DOM. &lt;br /&gt;
&lt;br /&gt;
If your code needs to run after the DOM has been updated, you can use setTimeout to call it. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
return [&lt;br /&gt;
    &#039;template&#039; =&amp;gt; [ ... ],&lt;br /&gt;
    &#039;javascript&#039; =&amp;gt; &#039;setTimeout(function() { console.log(&amp;quot;DOM is available now&amp;quot;); });&#039;,&lt;br /&gt;
    &#039;otherdata&#039; =&amp;gt; &#039;&#039;,&lt;br /&gt;
    &#039;files&#039; =&amp;gt; []&lt;br /&gt;
];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Note: If you wanted to write a lot of code here, you might be better off putting it in a function defined in the response from an init template, so that it does not get loaded again with each page of content.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
===JS functions visible in the templates===&lt;br /&gt;
&lt;br /&gt;
The app provides some Javascript functions that can be used from the templates to update, refresh or view content. These are the functions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;openContent(title: string, args: any, component?: string, method?: string)&#039;&#039;&#039;: Open a new page to display some new content. You need to specify the &#039;&#039;title&#039;&#039; of the new page and the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
* &#039;&#039;&#039;refreshContent(showSpinner = true)&#039;&#039;&#039;: Refresh the current content. By default it will display a spinner while refreshing, if you don&#039;t want it to be displayed you should pass false as a parameter.&lt;br /&gt;
* &#039;&#039;&#039;updateContent(args: any, component?: string, method?: string)&#039;&#039;&#039;: Refresh the current content using different params. You need to specify the &#039;&#039;args&#039;&#039; to send to the method. If &#039;&#039;component&#039;&#039; and &#039;&#039;method&#039;&#039; aren&#039;t provided, it will use the same as in the current page.&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Group selector=====&lt;br /&gt;
&lt;br /&gt;
Imagine we have an activity that uses groups and we want to let the user select which group he wants to see. A possible solution would be to return all the groups in the same template (hidden), and then show the group user selects. However, we can make it more dynamic and return only the group the user is requesting.&lt;br /&gt;
&lt;br /&gt;
To do so, we&#039;ll use a drop down to select the group. When the user selects a group using this drop down we&#039;ll update the page content to display the new group.&lt;br /&gt;
&lt;br /&gt;
The main difficulty in this is to tell the view which group needs to be selected when the view is loaded. There are 2 ways to do it: using plain HTML or using Angular&#039;s &#039;&#039;ngModel&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
======Using plain HTML======&lt;br /&gt;
&lt;br /&gt;
We need to add a &amp;quot;&#039;&#039;selected&#039;&#039;&amp;quot; attribute to the option that needs to be selected. To do so, we need to pre-caclulate the selected option in the PHP code:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
        // Detect which group is selected.&lt;br /&gt;
        foreach ($groups as $gid=&amp;gt;$group) {&lt;br /&gt;
            $group-&amp;gt;selected = $gid === $groupid;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        $data = array(&lt;br /&gt;
            &#039;cmid&#039; =&amp;gt; $cm-&amp;gt;id,&lt;br /&gt;
            &#039;courseid&#039; =&amp;gt; $args-&amp;gt;courseid,&lt;br /&gt;
            &#039;groups&#039; =&amp;gt; $groups&lt;br /&gt;
        );&lt;br /&gt;
&lt;br /&gt;
        return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the code above, we&#039;re retrieving the groups the user can see and then we&#039;re adding a &amp;quot;selected&amp;quot; bool to each one to determine which one needs to be selected in the drop down. Finally, we pass the list of groups to the template.&lt;br /&gt;
&lt;br /&gt;
In the template, we display the drop down like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: $event})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot; &amp;lt;%#selected%&amp;gt;selected&amp;lt;%/selected%&amp;gt; &amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;ionChange&#039;&#039; function will be called everytime the user selects a different group with the drop down. We&#039;re using the function &#039;&#039;updateContent&#039;&#039; to update the current view using the new group. &#039;&#039;$event&#039;&#039; is an Angular variable that will have the selected value (in our case, the group ID that was just selected). This is enough to make the group selector work.&lt;br /&gt;
&lt;br /&gt;
======Using ngModel======&lt;br /&gt;
&lt;br /&gt;
ngModel is an Angular directive that allows storing the value of a certain input/select in a Javascript variable, and also the opposite way: tell the input/select which value to set. The main problem is that we cannot initialize a Javascript variable from the template (Angular doesn&#039;t have &#039;&#039;ng-init&#039;&#039; like in AngularJS), so we&#039;ll use &amp;quot;otherdata&amp;quot;.&lt;br /&gt;
&lt;br /&gt;
In the PHP function we&#039;ll return the group that needs to be selected in the &#039;&#039;otherdata&#039;&#039; array:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
        $groupid = empty($args-&amp;gt;group) ? 0 : $args-&amp;gt;group; // By default, group 0.&lt;br /&gt;
        $groups = groups_get_activity_allowed_groups($cm, $user-&amp;gt;id);&lt;br /&gt;
&lt;br /&gt;
         ...&lt;br /&gt;
&lt;br /&gt;
         return array(&lt;br /&gt;
            &#039;templates&#039; =&amp;gt; array(&lt;br /&gt;
                array(&lt;br /&gt;
                    &#039;id&#039; =&amp;gt; &#039;main&#039;,&lt;br /&gt;
                    &#039;html&#039; =&amp;gt; $OUTPUT-&amp;gt;render_from_template(&#039;mod_certificate/mobile_view_page&#039;, $data),&lt;br /&gt;
                ),&lt;br /&gt;
            ),&lt;br /&gt;
            &#039;otherdata&#039; =&amp;gt; array(&lt;br /&gt;
                &#039;group&#039; =&amp;gt; $groupid&lt;br /&gt;
            ),&lt;br /&gt;
        );&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above we don&#039;t need to iterate over the groups array like in the plain HTML example. However, now we&#039;re returning the groupid in the &amp;quot;otherdata&amp;quot; array. As it&#039;s explained in the [[Mobile_support_for_plugins#Using_.E2.80.9Cotherdata.E2.80.9D|Using &amp;quot;otherdata&amp;quot;]] section, this &amp;quot;otherdata&amp;quot; is visible in the templates inside a variable named &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. So in the template we&#039;ll use this variable like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;ion-select [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.group&amp;quot; (ionChange)=&amp;quot;updateContent({cmid: &amp;lt;% cmid %&amp;gt;, courseid: &amp;lt;% courseid %&amp;gt;, group: CONTENT_OTHERDATA.group})&amp;quot; interface=&amp;quot;popover&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;%#groups%&amp;gt;&lt;br /&gt;
        &amp;lt;ion-option value=&amp;quot;&amp;lt;% id %&amp;gt;&amp;quot;&amp;gt;&amp;lt;% name %&amp;gt;&amp;lt;/ion-option&amp;gt;&lt;br /&gt;
    &amp;lt;%/groups%&amp;gt;&lt;br /&gt;
&amp;lt;/ion-select&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Initialization===&lt;br /&gt;
&lt;br /&gt;
All handlers can specify a “&#039;&#039;init&#039;&#039;” method in the mobile.php file. This method is meant to return some JavaScript code that needs to be executed as soon as the plugin is retrieved.&lt;br /&gt;
&lt;br /&gt;
When the app retrieves all the handlers, the first thing it will do is call the &#039;&#039;tool_mobile_get_content&#039;&#039; WebService with the init method. This WS call will only receive the default args.&lt;br /&gt;
&lt;br /&gt;
The app will immediately execute the JavaScript code returned by this WS call. This JavaScript can be used to manually register your handlers in the delegates you want, without having to rely on the default handlers built based on the mobile.php data.&lt;br /&gt;
&lt;br /&gt;
The templates returned by this init method will be added to a INIT_TEMPLATES variable that will be passed to all the Javascript code of that handler. This means that the Javascript returned by the init method or the “main” method can access any of the templates HTML like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.INIT_TEMPLATES[‘main’];&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
In this case, “main” is the ID of the template we want to use.&lt;br /&gt;
&lt;br /&gt;
The same happens with the &#039;&#039;otherdata&#039;&#039; returned by this init method, it is added to a INIT_OTHERDATA variable.&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;restrict&#039;&#039; field returned by this init call will be used to determine if your handler is enabled or not. For example, if your handler is for the delegate &#039;&#039;CoreCourseOptionsDelegate&#039;&#039; and you return a list of courseids in restrict-&amp;gt;courses, then your handler will only be enabled in the courses you returned. This only applies to the “default” handlers, if you register your own handler using the Javascript code then you should check yourself if the handler is enabled.&lt;br /&gt;
&lt;br /&gt;
Finally, if you return an object in this init Javascript code, all the properties of that object will be passed to all the Javascript code of that handler so you can use them when the code is run. For example, if your init Javascript code does something like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    MyAddonClass: new MyAddonClass()&lt;br /&gt;
};&lt;br /&gt;
result:&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Then, for the rest of Javascript code of your handler (e.g. for the “main” method) you can use this variable like this:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
this.MyAddonClass&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Examples====&lt;br /&gt;
&lt;br /&gt;
=====Module link handler=====&lt;br /&gt;
&lt;br /&gt;
A link handler allows you to decide what to do when a link with a certain URL is clicked. This is useful, for example, to open your module when a link to the module is clicked. In this example we’ll create a link handler to detect links to a certificate module using a init JavaScript:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function AddonModCertificateModuleLinkHandler() {&lt;br /&gt;
    that.CoreContentLinksModuleIndexHandler.call(this, that.CoreCourseHelperProvider, &#039;mmaModCertificate&#039;, &#039;certificate&#039;);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateLinkHandler&amp;quot;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype = Object.create(this.CoreContentLinksModuleIndexHandler.prototype);&lt;br /&gt;
AddonModCertificateModuleLinkHandler.prototype.constructor = AddonModCertificateModuleLinkHandler;&lt;br /&gt;
&lt;br /&gt;
this.CoreContentLinksDelegate.registerHandler(new AddonModCertificateModuleLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Advanced link handler=====&lt;br /&gt;
Link handlers have some advanced features that allow you to change how links behave under different conditions.&lt;br /&gt;
======Patterns======&lt;br /&gt;
You can define a Regular Expression pattern to match certain links.  This will apply the handler only to links that match the pattern.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.pattern = RegExp(&#039;\/mod\/foo\/specialpage.php&#039;);&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Priority======&lt;br /&gt;
Multiple link handlers may apply to a given link.  You can define the order of precedence by setting the priority - the handler with the highest priority will be used.&lt;br /&gt;
All default handlers have a priority of 0, so 1 or higher will override the default.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    ....&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
    ....&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
======Multiple actions======&lt;br /&gt;
Once a link has been matched, the handler&#039;s getActions() method determines what the link should do.  This method has access to the URL and its parameters.&lt;br /&gt;
Different actions can be returned depending on different conditions.&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {&lt;br /&gt;
    return [{&lt;br /&gt;
        action: function(siteId, navCtrl) {&lt;br /&gt;
            // The actual behaviour of the link goes here.&lt;br /&gt;
        },&lt;br /&gt;
        sites: [...]&lt;br /&gt;
    }, {&lt;br /&gt;
        ...&lt;br /&gt;
    }];&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
Once handlers have been matched for a link, the actions will be fetched for all the matching handlers, in priorty order.  The first &amp;quot;valid&amp;quot; action will be used to open the link.&lt;br /&gt;
If your handler is matched with a link, but a condition assessed in the getActions() function means you want to revert to the next highest priorty handler, you can &amp;quot;invalidate&amp;quot;&lt;br /&gt;
your action by settings its sites propety to an empty array.&lt;br /&gt;
======Complex example======&lt;br /&gt;
This will match all URLs containing /mod/foo/, and force those with an id parameter that&#039;s not in the &amp;quot;supportedModFoos&amp;quot; array to open in the user&#039;s browser, rather than the app.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
var supportedModFoos = [...];&lt;br /&gt;
function AddonModFooLinkHandler() {&lt;br /&gt;
    this.pattern = new RegExp(&#039;\/mod\/foo\/&#039;);&lt;br /&gt;
    this.name = &amp;quot;AddonModFooLinkHandler&amp;quot;;&lt;br /&gt;
    this.priority = 1;&lt;br /&gt;
}&lt;br /&gt;
AddonModFooLinkHandler.prototype = Object.create(that.CoreContentLinksHandlerBase.prototype);&lt;br /&gt;
AddonModFooLinkHandler.prototype.constructor = AddonModFooLinkHandler;&lt;br /&gt;
AddonModFooLinkHandler.prototype.getActions = function(siteIds, url, params) {     &lt;br /&gt;
    var action = {&lt;br /&gt;
        action: function() {&lt;br /&gt;
            that.CoreUtilsProvider.openInBrowser(url);&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    if (supportedModFoos.indexOf(parseInt(params.id)) !== -1) {&lt;br /&gt;
        action.sites = [];&lt;br /&gt;
    }&lt;br /&gt;
    return [action];&lt;br /&gt;
};&lt;br /&gt;
that.CoreContentLinksDelegate.registerHandler(new AddonModFooLinkHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Module prefetch handler=====&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;CoreCourseModuleDelegate&#039;&#039; handler allows you to define a list of &#039;&#039;offlinefunctions&#039;&#039; to prefetch a module. However, you might want to create your own prefetch handler to determine what needs to be downloaded. For example, you might need to chain WS calls (pass the result of a WS call to the next one), and this cannot be done using &#039;&#039;offlinefunctions&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to create a prefetch handler using init JS:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
// Create a class that &amp;quot;inherits&amp;quot; from CoreCourseActivityPrefetchHandlerBase.&lt;br /&gt;
function AddonModCertificateModulePrefetchHandler() {&lt;br /&gt;
    that.CoreCourseActivityPrefetchHandlerBase.call(this, that.TranslateService, that.CoreAppProvider, that.CoreUtilsProvider,&lt;br /&gt;
            that.CoreCourseProvider, that.CoreFilepoolProvider, that.CoreSitesProvider, that.CoreDomUtilsProvider);&lt;br /&gt;
&lt;br /&gt;
    this.name = &amp;quot;AddonModCertificateModulePrefetchHandler&amp;quot;;&lt;br /&gt;
    this.modName = &amp;quot;certificate&amp;quot;;&lt;br /&gt;
    this.component = &amp;quot;mmaModCertificate&amp;quot;;&lt;br /&gt;
    this.updatesNames = /^configuration$|^.*files$/;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype = Object.create(this.CoreCourseActivityPrefetchHandlerBase.prototype);&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.constructor = AddonModCertificateModulePrefetchHandler;&lt;br /&gt;
&lt;br /&gt;
// Override the prefetch call.&lt;br /&gt;
AddonModCertificateModulePrefetchHandler.prototype.prefetch = function(module, courseId, single, dirPath) {&lt;br /&gt;
    return this.prefetchPackage(module, courseId, single, prefetchCertificate);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function prefetchCertificate(module, courseId, single, siteId) {&lt;br /&gt;
    // Perform all the WS calls.&lt;br /&gt;
    // You can access most of the app providers using that.ClassName. E.g. that.CoreWSProvider.call().&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseModulePrefetchDelegate.registerHandler(new AddonModCertificateModulePrefetchHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
One relatively simple full example is where you have a function that needs to work offline, but it has an additional argument other than the standard ones. You can imagine for this an activity like the book module, where it has multiple pages for the same cmid. The app will not automatically work with this situation - it will call the offline function with the standard arguments only, so you won&#039;t be able to prefetch all the possible parameters. &lt;br /&gt;
&lt;br /&gt;
To deal with this, you need to implement a web service in your Moodle component that returns the list of possible extra arguments, and then you can call this web service and loop around doing the same thing the app does when it prefetches the offline functions. Here is an example from a third-party module (showing only the actual prefetch function - the rest of the code is as above) where there are multiple values of a custom &#039;section&#039; parameter for the mobile function &#039;mobile_document_view&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
function prefetchOucontent(module, courseId, single, siteId) {&lt;br /&gt;
    var component = &#039;mod_oucontent&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Get the site, first.&lt;br /&gt;
    return that.CoreSitesProvider.getSite(siteId).then(function(site) {&lt;br /&gt;
        // Read the list of pages in this document using a web service.&lt;br /&gt;
        return site.read(&#039;mod_oucontent_get_page_list&#039;, {&#039;cmid&#039;: module.id}).then(function(response) {&lt;br /&gt;
            var promises = [];&lt;br /&gt;
&lt;br /&gt;
            // For each page, read and process the page - this is a copy of logic in the app at&lt;br /&gt;
            // siteplugins.ts (prefetchFunctions), but modified to add the custom argument.&lt;br /&gt;
            for(var i = 0; i &amp;lt; response.length; i++) {&lt;br /&gt;
                var args = {&lt;br /&gt;
                    courseid: courseId,&lt;br /&gt;
                    cmid: module.id,&lt;br /&gt;
                    userid: site.getUserId()&lt;br /&gt;
                };&lt;br /&gt;
                if (response[i] !== &#039;&#039;) {&lt;br /&gt;
                    args.section = response[i];&lt;br /&gt;
                }&lt;br /&gt;
&lt;br /&gt;
                promises.push(that.CoreSitePluginsProvider.getContent(&lt;br /&gt;
                        component, &#039;mobile_document_view&#039;, args).then(&lt;br /&gt;
                        function(result) {&lt;br /&gt;
                            var subPromises = [];&lt;br /&gt;
                            if (result.files &amp;amp;&amp;amp; result.files.length) {&lt;br /&gt;
                                subPromises.push(that.CoreFilepoolProvider.downloadOrPrefetchFiles(&lt;br /&gt;
                                        site.id, result.files, true, false, component, module.id));&lt;br /&gt;
                            }&lt;br /&gt;
                            return Promise.all(subPromises);&lt;br /&gt;
                        }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            return Promise.all(promises);&lt;br /&gt;
        });&lt;br /&gt;
    });&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Single activity course format=====&lt;br /&gt;
&lt;br /&gt;
In the following example, the value of INIT_TEMPLATES[&amp;quot;main&amp;quot;] is:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;core-dynamic-component [component]=&amp;quot;componentClass&amp;quot; [data]=&amp;quot;data&amp;quot;&amp;gt;&amp;lt;/core-dynamic-component&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This template is returned by the init method. And this is the JavaScript code returned by the init method:&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
function getAddonSingleActivityFormatComponent() {&lt;br /&gt;
    function AddonSingleActivityFormatComponent() {&lt;br /&gt;
        this.data = {};&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.constructor = AddonSingleActivityFormatComponent;&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.ngOnChanges = function(changes) {&lt;br /&gt;
        var self = this;&lt;br /&gt;
&lt;br /&gt;
        if (this.course &amp;amp;&amp;amp; this.sections &amp;amp;&amp;amp; this.sections.length) {&lt;br /&gt;
            var module = this.sections[0] &amp;amp;&amp;amp; this.sections[0].modules &amp;amp;&amp;amp; this.sections[0].modules[0];&lt;br /&gt;
            if (module &amp;amp;&amp;amp; !this.componentClass) {&lt;br /&gt;
                that.CoreCourseModuleDelegate.getMainComponent(that.Injector, this.course, module).then((component) =&amp;gt; {&lt;br /&gt;
                    self.componentClass = component || that.CoreCourseUnsupportedModuleComponent;&lt;br /&gt;
                });&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.data.courseId = this.course.id;&lt;br /&gt;
            this.data.module = module;&lt;br /&gt;
        }&lt;br /&gt;
    };&lt;br /&gt;
    AddonSingleActivityFormatComponent.prototype.doRefresh = function(refresher, done) {&lt;br /&gt;
        return Promise.resolve(this.dynamicComponent.callComponentFunction(&amp;quot;doRefresh&amp;quot;, [refresher, done]));&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
return AddonSingleActivityFormatComponent;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function AddonSingleActivityFormatHandler() {&lt;br /&gt;
    this.name = &amp;quot;singleactivity&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.constructor = AddonSingleActivityFormatHandler;&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.isEnabled = function() {&lt;br /&gt;
    return true;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.canViewAllSections = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseTitle = function(course, sections) {&lt;br /&gt;
    if (sections &amp;amp;&amp;amp; sections[0] &amp;amp;&amp;amp; sections[0].modules &amp;amp;&amp;amp; sections[0].modules[0]) {&lt;br /&gt;
        return sections[0].modules[0].name;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return course.fullname || &amp;quot;&amp;quot;;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displayEnableDownload = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.displaySectionSelector = function(course) {&lt;br /&gt;
    return false;&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
AddonSingleActivityFormatHandler.prototype.getCourseFormatComponent = function(injector, course) {&lt;br /&gt;
    that.Injector = injector || that.Injector;&lt;br /&gt;
&lt;br /&gt;
    return that.CoreCompileProvider.instantiateDynamicComponent(that.INIT_TEMPLATES[&amp;quot;main&amp;quot;], getAddonSingleActivityFormatComponent(), injector);&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
this.CoreCourseFormatDelegate.registerHandler(new AddonSingleActivityFormatHandler());&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Using the JavaScript API===&lt;br /&gt;
&lt;br /&gt;
The Javascript API is partly supported right now, only the &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; supports it now. This API allows you to override any of the functions of the default handler. &lt;br /&gt;
&lt;br /&gt;
The “method” specified in a handler registered in the &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; will be called immediately after the init method, and the Javascript returned by this method will be run. If this Javascript code returns an object with certain functions, these function will override the ones in the default handler.&lt;br /&gt;
&lt;br /&gt;
For example, if the Javascript returned by the method returns something like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var result = {&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The the &#039;&#039;getData&#039;&#039; function of the default handler will be overridden by the returned getData function.&lt;br /&gt;
&lt;br /&gt;
The default handler for &#039;&#039;CoreUserProfileFieldDelegate&#039;&#039; only has 2 functions: &#039;&#039;getComponent&#039;&#039; and &#039;&#039;getData&#039;&#039;. In addition, the JavaScript code can return an extra function named &#039;&#039;componentInit&#039;&#039; that will be executed when the component returned by &#039;&#039;getComponent&#039;&#039; is initialized.&lt;br /&gt;
&lt;br /&gt;
Here’s an example on how to support the text user profile field using this API:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
var that = this;&lt;br /&gt;
&lt;br /&gt;
var result = {&lt;br /&gt;
    componentInit: function() {&lt;br /&gt;
        if (this.field &amp;amp;&amp;amp; this.edit &amp;amp;&amp;amp; this.form) {&lt;br /&gt;
            this.field.modelName = &amp;quot;profile_field_&amp;quot; + this.field.shortname;&lt;br /&gt;
&lt;br /&gt;
            if (this.field.param2) {&lt;br /&gt;
                this.field.maxlength = parseInt(this.field.param2, 10) || &amp;quot;&amp;quot;;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            this.field.inputType = that.CoreUtilsProvider.isTrueOrOne(this.field.param3) ? &amp;quot;password&amp;quot; : &amp;quot;text&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
            var formData = {&lt;br /&gt;
                value: this.field.defaultdata,&lt;br /&gt;
                disabled: this.disabled&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            this.form.addControl(this.field.modelName, that.FormBuilder.control(formData, this.field.required &amp;amp;&amp;amp; !this.field.locked ? that.Validators.required : null));&lt;br /&gt;
        }&lt;br /&gt;
    },&lt;br /&gt;
    getData: function(field, signup, registerAuth, formValues) {&lt;br /&gt;
        var name = &amp;quot;profile_field_&amp;quot; + field.shortname;&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            type: &amp;quot;text&amp;quot;,&lt;br /&gt;
            name: name,&lt;br /&gt;
            value: that.CoreTextUtilsProvider.cleanTags(formValues[name])&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
result;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Troubleshooting==&lt;br /&gt;
&lt;br /&gt;
=== Invalid response received ===&lt;br /&gt;
&lt;br /&gt;
You might receive this error when using the &amp;quot;core-site-plugins-call-ws&amp;quot; directive or similar. By default, the app expects all WebService calls to return an object, if your WebService returns another type (string, bool, ...) then you need to specify it using the preSets attribute of the directive. For example, if your WS returns a boolean value, then you should specify it like this:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{typeExpected: &#039;boolean&#039;}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
In a similar way, if your WebService returns null you need to tell the app not to expect any result using the preSets:&lt;br /&gt;
&lt;br /&gt;
[preSets]=&amp;quot;{responseExpected: false}&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Values of ion-radio, ion-checkbox or ion-select aren&#039;t sent to my WS ===&lt;br /&gt;
&lt;br /&gt;
Some directives allow you to specify a form id or name to send the data from the form to a certain WS. These directives look for HTML inputs to retrieve the data to send. However, ion-radio, ion-checkbox and ion-select don&#039;t use HTML inputs, they simulate them, so the directive isn&#039;t going to find their data and so it won&#039;t be sent to the WebService.&lt;br /&gt;
&lt;br /&gt;
There are 2 workarounds to fix this problem. It seems that the next major release of Ionic framework does use HTML inputs, so these are temporary solutions.&lt;br /&gt;
&lt;br /&gt;
==== Sending the data manually ====&lt;br /&gt;
&lt;br /&gt;
The first solution is to send the missing params manually using the &amp;quot;&#039;&#039;params&#039;&#039;&amp;quot; property. We will use &#039;&#039;ngModel&#039;&#039; to store the input value in a variable, and this variable will be passed to the params. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a template like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group name=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;ion-list radio-group [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/ion-list&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;myws&amp;quot; [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, responses: responses}&amp;quot; form=&amp;quot;myform&amp;quot;&amp;gt;&lt;br /&gt;
    {{ &#039;plugin.mycomponent.save&#039; | translate }}&lt;br /&gt;
&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Basically, you need to add &#039;&#039;ngModel&#039;&#039; to the affected element (in this case, the &#039;&#039;radio-group&#039;&#039;). You can put whatever name you want as the value, we used &amp;quot;responses&amp;quot;. With this, everytime the user selects a radio button the value will be stored in a variable named &amp;quot;responses&amp;quot;. Then, in the button we are passing this variable to the params of the WebService.&lt;br /&gt;
&lt;br /&gt;
Please notice that the &amp;quot;form&amp;quot; attribute has priority over &amp;quot;params&amp;quot;, so if you have an input with name=&amp;quot;responses&amp;quot; it will override what you&#039;re manually passing to params.&lt;br /&gt;
&lt;br /&gt;
==== Using a hidden input ====&lt;br /&gt;
&lt;br /&gt;
Since the directive is looking for HTML inputs, you need to add one with the value to send to the server. You can use &#039;&#039;ngModel&#039;&#039; to synchronize your ion-radio/ion-checkbox/ion-select with the new hidden input. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too.&lt;br /&gt;
&lt;br /&gt;
For example, if you have a radio button like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Then you should modify it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;div radio-group name=&amp;quot;responses&amp;quot; [(ngModel)]=&amp;quot;responses&amp;quot;&amp;gt; &lt;br /&gt;
    &amp;lt;ion-item&amp;gt;&lt;br /&gt;
        &amp;lt;ion-label&amp;gt;First value&amp;lt;/ion-label&amp;gt;&lt;br /&gt;
        &amp;lt;ion-radio value=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/ion-radio&amp;gt;&lt;br /&gt;
    &amp;lt;/ion-item&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;ion-input type=&amp;quot;hidden&amp;quot; [ngModel]=&amp;quot;responses&amp;quot; name=&amp;quot;responses&amp;quot;&amp;gt;&amp;lt;/ion-input&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
In the example above, we&#039;re using a variable named &amp;quot;responses&amp;quot; to synchronize the data between the &#039;&#039;radio-group&#039;&#039; and the hidden input. You can use whatever name you want.&lt;br /&gt;
&lt;br /&gt;
=== I can&#039;t return an object or array in otherdata ===&lt;br /&gt;
&lt;br /&gt;
If you try to return an object or an array in any field inside &#039;&#039;otherdata&#039;&#039;, the WebService call will fail with the following error:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Scalar type expected, array or object received&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Each field in &#039;&#039;otherdata&#039;&#039; must be a string, number or boolean, it cannot be an object or array. To make it work, you need to encode your object or array into a JSON string:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($data))&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The app will automatically parse this JSON and convert it back into an array or object.&lt;br /&gt;
&lt;br /&gt;
==Examples==&lt;br /&gt;
&lt;br /&gt;
===Accepting dynamic names in a WebService===&lt;br /&gt;
&lt;br /&gt;
We want to display a form where the names of the fields are dynamic, like it happens in quiz. This data will be sent to a new WebService that we have created.&lt;br /&gt;
&lt;br /&gt;
The first issue we find is that the WebService needs to define the names of the parameters received, but in this case they&#039;re dynamic. The solution is to accept an array of objects with name and value. So in the &#039;&#039;_parameters()&#039;&#039; function of our new WebService, we will add this parameter:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;data&#039; =&amp;gt; new external_multiple_structure(&lt;br /&gt;
     new external_single_structure(&lt;br /&gt;
        array(&lt;br /&gt;
            &#039;name&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data name&#039;),&lt;br /&gt;
            &#039;value&#039; =&amp;gt; new external_value(PARAM_RAW, &#039;data value&#039;),&lt;br /&gt;
        )&lt;br /&gt;
    ),&lt;br /&gt;
    &#039;The data to be saved&#039;, VALUE_DEFAULT, array()&lt;br /&gt;
)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now we need to adapt our form to send the data as the WebService requires it. In our template, we have a button with the directive &#039;&#039;core-site-plugins-call-ws&#039;&#039; that will send the form data to our WebService. To make this work we will have to pass the parameters manually, without using the &amp;quot;&#039;&#039;form&#039;&#039;&amp;quot; attribute, because we need to format the data before it is sent.&lt;br /&gt;
&lt;br /&gt;
Since we will send the params manually and we want it all to be sent in the same array, we will use &#039;&#039;ngModel&#039;&#039; to store the input data into a variable that we&#039;ll call &amp;quot;data&amp;quot;, but you can use the name you want. This &amp;quot;data&amp;quot; will be an object that will hold the input data with the format &amp;quot;name-&amp;gt;value&amp;quot;. For example, if I have an input with name &amp;quot;a1&amp;quot; and value &amp;quot;My answer&amp;quot;, the data object will be:&lt;br /&gt;
&lt;br /&gt;
{a1: &amp;quot;My answer&amp;quot;}&lt;br /&gt;
&lt;br /&gt;
So we need to add &#039;&#039;ngModel&#039;&#039; to all the inputs whose values need to be sent to the &amp;quot;data&amp;quot; WS param. Please notice that &#039;&#039;ngModel&#039;&#039; &#039;&#039;&#039;requires&#039;&#039;&#039; the element to have a name, so if you add &#039;&#039;ngModel&#039;&#039; to a certain element you need to add a name too. For example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&amp;lt;ion-input name=&amp;quot;&amp;lt;% name %&amp;gt;&amp;quot; [(ngModel)]=&amp;quot;CONTENT_OTHERDATA.data[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see, we&#039;re using &#039;&#039;CONTENT_OTHERDATA&#039;&#039; to store the data. We do it like this because we&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the form, setting the values the user has already stored. If you don&#039;t need to initialize the form, then you can use the variable &amp;quot;dataObject&amp;quot;, an empty object that the Mobile app creates for you: [(ngModel)]=&amp;quot;dataObject[&#039;&amp;lt;% name %&amp;gt;&#039;]&amp;quot;&lt;br /&gt;
&lt;br /&gt;
The Mobile app has a function that allows you to convert this data object into an array like the one the WS expects: &#039;&#039;objectToArrayOfObjects&#039;&#039;. So in our button we&#039;ll use this function to format the data before it&#039;s sent:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code javascript&amp;gt;&lt;br /&gt;
&amp;lt;button ion-button block type=&amp;quot;submit&amp;quot; core-site-plugins-call-ws name=&amp;quot;my_ws_name&amp;quot;&lt;br /&gt;
    [params]=&amp;quot;{id: &amp;lt;% id %&amp;gt;, data: CoreUtilsProvider.objectToArrayOfObjects(CONTENT_OTHERDATA.data, &#039;name&#039;, &#039;value&#039;)}&amp;quot;&lt;br /&gt;
    successMessage&lt;br /&gt;
    refreshOnSuccess=&amp;quot;true&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As you can see in the example above, we&#039;re specifying that the keys of the &amp;quot;data&amp;quot; object need to be stored in a property named &amp;quot;name&amp;quot;, and the values need to be stored in a property named &amp;quot;value&amp;quot;. If your WebService expects different names you need to change the parameters of the function &#039;&#039;objectToArrayOfObjects&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
If you open your plugin now in the Mobile app it will display an error in the Javascript console. The reason is that the variable &amp;quot;data&amp;quot; doesn&#039;t exist inside &#039;&#039;CONTENT_OTHERDATA&#039;&#039;. As it is explained in previous sections, &#039;&#039;CONTENT_OTHERDATA&#039;&#039; holds the data that you return in &#039;&#039;otherdata&#039;&#039; for your method. We&#039;ll use &#039;&#039;otherdata&#039;&#039; to initialize the values to be displayed in the form.&lt;br /&gt;
&lt;br /&gt;
If the user hasn&#039;t answered the form yet, we can initialize the &amp;quot;data&amp;quot; object as an empty object. Please remember that we cannot return arrays or objects in &#039;&#039;otherdata&#039;&#039;, so we&#039;ll return a JSON string.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; &#039;{}&#039;)&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With the code above, the form will always be empty when the user opens it. But now we want to check if the user has already answered the form and fill the form with the previous values. We will do it like this:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$userdata = get_user_responses(); // It will held the data in a format name-&amp;gt;value. Example: array(&#039;a1&#039; =&amp;gt; &#039;My value&#039;).&lt;br /&gt;
...&lt;br /&gt;
&#039;otherdata&#039; =&amp;gt; array(&#039;data&#039; =&amp;gt; json_encode($userdata))&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now the user will be able to see previous values when the form is opened, and clicking the button will send the data to our WebService in array format.&lt;br /&gt;
&lt;br /&gt;
==Moodle plugins with mobile support==&lt;br /&gt;
&lt;br /&gt;
* Group choice: [https://moodle.org/plugins/mod_choicegroup Moodle plugins directory entry] and [https://github.com/ndunand/moodle-mod_choicegroup code in github].&lt;br /&gt;
* Custom certificate: [https://moodle.org/plugins/mod_customcert Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_customcert code in github].&lt;br /&gt;
* Gapfill question type: [https://moodle.org/plugins/qtype_gapfill Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_gapfill in github].&lt;br /&gt;
* Wordselect question type: [https://moodle.org/plugins/qtype_wordselect Moodle plugins directory entry] and [https://github.com/marcusgreen/moodle-qtype_wordselect in github].&lt;br /&gt;
* RegExp question type: [https://moodle.org/plugins/qtype_regexp Moodle plugins directory entry] and [https://github.com/rezeau/moodle-qtype_regexp in github].&lt;br /&gt;
* Certificate: [https://moodle.org/plugins/mod_certificate Moodle plugins directory entry] and [https://github.com/markn86/moodle-mod_certificate in github].&lt;br /&gt;
* Attendance [https://moodle.org/plugins/mod_attendance Moodle plugins directory entry] and [https://github.com/danmarsden/moodle-mod_attendance in github].&lt;br /&gt;
&lt;br /&gt;
See the complete list in the plugins database [https://moodle.org/plugins/browse.php?list=award&amp;amp;id=6 here] (it may contain some outdated plugins)&lt;br /&gt;
[[Category:Mobile]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55580</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55580"/>
		<updated>2019-02-13T10:44:05Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;From Moodle 3.7 it is possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.1&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.1&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. &lt;br /&gt;
&lt;br /&gt;
If you are using a local installation of the app, and want Behat to automatically run the app for you, then put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself before running any Behat tests, and also kill it yourself once you are finished with it. &lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you run it using Docker, it will automatically start up and you will see similar output.&lt;br /&gt;
&lt;br /&gt;
However you launched it, to use it in Behat, add this line to the Moodle config.php (instead of the behat_ionic_dirroot line above):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
The detected version number will be displayed in the output from the init command:&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
 You are already using composer version 1.8.4 (stable channel).&lt;br /&gt;
 Loading composer repositories with package information&lt;br /&gt;
 Installing dependencies (including require-dev) from lock file&lt;br /&gt;
 Nothing to install or update&lt;br /&gt;
 Generating autoload files&lt;br /&gt;
 Behat test environment already installed&lt;br /&gt;
 &#039;&#039;&#039;Configured app tests for version 3.6.1&#039;&#039;&#039;&lt;br /&gt;
 2.5 behat profile detected, automatically converted to current 3.x format&lt;br /&gt;
 Acceptance tests environment enabled on http://localhost/core-moodle-github, to run the tests use:&lt;br /&gt;
 vendor/bin/behat --config C:/mylocation/behat/behat.yml&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
After adding a test with one of these tags (or changing the app version used for testing), make sure you re-run Behat init; it is the init step that decides which tests to include.&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55576</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55576"/>
		<updated>2019-02-12T13:49:00Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Behat init */ Added version number detection info&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.1&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.1&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. &lt;br /&gt;
&lt;br /&gt;
If you are using a local installation of the app, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself before running any Behat tests, and also kill it yourself once you are finished with it. &lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you run it using Docker, it will automatically start up and you will see similar output.&lt;br /&gt;
&lt;br /&gt;
However you launched it, to use it in Behat, add this line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
The detected version number will be displayed in the output from the init command:&lt;br /&gt;
&lt;br /&gt;
 $ php admin/tool/behat/cli/init.php&lt;br /&gt;
 You are already using composer version 1.8.4 (stable channel).&lt;br /&gt;
 Loading composer repositories with package information&lt;br /&gt;
 Installing dependencies (including require-dev) from lock file&lt;br /&gt;
 Nothing to install or update&lt;br /&gt;
 Generating autoload files&lt;br /&gt;
 Behat test environment already installed&lt;br /&gt;
 &#039;&#039;&#039;Configured app tests for version 3.6.1&#039;&#039;&#039;&lt;br /&gt;
 2.5 behat profile detected, automatically converted to current 3.x format&lt;br /&gt;
 Acceptance tests environment enabled on http://localhost/core-moodle-github, to run the tests use:&lt;br /&gt;
 vendor/bin/behat --config C:/mylocation/behat/behat.yml&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
After adding a test with one of these tags (or changing the app version used for testing), make sure you re-run Behat init; it is the init step that decides which tests to include.&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55575</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55575"/>
		<updated>2019-02-12T13:45:14Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Versioning */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.1&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.1&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. &lt;br /&gt;
&lt;br /&gt;
If you are using a local installation of the app, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself before running any Behat tests, and also kill it yourself once you are finished with it. &lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you run it using Docker, it will automatically start up and you will see similar output.&lt;br /&gt;
&lt;br /&gt;
However you launched it, to use it in Behat, add this line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
After adding a test with one of these tags (or changing the app version used for testing), make sure you re-run Behat init; it is the init step that decides which tests to include.&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55574</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55574"/>
		<updated>2019-02-12T13:43:41Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.1&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.1&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. &lt;br /&gt;
&lt;br /&gt;
If you are using a local installation of the app, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself before running any Behat tests, and also kill it yourself once you are finished with it. &lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you run it using Docker, it will automatically start up and you will see similar output.&lt;br /&gt;
&lt;br /&gt;
However you launched it, to use it in Behat, add this line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55573</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55573"/>
		<updated>2019-02-12T13:42:38Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Manually launch the app environment yourself */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.1&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.1&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. &lt;br /&gt;
&lt;br /&gt;
If you are using a local installation of the app, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself before running any Behat tests, and also kill it yourself once you are finished with it. &lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you run it using Docker, it will automatically start up and you will see similar output.&lt;br /&gt;
&lt;br /&gt;
However you launched it, to use it in Behat, add this line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
==== App version detection ====&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55572</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55572"/>
		<updated>2019-02-12T13:42:17Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Add the mobile app Behat configuration */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.1&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.1&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. &lt;br /&gt;
&lt;br /&gt;
If you are using a local installation of the app, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php. This only works if you installed the app development environment locally, not if you are using Docker.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself before running any Behat tests, and also kill it yourself once you are finished with it. &lt;br /&gt;
&lt;br /&gt;
If you have installed the development environment locally, you can launch it using tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;. After launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you run it using Docker, it will automatically start up and you will see similar output.&lt;br /&gt;
&lt;br /&gt;
However you launched it, to use it in Behat, add this line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
==== App version detection ====&lt;br /&gt;
&lt;br /&gt;
When you run Behat init, the system needs to know which version of the app you are running. This is used in order to select tests that only work on certain versions of the app (see below).&lt;br /&gt;
&lt;br /&gt;
* If you specify behat_ionic_dirroot, files in this location will be used to determine the app version. (If you also specify behat_ionic_wwwroot, this will not be used to determine the app version, but you should ensure that the version of the running app is the same as the version of the code in behat_ionic_dirroot.)&lt;br /&gt;
* If you only specify behat_ionic_wwwroot, the version number will be taken from the running app, so you must ensure the app is running when you run the Behat init command, not only when you start tests. You will get an error if it isn&#039;t. (This version detection only works with app version 3.6.1 and above; for older versions you must specify behat_ionic_dirroot.)&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55571</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55571"/>
		<updated>2019-02-12T12:40:52Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Set up a mobile app development environment */ Added information about Docker use&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment. There are two ways to do this: you can either set up your own environment manually (which will be useful if you intend to submit changes or bugfixes to the core app), or you can use Docker to set up a virtual environment. &lt;br /&gt;
&lt;br /&gt;
However you set up the environment, if you update the app, you must re-run Behat init on the corresponding Moodle installation so that it knows about the newer app version.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment yourself ====&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
&lt;br /&gt;
==== Setting up the environment using Docker ====&lt;br /&gt;
&lt;br /&gt;
(For this to work, you must have a Docker installation and know roughly how to use it.) &lt;br /&gt;
&lt;br /&gt;
You can run the app using a Docker image provided by Moodle HQ, with commands like these:&lt;br /&gt;
&lt;br /&gt;
* Specific version 3.6.1&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:3.6.1&lt;br /&gt;
&lt;br /&gt;
* Latest stable version:&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:latest&lt;br /&gt;
&lt;br /&gt;
* Nightly build of the next release&lt;br /&gt;
&lt;br /&gt;
    docker run --rm -p 8100:8100 moodlehq/moodlemobile2:next&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55570</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55570"/>
		<updated>2019-02-12T10:15:16Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Debugging tests */ Changed name of JavaScript functions&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.setField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.press(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behat.pressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few other functions in the &#039;behat&#039; object; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55564</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55564"/>
		<updated>2019-02-11T16:42:09Z</updated>

		<summary type="html">&lt;p&gt;Quen: Rename the config variables&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_dirroot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionic_wwwroot = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_ionic_dirroot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatSetField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatPress(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatPressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few others which all begin with &#039;behat&#039;; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55465</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55465"/>
		<updated>2019-01-15T18:20:00Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Let Behat launch the app environment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_approot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that has the @app tag. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionicaddress = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_approot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatSetField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatPress(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatPressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few others which all begin with &#039;behat&#039;; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55464</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55464"/>
		<updated>2019-01-15T17:53:24Z</updated>

		<summary type="html">&lt;p&gt;Quen: Added a complete example feature file.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_approot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that uses the &#039;I enter the mobile app&#039; step. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionicaddress = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
=== A complete example ===&lt;br /&gt;
&lt;br /&gt;
This example is a complete feature file that loads the app, clicks on a course, and checks the app has now gone to the course page.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
@app @javascript&lt;br /&gt;
Feature: Test app (demo)&lt;br /&gt;
  In order to test something in the app&lt;br /&gt;
  As a developer&lt;br /&gt;
  I need for this test script to run the app&lt;br /&gt;
&lt;br /&gt;
  Background:&lt;br /&gt;
    Given the following &amp;quot;courses&amp;quot; exist:&lt;br /&gt;
      | fullname | shortname |&lt;br /&gt;
      | Course 1 | C1        |&lt;br /&gt;
    And the following &amp;quot;users&amp;quot; exist:&lt;br /&gt;
      | username |&lt;br /&gt;
      | student1 |&lt;br /&gt;
    And the following &amp;quot;course enrolments&amp;quot; exist:&lt;br /&gt;
      | user     | course | role    |&lt;br /&gt;
      | student1 | C1     | student |&lt;br /&gt;
&lt;br /&gt;
  Scenario: Try going into the course&lt;br /&gt;
    When I enter the app&lt;br /&gt;
    And I log in as &amp;quot;student1&amp;quot;&lt;br /&gt;
    And I press &amp;quot;Course 1&amp;quot; near &amp;quot;Course overview&amp;quot; in the app&lt;br /&gt;
    Then the header should be &amp;quot;Course 1&amp;quot; in the app&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_approot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatSetField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatPress(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatPressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few others which all begin with &#039;behat&#039;; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55463</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55463"/>
		<updated>2019-01-15T17:50:38Z</updated>

		<summary type="html">&lt;p&gt;Quen: Added some help about using commands in the developer console.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_approot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that uses the &#039;I enter the mobile app&#039; step. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionicaddress = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_approot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;br /&gt;
&lt;br /&gt;
=== Debugging tests ===&lt;br /&gt;
&lt;br /&gt;
If you insert a pause into your test and open the developer tools (F12), you can see log information in the console about which actions were carried out so far, and whether Behat is waiting for anything. Here is an example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.477 Action - Set field Username to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.480 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:15.982 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.28 PENDING-: &lt;br /&gt;
VM649:391 BEHAT: 17:45:16.98 Action - Set field Password to: student2&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.106 PENDING+: DELAY,dom-mutation&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.607 PENDING-: DELAY&lt;br /&gt;
VM649:391 BEHAT: 17:45:16.653 PENDING-: &lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
While the test is paused you can also carry out some of the app Behat steps manually by typing commands into the console, which is convenient if you&#039;re not quite sure what command would work. Here are examples of the most useful commands:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatSetField(&#039;Password&#039;, &#039;student2&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatPress(&#039;Log in&#039;, &#039;Forgotten&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
* &amp;lt;tt&amp;gt;behatPressStandard(&#039;back&#039;)&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
There are a few others which all begin with &#039;behat&#039;; try using the browser&#039;s autocomplete to see the options.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55462</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55462"/>
		<updated>2019-01-15T17:43:52Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Standard test steps */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_approot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that uses the &#039;I enter the mobile app&#039; step. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionicaddress = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Before the app starts ====&lt;br /&gt;
&lt;br /&gt;
Before starting the app, you normally need to set up information in Moodle (e.g. creating a course and user). For this part of your test you can obviously use all the normal Moodle steps.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;640x360&amp;quot;&amp;lt;/tt&amp;gt; is a useful step if you need to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_approot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55461</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55461"/>
		<updated>2019-01-15T17:41:29Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Actions */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_approot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that uses the &#039;I enter the mobile app&#039; step. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionicaddress = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;360x640&amp;quot;&amp;lt;/tt&amp;gt; is a useful background step if you want your tests to run in mobile phone (rather than tablet/desktop) format. You can also use this step mid-test to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_approot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55460</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55460"/>
		<updated>2019-01-15T17:41:15Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Log in to the app */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_approot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that uses the &#039;I enter the mobile app&#039; step. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionicaddress = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot;&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the same step as used to log into standard Moodle, but if you are on the app login page it will automatically work to log into the app instead. It will log in with the given username, using the same password as username. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;360x640&amp;quot;&amp;lt;/tt&amp;gt; is a useful background step if you want your tests to run in mobile phone (rather than tablet/desktop) format. You can also use this step mid-test to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_approot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Moodle_Desktop&amp;diff=55450</id>
		<title>Moodle Desktop</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Moodle_Desktop&amp;diff=55450"/>
		<updated>2019-01-14T14:31:39Z</updated>

		<summary type="html">&lt;p&gt;Quen: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{Moodle Mobile}}&lt;br /&gt;
&lt;br /&gt;
[http://download.moodle.org/desktop/ Moodle Desktop] is build using the popular framework [http://electron.atom.io/ Electron].&lt;br /&gt;
&lt;br /&gt;
Electron is a very popular cross-platform for building desktop apps with JavaScript, HTML, and CSS.&lt;br /&gt;
&lt;br /&gt;
If you want to build your custom version of Moodle Desktop, please follow the Electron Guides that you will find in https://electron.atom.io/docs/&lt;br /&gt;
&lt;br /&gt;
Please, consider the following:&lt;br /&gt;
* For building a Windows version you will need a Windows computer.&lt;br /&gt;
* The Mac version will require a Mac computer.&lt;br /&gt;
* The Linux version may require a Linux distribution (depending on the selected target).&lt;br /&gt;
&lt;br /&gt;
Note also that if you want to publish your custom versions in the Windows or Apple stores, you will need to sign-up for a developer account (it costs money). &lt;br /&gt;
&lt;br /&gt;
Moodle HQ provides a branding service of Moodle Desktop via the [https://moodle.com/mobileapp/ The Branded Moodle Mobile App]. This service includes publishing in the stores and code signing.&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Deleting the profile ===&lt;br /&gt;
&lt;br /&gt;
Sometimes the app can get into a bad state and you might want to delete its data. When using the desktop environment, you need to delete the contents of the profile folder.&lt;br /&gt;
&lt;br /&gt;
* On Windows this can sometimes be found at C:\Users\ (your username) \AppData\Roaming\moodlemobile&lt;br /&gt;
&lt;br /&gt;
Just quit the app, delete all the files in this folder, and open it again; you&#039;ll start with a fresh app.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Privacy_API&amp;diff=55108</id>
		<title>Privacy API</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Privacy_API&amp;diff=55108"/>
		<updated>2018-11-28T09:57:23Z</updated>

		<summary type="html">&lt;p&gt;Quen: Corrected text explanation on the userlist example so it actually says what the function is doing&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The [https://en.wikipedia.org/wiki/General_Data_Protection_Regulation General Data Protection Regulation] (GDPR) is an EU directive that looks at providing users with more control over their data and how it is processed. This regulation will come into effect on 25th of May 2018 and covers any citizen or permanent resident of the European Union. The directive will be respected by a number of other countries outside of the European Union.&lt;br /&gt;
&lt;br /&gt;
To help institutions become compliant with this new regulation we are adding functionality to Moodle. This includes a number of components, amongst others these include a user’s right to:&lt;br /&gt;
&lt;br /&gt;
* request information on the types of personal data held, the instances of that data, and the deletion policy for each;&lt;br /&gt;
* access all of their data; and&lt;br /&gt;
* be forgotten.&lt;br /&gt;
&lt;br /&gt;
The compliance requirements also extend to installed plugins (including third party plugins). These need to also be able to report what information they store or process regarding users, and have the ability to provide and delete data for a user request.&lt;br /&gt;
&lt;br /&gt;
This document describes the proposed API changes required for plugins which will allow a Moodle installation to become GDPR compliant.&lt;br /&gt;
&lt;br /&gt;
Target Audience: The intended audience for this document is Moodle plugin developers, who are aiming to ensure their plugins are updated to comply with GDPR requirements coming into effect in the EU in May, 2018.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Personal data in Moodle==&lt;br /&gt;
&lt;br /&gt;
From the GDPR Spec, Article 4:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;‘personal data’ means any information relating to an identified or identifiable natural person (‘data subject’); an identifiable natural person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number, location data, an online identifier or to one or more factors specific to the physical, physiological, genetic, mental, economic, cultural or social identity of that natural person;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In Moodle, we need to consider two main types of personal data; information entered by the user and information stored about the user. The key difference being that information stored about the user will have come from a source other than the user themselves. Both types of data can be used to form a profile of the individual.&lt;br /&gt;
&lt;br /&gt;
The most obvious clue to finding personal data entered by the user is the presence of a userid on a database field. Any data on the record (or linked records) pertaining to that user may be deemed personal data for that user, including things like timestamps and record identification numbers. Additionally, any free text field which allows the user to enter information must also be considered to be the personal data of that user.&lt;br /&gt;
&lt;br /&gt;
Data stored about the user includes things like ratings and comments made on a student submission. These may have been made by an assessor or teacher, but are considered the personal data of the student, as they are considered a reflection of the user’s competency in the subject matter and can be used to form a profile of that individual. &lt;br /&gt;
&lt;br /&gt;
The sections that follow outline what you need to do as a plugin developer to ensure any personal data is advertised and can be accessed and deleted according to the GDPR requirements.&lt;br /&gt;
&lt;br /&gt;
==Background==&lt;br /&gt;
&lt;br /&gt;
===Architecture overview===&lt;br /&gt;
&lt;br /&gt;
[[File:MoodlePrivacyMetadataUML.png|thumb|UML diagram of the metadata part of the privacy subsystem]]&lt;br /&gt;
[[File:MoodlePrivacyRequestUML.png|thumb|UML diagram of the request providers part of the privacy subsystem]]&lt;br /&gt;
&lt;br /&gt;
A new system for Privacy has been created within Moodle. This is broken down into several main parts and forms the &#039;&#039;core_privacy&#039;&#039; subsystem:&lt;br /&gt;
&lt;br /&gt;
* Some metadata providers - a set of PHP interfaces to be implemented by components for that component to describe the kind of data that it stores, and the purpose for its storage;&lt;br /&gt;
* Some request providers - a set of PHP interfaces to be implemented by components to allow that component to act upon user requests such as the Right to be Forgotten, and a Subject Access Request; and&lt;br /&gt;
* A manager - a concrete class used to bridge components which implement the providers with tools which request their data.&lt;br /&gt;
&lt;br /&gt;
All plugins will implement one metadata provider, and zero, one or two request providers.&lt;br /&gt;
&lt;br /&gt;
The &#039;request&#039; providers are responsible for several separate areas:&lt;br /&gt;
&lt;br /&gt;
# Detecting in which Moodle contexts a specific user has any personal data;&lt;br /&gt;
# Exporting all personal data from each of those contexts for that user;&lt;br /&gt;
# Deleting all personal data from each of those contexts for that user;&lt;br /&gt;
# Detecting which users have personal data in a specific Moodle context;&lt;br /&gt;
# Deleting all personal data for each of those users in that context; and&lt;br /&gt;
# Deleting all personal data for all users in a specific context.&lt;br /&gt;
&lt;br /&gt;
The export and delete phases use data from the detection phase, which allows for the possibility to exclude all data from certain contexts, as required.&lt;br /&gt;
&lt;br /&gt;
Please refer to the inline phpdocs of the [https://github.com/moodle/moodle/blob/v3.5.0/privacy/classes/manager.php#L31 core_privacy::manager class] for detailed description of the interfaces, their hierarchy and meaning.&lt;br /&gt;
&lt;br /&gt;
Please note that support for locating and removing multiple users in a single context was added in MDL-62560 for Moodle 3.6, 3.5.3, and 3.4.6.&lt;br /&gt;
This functionality has been added to allow support for removal of user data for users subject to speific role criterion, and to support expiry of the same data by role too.&lt;br /&gt;
&lt;br /&gt;
===Implementing a provider===&lt;br /&gt;
&lt;br /&gt;
All plugins will need to create a concrete class which implements the relevant metadata and request providers. The exact providers you need to implement will depend on what data you store, and the type of plugin. This is covered in more detail in the following sections of the document.&lt;br /&gt;
&lt;br /&gt;
In order to do so:&lt;br /&gt;
&lt;br /&gt;
# You must create a class called &#039;&#039;provider&#039;&#039; within the namespace &#039;&#039;\your_pluginname\privacy&#039;&#039;.&lt;br /&gt;
# This class must be created at &#039;&#039;path/to/your/plugin/classes/privacy/provider.php&#039;&#039;.&lt;br /&gt;
# You must have your class implement the relevant metadata and request interfaces.&lt;br /&gt;
&lt;br /&gt;
==Plugins which do not store personal data==&lt;br /&gt;
&lt;br /&gt;
Many Moodle plugins do not store any personal data. This is usually the case for plugins which just add functionality, or which display the data already stored elsewhere in Moodle.&lt;br /&gt;
&lt;br /&gt;
Some examples of plugin types which might fit this criteria include themes, blocks, filters, editor plugins, etc.&lt;br /&gt;
&lt;br /&gt;
Plugins which cause data to be stored elsewhere in Moodle (e.g. via a subsystem call) are considered to store data.&lt;br /&gt;
&lt;br /&gt;
One examples of a plugin which does not store any data would be the Calendar month block which just displays a view of the user’s calendar. It does not store any data itself.&lt;br /&gt;
&lt;br /&gt;
An example of a plugin which must not use the null provider is the Comments block. The comments block is responsible for data subsequently being stored within Moodle. Although the block doesn’t store anything itself, it interacts with the comments subsystem and is the only component which knows how that data maps to a user.&lt;br /&gt;
&lt;br /&gt;
===Implementation requirements===&lt;br /&gt;
&lt;br /&gt;
In order to let Moodle know that you have audited your plugin, and that you do not store any personal user data, you must implement the &#039;&#039;\core_privacy\local\metadata\null_provider&#039;&#039; interface in your plugin’s provider.&lt;br /&gt;
&lt;br /&gt;
These null providers can only be implemented where a plugin has:&lt;br /&gt;
&lt;br /&gt;
* no external links (e.g. sends data to an external service like an LTI provider, repository plugin which you can search on)&lt;br /&gt;
* no database tables which store user data (including IP addresses)&lt;br /&gt;
* no user preferences&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;null_provider&#039;&#039; requires you to define one function &#039;&#039;get_reason()&#039;&#039; which returns the language string identifier within your component.&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;blocks/calendar_month/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
 &amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
&lt;br /&gt;
namespace block_calendar_month\privacy;&lt;br /&gt;
&lt;br /&gt;
class provider implements&lt;br /&gt;
    // This plugin does not store any personal user data.&lt;br /&gt;
    \core_privacy\local\metadata\null_provider {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the language string identifier with the component&#039;s language&lt;br /&gt;
     * file to explain why this plugin stores no data.&lt;br /&gt;
     *&lt;br /&gt;
     * @return  string&lt;br /&gt;
     */&lt;br /&gt;
    public static function get_reason() : string {&lt;br /&gt;
        return &#039;privacy:metadata&#039;;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;blocks/calendar_month/lang/en/block_calendar_month.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
&lt;br /&gt;
...&lt;br /&gt;
$string[&#039;privacy:metadata&#039;] = &#039;The Calendar block only displays existing calendar data.&#039;;&lt;br /&gt;
...&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
That’s it. Congratulations, your plugin now implements the Privacy API.&lt;br /&gt;
&lt;br /&gt;
==Plugins which store personal data==&lt;br /&gt;
&lt;br /&gt;
Many Moodle plugins do store some form of personal data.&lt;br /&gt;
&lt;br /&gt;
In some cases this will be stored within database tables in your plugin, and in other cases this will be in one of Moodle’s core subsystems - for example your plugin may store files, ratings, comments, or tags.&lt;br /&gt;
&lt;br /&gt;
Plugins which do store data will need to:&lt;br /&gt;
&lt;br /&gt;
* Describe the type of data that they store;&lt;br /&gt;
* Provide a way to export that data; and&lt;br /&gt;
* Provide a way to delete that data.&lt;br /&gt;
&lt;br /&gt;
Data is described via a &#039;&#039;metadata&#039;&#039; provider, and it is both exported and deleted via an implementation of a &#039;&#039;request&#039;&#039; provider.&lt;br /&gt;
&lt;br /&gt;
These are both explained in the sections below.&lt;br /&gt;
&lt;br /&gt;
===Describing the type of data you store===&lt;br /&gt;
&lt;br /&gt;
In order to describe the type of data that you store, you must implement the &#039;&#039;\core_privacy\local\metadata\provider&#039;&#039; interface.&lt;br /&gt;
&lt;br /&gt;
This interfaces requires that you define one function: &#039;&#039;get_metadata&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
There are several types of item to describe the data that you store. These are for:&lt;br /&gt;
&lt;br /&gt;
* Items in the Moodle database;&lt;br /&gt;
* Items stored by you in a Moodle subsystem - for example files, and ratings; and&lt;br /&gt;
* User preferences stored site-wide within Moodle for your plugin&lt;br /&gt;
&lt;br /&gt;
Note: All fields should include a description from a language string within your plugin.&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
&lt;br /&gt;
namespace mod_forum\privacy;&lt;br /&gt;
use core_privacy\local\metadata\collection;&lt;br /&gt;
&lt;br /&gt;
class provider implements &lt;br /&gt;
        // This plugin does store personal user data.&lt;br /&gt;
        \core_privacy\local\metadata\provider {&lt;br /&gt;
&lt;br /&gt;
    public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
        // Here you will add more items into the collection.&lt;br /&gt;
&lt;br /&gt;
        return $collection;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Indicating that you store content in a Moodle subsystem====&lt;br /&gt;
&lt;br /&gt;
Many plugins will use one of the core Moodle subsystems to store data.&lt;br /&gt;
&lt;br /&gt;
As a plugin developer we do not expect you to describe those subsystems in detail, but we do need to know that you use them and to know what you use them for.&lt;br /&gt;
&lt;br /&gt;
You can indicate this by calling the &#039;&#039;add_subsystem_link()&#039;&#039; method on the &#039;&#039;collection&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=====Relevant subsystems=====&lt;br /&gt;
&lt;br /&gt;
You are likely to need to indicate use of the following subsystems that store user data:&lt;br /&gt;
&lt;br /&gt;
* Ratings (if users are allowed to rate items within your plugin)&lt;br /&gt;
* Tags (if users can tag items within your plugin)&lt;br /&gt;
* Comments (if users can make comments on items in your plugin)&lt;br /&gt;
* Questions (if the plugin uses core question types)&lt;br /&gt;
* Filesystem (if users can attach files to items within your plugin)&lt;br /&gt;
* ...?&lt;br /&gt;
&lt;br /&gt;
Some subsystems which store user data do not need to be listed:&lt;br /&gt;
&lt;br /&gt;
* (TBC - how about global search? Nothing lists it that I could see.)&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
    $collection-&amp;gt;add_subsystem_link(&lt;br /&gt;
        &#039;core_files&#039;,&lt;br /&gt;
        [],&lt;br /&gt;
        &#039;privacy:metadata:core_files&#039;&lt;br /&gt;
    );&lt;br /&gt;
&lt;br /&gt;
    return $collection;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/lang/en/forum.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
&lt;br /&gt;
$string[&#039;privacy:metadata:core_files&#039;] = &#039;The forum stores files which have been uploaded by the user to form part of a forum post.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Describing data stored in database tables====&lt;br /&gt;
&lt;br /&gt;
Most Moodle plugins will store some form of user data in their own database tables.&lt;br /&gt;
&lt;br /&gt;
As a plugin developer you will need to describe each database table, and each field which includes user data.&lt;br /&gt;
&lt;br /&gt;
It is a matter of judgement which fields contain user data and which don&#039;t. Anything entered by, or directly about, the user probably counts as user data but it may be useful to include additional fields that explain the context of the data.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
    $collection-&amp;gt;add_database_table(&lt;br /&gt;
        &#039;forum_discussion_subs&#039;,&lt;br /&gt;
         [&lt;br /&gt;
            &#039;userid&#039; =&amp;gt; &#039;privacy:metadata:forum_discussion_subs:userid&#039;,&lt;br /&gt;
            &#039;discussionid&#039; =&amp;gt; &#039;privacy:metadata:forum_discussion_subs:discussionid&#039;,&lt;br /&gt;
            &#039;preference&#039; =&amp;gt; &#039;privacy:metadata:forum_discussion_subs:preference&#039;,&lt;br /&gt;
&lt;br /&gt;
         ],&lt;br /&gt;
        &#039;privacy:metadata:forum_discussion_subs&#039;&lt;br /&gt;
    );&lt;br /&gt;
&lt;br /&gt;
    return $collection;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/lang/en/forum.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
&lt;br /&gt;
$string[&#039;privacy:metadata:forum_discussion_subs&#039;] = &#039;Information about the subscriptions to individual forum discussions. This includes when a user has chosen to subscribe to a discussion, or to unsubscribe from one where they would otherwise be subscribed.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:forum_discussion_subs:userid&#039;] = &#039;The ID of the user with this subscription preference.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:forum_discussion_subs:discussionid&#039;] = &#039;The ID of the discussion that was subscribed to.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:forum_discussion_subs:preference&#039;] = &#039;The start time of the subscription.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Indicating that you store site-wide user preferences====&lt;br /&gt;
&lt;br /&gt;
Many plugins will include one or more user preferences. Unfortunately this is one of Moodle’s older components and many of the values stored are not pure user preferences. Each plugin should be aware of how it handles its own preferences and is best placed to determine whether they are site-wide preferences, or per-instance preferences.&lt;br /&gt;
&lt;br /&gt;
Whilst most of these will have a fixed name (e.g. &#039;&#039;filepicker_recentrepository&#039;&#039;), some will include a variable of some kind (e.g. &#039;&#039;tool_usertours_tour_completion_time_2&#039;&#039;). Only the general name (in this case &amp;quot;tool_usertours_tour_completion_time_&amp;quot;) needs to be indicated, rather than one copy for each possible value of the variable.&lt;br /&gt;
&lt;br /&gt;
Also, these should only be &#039;&#039;site-wide&#039;&#039; user preferences which do not belong to a specific Moodle context.&lt;br /&gt;
&lt;br /&gt;
In the above examples:&lt;br /&gt;
&lt;br /&gt;
* Preference &#039;&#039;filepicker_recentrepository&#039;&#039; belongs to the file subsystem, and is a site-wide preference affecting the user anywhere that they view the filepicker.&lt;br /&gt;
* Preference &#039;&#039;tool_usertours_tour_completion_time_2&#039;&#039; belongs to user tours. User tours are a site-wide feature which can affect many parts of Moodle and cross multiple contexts.&lt;br /&gt;
&lt;br /&gt;
In some cases a value may be stored in the preferences table but is known to belong to a specific context within Moodle. In these cases they should be stored as metadata against that context rather than as a site-wide user preference.&lt;br /&gt;
&lt;br /&gt;
You can indicate this by calling the &#039;&#039;add_user_preference()&#039;&#039; method on the &#039;&#039;collection&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Any plugin providing user preferences must also implement the &#039;&#039;\core_privacy\local\request\preference_provider&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;admin/tool/usertours/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
    $collection-&amp;gt;add_user_preference(&#039;tool_usertours_tour_completion_time&#039;,&lt;br /&gt;
        &#039;privacy:metadata:preference:tool_usertours_tour_completion_time&#039;);&lt;br /&gt;
&lt;br /&gt;
    return $collection;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;admin/tool/usertours/lang/en/tool_usertours.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
&lt;br /&gt;
$string[&#039;privacy:metadata:tool_usertours_tour_completion_time&#039;] = &#039;The time that a specific user tour was last completed by a user.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Indicating that you export data to an external location====&lt;br /&gt;
&lt;br /&gt;
Many plugins will interact with external systems - for example cloud-based services. Often this external location is configurable within the plugin either at the site or the instance level.&lt;br /&gt;
&lt;br /&gt;
As a plugin developer you will need to describe each &#039;&#039;type&#039;&#039; of target destination, alongside a list of each exported field which includes user data.&lt;br /&gt;
The &#039;&#039;actual&#039;&#039; destination does not need to be described as this can change based on configuration.&lt;br /&gt;
&lt;br /&gt;
You can indicate this by calling the &#039;&#039;add_external_location_link()&#039;&#039; method on the collection.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/lti/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function get_metadata(collection $collection) : collection {&lt;br /&gt;
&lt;br /&gt;
    $collection-&amp;gt;add_external_location_link(&#039;lti_client&#039;, [&lt;br /&gt;
            &#039;userid&#039; =&amp;gt; &#039;privacy:metadata:lti_client:userid&#039;,&lt;br /&gt;
            &#039;fullname&#039; =&amp;gt; &#039;privacy:metadata:lti_client:fullname&#039;,&lt;br /&gt;
        ], &#039;privacy:metadata:lti_client&#039;);&lt;br /&gt;
&lt;br /&gt;
    return $collection;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/lti/lang/en/lti.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
&lt;br /&gt;
$string[&#039;privacy:metadata:lti_client&#039;] = &#039;In order to integrate with a remote LTI service, user data needs to be exchanged with that service.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:lti_client:userid&#039;] = &#039;The userid is sent from Moodle to allow you to access your data on the remote system.&#039;;&lt;br /&gt;
$string[&#039;privacy:metadata:lti_client:fullname&#039;] = &#039;Your full name is sent to the remote system to allow a better user experience.&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Providing a way to export user data===&lt;br /&gt;
&lt;br /&gt;
In order to export the user data that you store, you must implement the relevant request provider.&lt;br /&gt;
&lt;br /&gt;
We have named these request providers because they are called in response to a specific request from a user to access their information, however they also deal with the removal of user data following it&#039;s expiry.&lt;br /&gt;
&lt;br /&gt;
There are several different types of request provider, and you may need to implement several of these, depending on the type and nature of your plugin.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking plugins will fit into one of the following categories:&lt;br /&gt;
&lt;br /&gt;
* Plugins which are a subplugin of another plugin. Examples include &#039;&#039;assignsubmission&#039;&#039;, &#039;&#039;atto&#039;&#039;, and &#039;&#039;datafield&#039;&#039;;&lt;br /&gt;
* Plugins which are typically called by a Moodle subsystem. Examples include &#039;&#039;qtype&#039;&#039;, and &#039;&#039;profilefield&#039;&#039;;&lt;br /&gt;
* All other plugins which store data.&lt;br /&gt;
&lt;br /&gt;
Most plugins will fit into this final category, whilst other plugins may fall into several categories.&lt;br /&gt;
Plugins which &#039;&#039;define&#039;&#039; a subplugin will also be responsible for  collecting this data from their subplugins.&lt;br /&gt;
&lt;br /&gt;
A final category exists - plugins which store user preferences. In some cases this may be the &#039;&#039;only&#039;&#039; provider implemented.&lt;br /&gt;
&lt;br /&gt;
====Standard plugins which store data====&lt;br /&gt;
&lt;br /&gt;
A majority of Moodle plugins will fit into this category and will be required to implement the &#039;&#039;\core_privacy\local\request\plugin\provider&#039;&#039; interface. This interface requires that you define four functions (the first two of which are dealt with in this section):&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;get_contexts_for_userid&#039;&#039; - to explain where data is held within Moodle for your plugin; and&lt;br /&gt;
* &#039;&#039;export_user_data&#039;&#039; - to export a user’s personal data from your plugin.&lt;br /&gt;
* &#039;&#039;delete_data_for_all_users_in_context&#039;&#039; - to delete all data for all users in the specified context.&lt;br /&gt;
* &#039;&#039;delete_data_for_user&#039;&#039; - to delete all user data for the specified user, in the specified contexts.&lt;br /&gt;
&lt;br /&gt;
These APIs make use of the Moodle &#039;&#039;context&#039;&#039; system to hierarchically store this data.&lt;br /&gt;
&lt;br /&gt;
In addition to these requirements, since Moodle 3.4.6, 3.5.3, any plugin which implements the plugin provider interface must also implement the &#039;&#039;\core_privacy\local\request\core_userlist_provider&#039;&#039; provider and implement functions:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;get_users_in_context&#039;&#039; - to locate the users who hold any personal data in a specific context; and&lt;br /&gt;
* &#039;&#039;delete_data_for_users&#039;&#039; - to delete data for multiple users in the specified context.&lt;br /&gt;
&lt;br /&gt;
=====Retrieving the list of contexts=====&lt;br /&gt;
&lt;br /&gt;
You are required to return the list of contexts for which the plugin stores data about the user. These are the standard Moodle contexts - CONTEXT_COURSE, CONTEXT_USER, CONTEXT_MODULE and so on. In many cases the link between the module type and the context is self-evident (e.g. activity modules). In some cases it may be less so. For example, an enrolment plugin (that stores user data, which most don&#039;t) would link to course context (users enrol in courses). Other types of plugins may be less obvious but you need to pick something. One way might to consider what context you would use when checking role capabilities. Consider that this will be used to structure the exported data and define what data is deleted when a context is expired. &lt;br /&gt;
&lt;br /&gt;
Contexts are retrieved using the &#039;&#039;get_contexts_for_userid&#039;&#039; function which takes the ID of the user being fetched, and returns a list of contexts in which the user has any data.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the list of contexts that contain user information for the specified user.&lt;br /&gt;
     *&lt;br /&gt;
     * @param   int           $userid       The user to search.&lt;br /&gt;
     * @return  contextlist   $contextlist  The list of contexts used in this plugin.&lt;br /&gt;
     */&lt;br /&gt;
    public static function get_contexts_for_userid(int $userid) : contextlist {}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The function returns a &#039;&#039;\core_privacy\local\request\contextlist&#039;&#039; which is used to keep a set of contexts together in a fixed fashion.&lt;br /&gt;
&lt;br /&gt;
Because a Subject Access Request covers &#039;&#039;every&#039;&#039; piece of data that is held for a user within Moodle, efficiency and performance is highly important. As a result, contexts are added to the &#039;&#039;contextlist&#039;&#039; by defining one or more SQL queries which return just the contextid. Multiple SQL queries can be added as required. &lt;br /&gt;
&lt;br /&gt;
Many plugins will interact with specific subsystems and store data within them.&lt;br /&gt;
These subsystems will also provide a way in which to link the data that you have stored with your own database tables.&lt;br /&gt;
At present these are still a work in progress and only the &#039;&#039;core_ratings&#039;&#039; subsystem includes this.&lt;br /&gt;
&lt;br /&gt;
======Basic example======&lt;br /&gt;
&lt;br /&gt;
The following example simply fetches the contextid for all forums where a user has a single discussion (note: this is an incomplete example):&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the list of contexts that contain user information for the specified user.&lt;br /&gt;
     *&lt;br /&gt;
     * @param   int           $userid       The user to search.&lt;br /&gt;
     * @return  contextlist   $contextlist  The list of contexts used in this plugin.&lt;br /&gt;
     */&lt;br /&gt;
    public static function get_contexts_for_userid(int $userid) : contextlist {&lt;br /&gt;
        $contextlist = new \core_privacy\local\request\contextlist();&lt;br /&gt;
&lt;br /&gt;
        $sql = &amp;quot;SELECT c.id&lt;br /&gt;
                 FROM {context} c&lt;br /&gt;
           INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel&lt;br /&gt;
           INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname&lt;br /&gt;
           INNER JOIN {forum} f ON f.id = cm.instance&lt;br /&gt;
            LEFT JOIN {forum_discussions} d ON d.forum = f.id&lt;br /&gt;
                WHERE (&lt;br /&gt;
                d.userid        = :discussionuserid&lt;br /&gt;
                )&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
&lt;br /&gt;
        $params = [&lt;br /&gt;
            &#039;modname&#039;           =&amp;gt; &#039;forum&#039;,&lt;br /&gt;
            &#039;contextlevel&#039;      =&amp;gt; CONTEXT_MODULE,&lt;br /&gt;
            &#039;discussionuserid&#039;  =&amp;gt; $userid,&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        $contextlist-&amp;gt;add_from_sql($sql, $params);&lt;br /&gt;
&lt;br /&gt;
        return $contextlist;&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
======More complete example======&lt;br /&gt;
&lt;br /&gt;
The following example includes a link to core_rating. &lt;br /&gt;
It will find any forum, forum discussion, or forum post where the user has any data, including:&lt;br /&gt;
&lt;br /&gt;
* Per-forum digest preferences;&lt;br /&gt;
* Per-forum subscription preferences;&lt;br /&gt;
* Per-forum read tracking preferences;&lt;br /&gt;
* Per-discussion subscription preferences;&lt;br /&gt;
* Per-post read data (if a user has read a post or not); and&lt;br /&gt;
* Per-post rating data.&lt;br /&gt;
&lt;br /&gt;
In the case of the rating data, this will include any post where the user has rated the post of another user.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Get the list of contexts that contain user information for the specified user.&lt;br /&gt;
 *&lt;br /&gt;
 * @param   int           $userid       The user to search.&lt;br /&gt;
 * @return  contextlist   $contextlist  The list of contexts used in this plugin.&lt;br /&gt;
 */&lt;br /&gt;
public static function get_contexts_for_userid(int $userid) : contextlist {&lt;br /&gt;
    $ratingsql = \core_rating\privacy\provider::get_sql_join(&#039;rat&#039;, &#039;mod_forum&#039;, &#039;post&#039;, &#039;p.id&#039;, $userid);&lt;br /&gt;
    // Fetch all forum discussions, and forum posts.&lt;br /&gt;
    $sql = &amp;quot;SELECT c.id&lt;br /&gt;
                FROM {context} c&lt;br /&gt;
        INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel&lt;br /&gt;
        INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname&lt;br /&gt;
        INNER JOIN {forum} f ON f.id = cm.instance&lt;br /&gt;
            LEFT JOIN {forum_discussions} d ON d.forum = f.id&lt;br /&gt;
            LEFT JOIN {forum_posts} p ON p.discussion = d.id&lt;br /&gt;
            LEFT JOIN {forum_digests} dig ON dig.forum = f.id&lt;br /&gt;
            LEFT JOIN {forum_subscriptions} sub ON sub.forum = f.id&lt;br /&gt;
            LEFT JOIN {forum_track_prefs} pref ON pref.forumid = f.id&lt;br /&gt;
            LEFT JOIN {forum_read} hasread ON hasread.forumid = f.id&lt;br /&gt;
            LEFT JOIN {forum_discussion_subs} dsub ON dsub.forum = f.id&lt;br /&gt;
            {$ratingsql-&amp;gt;join}&lt;br /&gt;
                WHERE (&lt;br /&gt;
                p.userid        = :postuserid OR&lt;br /&gt;
                d.userid        = :discussionuserid OR&lt;br /&gt;
                dig.userid      = :digestuserid OR&lt;br /&gt;
                sub.userid      = :subuserid OR&lt;br /&gt;
                pref.userid     = :prefuserid OR&lt;br /&gt;
                hasread.userid  = :hasreaduserid OR&lt;br /&gt;
                dsub.userid     = :dsubuserid OR&lt;br /&gt;
                {$ratingsql-&amp;gt;userwhere}&lt;br /&gt;
            )&lt;br /&gt;
    &amp;quot;;&lt;br /&gt;
&lt;br /&gt;
    $params = [&lt;br /&gt;
        &#039;modname&#039;           =&amp;gt; &#039;forum&#039;,&lt;br /&gt;
        &#039;contextlevel&#039;      =&amp;gt; CONTEXT_MODULE,&lt;br /&gt;
        &#039;postuserid&#039;        =&amp;gt; $userid,&lt;br /&gt;
        &#039;discussionuserid&#039;  =&amp;gt; $userid,&lt;br /&gt;
        &#039;digestuserid&#039;      =&amp;gt; $userid,&lt;br /&gt;
        &#039;subuserid&#039;         =&amp;gt; $userid,&lt;br /&gt;
        &#039;prefuserid&#039;        =&amp;gt; $userid,&lt;br /&gt;
        &#039;hasreaduserid&#039;     =&amp;gt; $userid,&lt;br /&gt;
        &#039;dsubuserid&#039;        =&amp;gt; $userid,&lt;br /&gt;
    ];&lt;br /&gt;
    $params += $ratingsql-&amp;gt;params;&lt;br /&gt;
&lt;br /&gt;
    $contextlist = new \core_privacy\local\request\contextlist();&lt;br /&gt;
    $contextlist-&amp;gt;add_from_sql($sql, $params);&lt;br /&gt;
&lt;br /&gt;
    return $contextlist;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Retrieving the users in a context=====&lt;br /&gt;
&lt;br /&gt;
You are required to return the list of users holding personal data in a context for which the plugin stores data about the user.&lt;br /&gt;
This method is very similar to the &#039;&#039;get_contexts_for_userid&#039;&#039; function but has some important distinctions:&lt;br /&gt;
&lt;br /&gt;
* It takes a &#039;&#039;userlist&#039;&#039; as an argument and does not require that you, as a developer, create it yourself;&lt;br /&gt;
* There is no need to return any value; and&lt;br /&gt;
* The argument for the &#039;&#039;userlist::add_from_sql&#039;&#039; are different to those for &#039;&#039;contextlist::add_from_sql&#039;&#039; (this is because we learnt some important lessons after the initial implementation).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Get the list of users who have data within a context.&lt;br /&gt;
 *&lt;br /&gt;
 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.&lt;br /&gt;
 */&lt;br /&gt;
public static function get_users_in_context(userlist $userlist) {}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
======Basic example======&lt;br /&gt;
&lt;br /&gt;
The following example simply fetches the userid for all users in a given forum context, who created a discussion or a post in that forum (note: this is an incomplete example):&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Get the list of contexts that contain user information for the specified user.&lt;br /&gt;
 *&lt;br /&gt;
 * @param   int           $userid       The user to search.&lt;br /&gt;
 * @return  contextlist   $contextlist  The list of contexts used in this plugin.&lt;br /&gt;
 */&lt;br /&gt;
public static function get_contexts_for_userid(int $userid) : contextlist {&lt;br /&gt;
&lt;br /&gt;
    $context = $userlist-&amp;gt;get_context();&lt;br /&gt;
&lt;br /&gt;
    if (!$context instanceof \context_module) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $params = [&lt;br /&gt;
        &#039;instanceid&#039;    =&amp;gt; $context-&amp;gt;instanceid,&lt;br /&gt;
        &#039;modulename&#039;    =&amp;gt; &#039;forum&#039;,&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
    // Discussion authors.&lt;br /&gt;
    $sql = &amp;quot;SELECT d.userid&lt;br /&gt;
              FROM {course_modules} cm&lt;br /&gt;
              JOIN {modules} m ON m.id = cm.module AND m.name = :modulename&lt;br /&gt;
              JOIN {forum} f ON f.id = cm.instance&lt;br /&gt;
              JOIN {forum_discussions} d ON d.forum = f.id&lt;br /&gt;
             WHERE cm.id = :instanceid&amp;quot;;&lt;br /&gt;
    $userlist-&amp;gt;add_from_sql(&#039;userid&#039;, $sql, $params);&lt;br /&gt;
&lt;br /&gt;
     // Forum authors.&lt;br /&gt;
    $sql = &amp;quot;SELECT p.userid&lt;br /&gt;
              FROM {course_modules} cm&lt;br /&gt;
              JOIN {modules} m ON m.id = cm.module AND m.name = :modulename&lt;br /&gt;
              JOIN {forum} f ON f.id = cm.instance&lt;br /&gt;
              JOIN {forum_discussions} d ON d.forum = f.id&lt;br /&gt;
              JOIN {forum_posts} p ON d.id = p.discussion&lt;br /&gt;
             WHERE cm.id = :instanceid&amp;quot;;&lt;br /&gt;
    $userlist-&amp;gt;add_from_sql(&#039;userid&#039;, $sql, $params);&lt;br /&gt;
&lt;br /&gt;
    // ...&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=====Exporting user data=====&lt;br /&gt;
&lt;br /&gt;
After determining where in Moodle your plugin holds data about a user, the &#039;&#039;\core_privacy\manager&#039;&#039; will then ask your plugin to export all user data for a subset of those locations.&lt;br /&gt;
&lt;br /&gt;
This is achieved through use of the &#039;&#039;export_user_data&#039;&#039; function which takes the list of approved contexts in a &#039;&#039;\core_privacy\local\request\approved_contextlist&#039;&#039; object.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Export all user data for the specified user, in the specified contexts, using the supplied exporter instance.&lt;br /&gt;
 *&lt;br /&gt;
 * @param   approved_contextlist    $contextlist    The approved contexts to export information for.&lt;br /&gt;
 */&lt;br /&gt;
public static function export_user_data(approved_contextlist $contextlist) {}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;approved_contextlist&#039;&#039; includes both the user record, and a list of contexts, which can be retrieved by either processing it as an Iterator, or by calling &#039;&#039;get_contextids()&#039;&#039; or &#039;&#039;get_contexts()&#039;&#039; as required.&lt;br /&gt;
&lt;br /&gt;
Data is exported using a &#039;&#039;\core_privacy\local\request\content_writer&#039;&#039;, which is described in further detail below.&lt;br /&gt;
&lt;br /&gt;
====Plugins which store user preferences====&lt;br /&gt;
&lt;br /&gt;
Many plugins store a variety of user preferences, and must therefore export them.&lt;br /&gt;
&lt;br /&gt;
Since user preferences are a site-wide preference, these are exported separately to other user data.&lt;br /&gt;
In some cases the only data present is user preference data, whilst in others there is a combination of user-provided data, and user preferences.&lt;br /&gt;
&lt;br /&gt;
Storing of user preferences is achieved through implementation of the &#039;&#039;\core_privacy\local\request\user_preference_provider&#039;&#039; interface which defines one required function -- &#039;&#039;export_user_preferences&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
You need to provide a description of the value of the user preference. (This description is particularly useful in cases where the value might be, say, 3, but it actually means &#039;Alphabetical order&#039;.) Most likely you can use an existing language string from your plugin.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Export all user preferences for the plugin.&lt;br /&gt;
 *&lt;br /&gt;
 * @param   int         $userid The userid of the user whose data is to be exported.&lt;br /&gt;
 */&lt;br /&gt;
public static function export_user_preferences(int $userid) {&lt;br /&gt;
    $markasreadonnotification = get_user_preference(&#039;markasreadonnotification&#039;, null, $userid);&lt;br /&gt;
    if (null !== $markasreadonnotification) {&lt;br /&gt;
        switch ($markasreadonnotification) {&lt;br /&gt;
            case 0:&lt;br /&gt;
                $markasreadonnotificationdescription = get_string(&#039;markasreadonnotificationno&#039;, &#039;mod_forum&#039;);&lt;br /&gt;
                break;&lt;br /&gt;
            case 1:&lt;br /&gt;
            default:&lt;br /&gt;
                $markasreadonnotificationdescription = get_string(&#039;markasreadonnotificationyes&#039;, &#039;mod_forum&#039;);&lt;br /&gt;
                break;&lt;br /&gt;
        }&lt;br /&gt;
        writer::export_user_preference(&#039;mod_forum&#039;, &#039;markasreadonnotification&#039;, $markasreadonnotification, $markasreadonnotificationdescription);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Plugins which can have own subplugins ====&lt;br /&gt;
&lt;br /&gt;
Many plugin types are also able to define their own subplugins and will need to define a contract between themselves and their subplugins in order to fetch their data.&lt;br /&gt;
&lt;br /&gt;
This is required as the parent plugin and the child subplugin should be separate entities and the parent plugin must be able to function if one or more of its subplugins are uninstalled.&lt;br /&gt;
&lt;br /&gt;
The parent plugin is responsible for defining the contract,  and for interacting with its subplugins, though we intend to create helpers to make this easier.&lt;br /&gt;
&lt;br /&gt;
The parent plugin should define a new interface for each type of subplugin that it defines. This interface should extend the &#039;&#039;\core_privacy\local\request\plugin\subplugin_provider&#039;&#039; interface.&lt;br /&gt;
&lt;br /&gt;
===== When a parent plugin should and should not provide the interface for its subplugins =====&lt;br /&gt;
&lt;br /&gt;
There can be cases when there is no point for a plugin to provide the &amp;quot;subplugin_provider&amp;quot; based interface, even if it has own subplugins. See the Atto or TinyMCE editors as real examples.&lt;br /&gt;
&lt;br /&gt;
If the parent plugin has no data passed through to the subplugins, there is no benefit in defining a subplugin provider. For example, Atto subplugins are just used to enhance the functionality and they never receive anything like a context. Most of the time we need to define a subplugin provider, but in cases where there is no data passed from the plugin to its subplugins, there is no need to define the subplugin provider. If the subplugins still do store personal data that are not related to the parent plugin in any way, then subplugins should define their own standard provider.&lt;br /&gt;
&lt;br /&gt;
Compare with something like mod_assign where the subplugins store data for the parent and that data is contextually relevant to the parent plugin. In those cases the subplugin stores data for the plugin and it only makes sense to do so in the context of its parent plugin.&lt;br /&gt;
&lt;br /&gt;
=====Example=====&lt;br /&gt;
&lt;br /&gt;
The following example defines the contract that assign submission subplugins may be required to implement.&lt;br /&gt;
&lt;br /&gt;
The assignment module is responsible for returning the contexts of all assignments where a user has data, but in some cases it is unaware of all of those cases - for example if a Teacher comments on a student submission it may not be aware of these as the information about this interaction may not be stored within its own tables.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/assign/privacy/assignsubmission_provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
&lt;br /&gt;
namespace mod_assign\privacy;&lt;br /&gt;
use \core_privacy\local\metadata\collection;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
interface assignsubmission_provider extends&lt;br /&gt;
    // This Interface defines a subplugin.&lt;br /&gt;
    \core_privacy\local\request\plugin\subplugin_provider {&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Get the SQL required to find all submission items where this user has had any involvements. &lt;br /&gt;
     *&lt;br /&gt;
     * @param   int           $userid       The user to search.&lt;br /&gt;
     * @return  \stdClass                   Object containing the join, params, and where used to select a these records from the database.&lt;br /&gt;
     */&lt;br /&gt;
    public static function get_items_with_user_interaction(int $userid) : \stdClass ;&lt;br /&gt;
&lt;br /&gt;
    /**&lt;br /&gt;
     * Export all relevant user submissions information which match the combination of userid and attemptid.&lt;br /&gt;
     *&lt;br /&gt;
     * @param   int           $userid       The user to search.&lt;br /&gt;
     * @param   \context      $context      The context to export this submission against.&lt;br /&gt;
     * @param   array         $subcontext   The subcontext within the context to export this information&lt;br /&gt;
     * @param   int           $attid        The id of the submission to export.&lt;br /&gt;
     */&lt;br /&gt;
    public static function export_user_submissions(int $userid, \context $context, array $subcontext, int $attid) ;&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Plugins which are subplugins to another plugin====&lt;br /&gt;
&lt;br /&gt;
If you are developing a sub-plugin of another plugin, then you will have to look at the relevant plugin in order to determine the exact contract.&lt;br /&gt;
&lt;br /&gt;
Each subplugin type should define a new interface which extends the &#039;&#039;\core_privacy\local\request\plugin\subplugin_provider&#039;&#039; interface and it is up to the parent plugin to define how they will interact with their children.&lt;br /&gt;
&lt;br /&gt;
The principles remain the same, but the exact implementation will differ depending upon requirements.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/pluginname/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
namespace assignsubmission\onlinetext;&lt;br /&gt;
&lt;br /&gt;
class provider implements&lt;br /&gt;
    // This plugin does store personal user data.&lt;br /&gt;
    \core_privacy\local\metadata\provider,&lt;br /&gt;
&lt;br /&gt;
    // This plugin is a subplugin of assign and must meet that contract.&lt;br /&gt;
    \mod_assign\privacy\assignsubmission_provider {&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Plugins which are typically called by a Moodle subsystem====&lt;br /&gt;
&lt;br /&gt;
There are a number of plugintypes in Moodle which are typically called by a specific Moodle subsystem.&lt;br /&gt;
&lt;br /&gt;
Some of these are &#039;&#039;only&#039;&#039; called by that subsystem, for example plugins which are of the &#039;&#039;plagiarism&#039;&#039; plugintype should never be called directly, but are always invoked via the &#039;&#039;core_plagiarism&#039;&#039; subsystem.&lt;br /&gt;
&lt;br /&gt;
Conversely, there maybe other plugintypes which can be called both via a subsystem, and in some other fashion. We are still determining whether any plugintypes currently fit this pattern.&lt;br /&gt;
&lt;br /&gt;
If you are developing a plugin which belongs to a specific subsystem, then you will have to look at the relevant plugin in order to determine the exact contract.&lt;br /&gt;
&lt;br /&gt;
Each subsystem will define a new interface which extends the &#039;&#039;\core_privacy\local\request\plugin\subsystem_provider&#039;&#039; interface and it is up to that subsystem to define how they will interact with those plugins.&lt;br /&gt;
&lt;br /&gt;
The principles remain the same, but the exact implementation will differ depending upon requirements.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;plagiarism/detectorator/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
namespace plagiarism_detectorator\privacy;&lt;br /&gt;
&lt;br /&gt;
class provider implements&lt;br /&gt;
    // This plugin does export personal user data.&lt;br /&gt;
    \core_privacy\local\metadata\provider,&lt;br /&gt;
&lt;br /&gt;
    // This plugin is always linked against another activity module via the Plagiarism API.&lt;br /&gt;
    \core_plagiarism\privacy\plugin_provider {&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Exporting data====&lt;br /&gt;
&lt;br /&gt;
Any plugin which stores data must also export it.&lt;br /&gt;
&lt;br /&gt;
To cater for this the privacy API includes a &#039;&#039;\core_privacy\local\request\content_writer&#039;&#039;, which defines a set of functions to store different types of data.&lt;br /&gt;
&lt;br /&gt;
Broadly speaking data is broken into the following types:&lt;br /&gt;
&lt;br /&gt;
* Data - this is the object being described. For example the post content in a forum post;&lt;br /&gt;
* Related data - this is data related to the object being stored. For example, ratings of a forum post;&lt;br /&gt;
* Metadata - This is metadata about the main object. For example whether you are subscribed to a forum discussion;&lt;br /&gt;
* User preferences - this is data about a site-wide preference;&lt;br /&gt;
* Files - Any files that you are stored within Moodle on behalf of this plugin; and&lt;br /&gt;
* Custom files - For custom file formats - e.g. a calendar feed for calendar data. These should be used sparingly.&lt;br /&gt;
&lt;br /&gt;
Each piece of data is stored against a specific Moodle &#039;&#039;context&#039;&#039;, which will define how the data is structured within the exporter.&lt;br /&gt;
Data, and Related data only accept the &#039;&#039;stdClass&#039;&#039; object, whilst metadata should be stored as a set of key/value pairs which include a description.&lt;br /&gt;
&lt;br /&gt;
In some cases the data being stored belongs within an implicit structure. For example, one forum has many forum discussions, which each have a number of forum posts. This structure is represented by an &#039;&#039;array&#039;&#039; referred to as a &#039;&#039;subcontext&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;content_writer&#039;&#039; must &#039;&#039;always&#039;&#039; be called with a specific context, and can be called as follows:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
use \core_privacy\local\request\writer;&lt;br /&gt;
&lt;br /&gt;
writer::with_context($context)&lt;br /&gt;
    -&amp;gt;export_data($subcontext, $post)&lt;br /&gt;
    -&amp;gt;export_area_files($subcontext, &#039;mod_forum&#039;, &#039;post&#039;, $post-&amp;gt;id)&lt;br /&gt;
    -&amp;gt;export_metadata($subcontext, &#039;postread&#039;, (object) [&#039;firstread&#039; =&amp;gt; $firstread], new \lang_string(&#039;privacy:export:post:postread&#039;));&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Any text field which supports Moodle files must also be rewritten:&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/forum/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
// …&lt;br /&gt;
use \core_privacy\local\request\writer;&lt;br /&gt;
&lt;br /&gt;
$post-&amp;gt;message = writer::with_context($context)&lt;br /&gt;
    -&amp;gt;rewrite_pluginfile_urls($subcontext, &#039;mod_forum&#039;, &#039;post&#039;, $post-&amp;gt;id, $post-&amp;gt;message);&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Providing a way to delete user data===&lt;br /&gt;
&lt;br /&gt;
Deleting user data is also implemented in the request interface. There are two methods that need to be created. The first one to remove all user data from a context, the other to remove user data for a specific user in a list of contexts.&lt;br /&gt;
&lt;br /&gt;
====Delete for a context====&lt;br /&gt;
&lt;br /&gt;
A context is given and all user data (for all users) is to be deleted from the plugin. This will be called when the retention period for the context has expired to adhere to the privacy by design requirement. Retention periods are set in the Data registry.&lt;br /&gt;
&lt;br /&gt;
Note that this will be called for &#039;&#039;&#039;any&#039;&#039;&#039; context being expired, not only those the plugin holds data for (as returned by get_contexts_for_userid()), so you must carefully check the contexts given.&lt;br /&gt;
&lt;br /&gt;
When expiring content for a high-level context such as a course context, the function will be called not only with the course context but also with each context within the course: each module context, each block context, etc. At each level you should only expire data specifically related to that exact context. For a module plugin, this generally means you should only take action for CONTEXT_MODULE contexts and only if they relate to an instance of your module, as shown in the following example. (In the rare case where your module also stores user data related to the course, and not just for a module instance, then you do need to implement CONTEXT_COURSE level contexts to expire that course-level data.)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Delete all personal data for all users in the specified context.&lt;br /&gt;
 *&lt;br /&gt;
 * @param context $context Context to delete data from.&lt;br /&gt;
 */&lt;br /&gt;
public static function delete_data_for_all_users_in_context(\context $context) {&lt;br /&gt;
    global $DB;&lt;br /&gt;
&lt;br /&gt;
    if ($context-&amp;gt;contextlevel != CONTEXT_MODULE) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $cm = get_coursemodule_from_id(&#039;choice&#039;, $context-&amp;gt;instanceid);&lt;br /&gt;
    if (!$cm) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    $DB-&amp;gt;delete_records(&#039;choice_answers&#039;, [&#039;choiceid&#039; =&amp;gt; $cm-&amp;gt;instance]);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Delete personal information for a specific user and context(s)====&lt;br /&gt;
&lt;br /&gt;
An &#039;&#039;approved_contextlist&#039;&#039; is given and user data related to that user should either be completely deleted, or overwritten if a structure needs to be maintained. This will be called when a user has requested the right to be forgotten. All attempts should be made to delete this data where practical while still allowing the plugin to be used by other users.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/choice/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
public static function delete_data_for_user(approved_contextlist $contextlist) {&lt;br /&gt;
    global $DB;&lt;br /&gt;
    &lt;br /&gt;
    if (empty($contextlist-&amp;gt;count())) {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
    $userid = $contextlist-&amp;gt;get_user()-&amp;gt;id;&lt;br /&gt;
    foreach ($contextlist-&amp;gt;get_contexts() as $context) {&lt;br /&gt;
        $instanceid = $DB-&amp;gt;get_field(&#039;course_modules&#039;, &#039;instance&#039;, [&#039;id&#039; =&amp;gt; $context-&amp;gt;instanceid], MUST_EXIST);&lt;br /&gt;
        $DB-&amp;gt;delete_records(&#039;choice_answers&#039;, [&#039;choiceid&#039; =&amp;gt; $instanceid, &#039;userid&#039; =&amp;gt; $userid]);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
====Delete personal information for several users in a specific context====&lt;br /&gt;
&lt;br /&gt;
An &#039;&#039;approved_userlist&#039;&#039; is given and user data related to all users in the specified context should either be completely deleted, or overwritten if a structure needs to be maintained. This will be called when a user has requested the right to be forgotten when per-role overrides exist, or when performing a per-role expiry of a context. All attempts should be made to delete this data where practical while still allowing the plugin to be used by other users.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;mod/chat/classes/privacy/provider.php&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
/**&lt;br /&gt;
 * Delete multiple users within a single context.&lt;br /&gt;
 *&lt;br /&gt;
 * @param approved_userlist $userlist The approved context and user information to delete information for.&lt;br /&gt;
 */&lt;br /&gt;
public static function delete_data_for_users(approved_userlist $userlist) {&lt;br /&gt;
    global $DB;&lt;br /&gt;
&lt;br /&gt;
    $context = $userlist-&amp;gt;get_context();&lt;br /&gt;
    $cm = $DB-&amp;gt;get_record(&#039;course_modules&#039;, [&#039;id&#039; =&amp;gt; $context-&amp;gt;instanceid]);&lt;br /&gt;
    $chat = $DB-&amp;gt;get_record(&#039;chat&#039;, [&#039;id&#039; =&amp;gt; $cm-&amp;gt;instance]);&lt;br /&gt;
&lt;br /&gt;
    list($userinsql, $userinparams) = $DB-&amp;gt;get_in_or_equal($userlist-&amp;gt;get_userids(), SQL_PARAMS_NAMED);&lt;br /&gt;
    $params = array_merge([&#039;chatid&#039; =&amp;gt; $chat-&amp;gt;id], $userinparams);&lt;br /&gt;
    $sql = &amp;quot;chatid = :chatid AND userid {$userinsql}&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
    $DB-&amp;gt;delete_records_select(&#039;chat_messages&#039;, $sql, $params);&lt;br /&gt;
    $DB-&amp;gt;delete_records_select(&#039;chat_messages_current&#039;, $sql, $params);&lt;br /&gt;
    $DB-&amp;gt;delete_records_select(&#039;chat_users&#039;, $sql, $params);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Difference between Moodle 3.3 and more recent versions==&lt;br /&gt;
Moodle 3.3 has a minimum requirement of php 5.6 and so type hinting and return type declarations are not supported in this version. &lt;br /&gt;
Consequently the privacy API for this version does not have these features.&lt;br /&gt;
==Common Questions==&lt;br /&gt;
===What to do if you have one plugin that supports multiple branches===&lt;br /&gt;
This is something that we have considered and we have put in place a polyfill. This gets around the restrictions of one version having type hinting and return type declarations while another does not.&lt;br /&gt;
&lt;br /&gt;
====Example====&lt;br /&gt;
To use the polyfill include the legacy polyfill trait and create the necessary static methods but with an underscore (shown below).&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
class provider implements&lt;br /&gt;
    \core_privacy\local\metadata\provider,&lt;br /&gt;
    \core_privacy\local\request\plugin\provider {&lt;br /&gt;
&lt;br /&gt;
    // This trait must be included.&lt;br /&gt;
    use \core_privacy\local\legacy_polyfill;&lt;br /&gt;
&lt;br /&gt;
    // The required methods must be in this format starting with an underscore.&lt;br /&gt;
    public static function _get_metadata(collection $collection) {&lt;br /&gt;
        // Code for returning metadata goes here.&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===What to do if your plugin must implement a subplugin or subsystem plugin provider===&lt;br /&gt;
For subplugins (e.g. assignsubmission, assignfeedback, quiz report, quiz access rules), or subsystems which have a plugintype relationship (portfolio, plagiarism, and others), they will also define their own legacy polyfill.&lt;br /&gt;
&lt;br /&gt;
In this instance you will need to include the trait for both the core polyfill, and the provider polyfill as appropriate.&lt;br /&gt;
==== Example ====&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
class provider implements&lt;br /&gt;
    // This plugin has data and must therefore define the metadata provider in order to describe it.&lt;br /&gt;
    \core_privacy\local\metadata\provider,&lt;br /&gt;
&lt;br /&gt;
    // This is a plagiarism plugin. It interacts with the plagiarism subsystem rather than with core.&lt;br /&gt;
    \core_plagiarism\privacy\plagiarism_provider {&lt;br /&gt;
&lt;br /&gt;
    // This trait must be included to provide the relevant polyfill for the metadata provider.&lt;br /&gt;
    use \core_privacy\local\legacy_polyfill;&lt;br /&gt;
&lt;br /&gt;
    // This trait must be included to provide the relevant polyfill for the plagirism provider.&lt;br /&gt;
    use \core_plagiarism\privacy\plagiarism_provider\legacy_polyfill;&lt;br /&gt;
&lt;br /&gt;
    // The required methods must be in this format starting with an underscore.&lt;br /&gt;
    public static function _get_metadata(collection $collection) {&lt;br /&gt;
        // Code for returning metadata goes here.&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // This is one of the polyfilled methods from the plagiarism provider.&lt;br /&gt;
    public static function _export_plagiarism_user_data($userid, \context $context, array $subcontext, array $linkarray) {&lt;br /&gt;
        // ...&lt;br /&gt;
    }&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Tips for development ==&lt;br /&gt;
&lt;br /&gt;
* While implementing the privacy API into your plugin, there are CLI scripts that can help you to test things on the fly. Just don&#039;t forget these are not supposed to replace proper unit tests. See [[Privacy API/Utilities]] for details.&lt;br /&gt;
* Inherit Unit tests from the &amp;lt;code php&amp;gt;core_privacy\tests\provider_testcase&amp;lt;/code&amp;gt;, not &amp;lt;code php&amp;gt;advanced_testcase&amp;lt;/code&amp;gt;. Advanced test case doesn&#039;t reset the Privacy content_writer between tests!&lt;br /&gt;
&lt;br /&gt;
==See also==&lt;br /&gt;
&lt;br /&gt;
* [[Subject Access Request FAQ]]&lt;br /&gt;
* [[:en:GDPR|GDPR]] in the user documentation&lt;br /&gt;
* [[Privacy API/Utilities]] provides CLI scripts that are helpful during development&lt;br /&gt;
&lt;br /&gt;
[[Category:Privacy]]&lt;br /&gt;
[[Category:GDPR]]&lt;br /&gt;
[[Category:API]]&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55101</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55101"/>
		<updated>2018-11-27T16:59:19Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Summary */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app, for example so that you can test plugins that you may have written for the app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_approot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that uses the &#039;I enter the mobile app&#039; step. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionicaddress = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This step can only be used on the app login page. It will log in with the given username and password. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;360x640&amp;quot;&amp;lt;/tt&amp;gt; is a useful background step if you want your tests to run in mobile phone (rather than tablet/desktop) format. You can also use this step mid-test to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_approot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55100</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55100"/>
		<updated>2018-11-27T16:58:51Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Limitations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_approot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that uses the &#039;I enter the mobile app&#039; step. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionicaddress = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This step can only be used on the app login page. It will log in with the given username and password. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;360x640&amp;quot;&amp;lt;/tt&amp;gt; is a useful background step if you want your tests to run in mobile phone (rather than tablet/desktop) format. You can also use this step mid-test to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
I have split the limitations of this approach into three categories, below.&lt;br /&gt;
&lt;br /&gt;
=== Fundamental limitations ===&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
=== Extra steps that might be needed ===&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
Probably there are also other extra steps that would be useful - these would be discovered by trying to write tests. At the OU we plan to write Behat tests for our plugins once this feature is approved, so we will identify and add some extra steps at that point.&lt;br /&gt;
&lt;br /&gt;
=== Testing the app itself ===&lt;br /&gt;
&lt;br /&gt;
This system can absolutely be used to test features built into the app as well a plugins. &lt;br /&gt;
&lt;br /&gt;
However, there is a limitation, which is that currently the Behat feature files must live in the Moodle project. This may be less appropriate for app features.&lt;br /&gt;
&lt;br /&gt;
A future extension of this feature could use the behat_approot variable and automatically include feature files from a directory within the app&#039;s source tree. I think this would probably be quite simple but I have not yet tried it.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
	<entry>
		<id>https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55099</id>
		<title>Acceptance testing for the Moodle App</title>
		<link rel="alternate" type="text/html" href="https://docs.moodle.org/dev/index.php?title=Acceptance_testing_for_the_Moodle_App&amp;diff=55099"/>
		<updated>2018-11-27T14:05:26Z</updated>

		<summary type="html">&lt;p&gt;Quen: /* Summary */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;This is only a proposal, MDL-63977, and is not yet available in Moodle. Please contact sam marshall if you have any queries and do not use this information for anything yet!&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
From Moodle 3.7 if this change is accepted it will be possible to write Behat tests for mobile app features.&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
&lt;br /&gt;
It is now possible to create Behat tests that carry out automated functionality testing on the mobile app.&lt;br /&gt;
&lt;br /&gt;
By default, these do not run as part of a normal Behat run. This page tells you how to run the tests, and how to write them.&lt;br /&gt;
&lt;br /&gt;
A key point is that these tests for some parts of the mobile app are &#039;&#039;&#039;included within the Moodle codebase&#039;&#039;&#039;, not within the app codebase, because they are run using the Moodle Behat infrastructure. This is definitely appropriate for Moodle plugins that add app support. It may also be acceptable for tests of the app itself, but this is not yet agreed.&lt;br /&gt;
&lt;br /&gt;
The main advantages of this approach are:&lt;br /&gt;
&lt;br /&gt;
* It is easy for third-party plugin authors to create tests for app features in exactly the same way that they create tests for website features.&lt;br /&gt;
* Where institutions run tests automatically, it should be relatively easy to include some app tests within the existing approach.&lt;br /&gt;
* This system does not require any mobile device hardware and should work on all common platforms.&lt;br /&gt;
&lt;br /&gt;
This system has been tested on Windows 7 and Ubuntu 18.04 LTS.&lt;br /&gt;
&lt;br /&gt;
== Running Behat tests for the mobile app ==&lt;br /&gt;
&lt;br /&gt;
=== Set up a mobile app development environment ===&lt;br /&gt;
&lt;br /&gt;
First you will need to set up a mobile app development environment.&lt;br /&gt;
&lt;br /&gt;
Follow the first part of the instructions on this page:&lt;br /&gt;
&lt;br /&gt;
* [[Setting up your development environment for Moodle Mobile 2]]&lt;br /&gt;
&lt;br /&gt;
You need to get as far as the part in section 5 where you open the app in the browser; this is what Behat will do. You don&#039;t need to complete the later steps.&lt;br /&gt;
&lt;br /&gt;
* You will need to update this environment periodically, for example when a new version of the mobile app is released. Behat does not do this for you.&lt;br /&gt;
* When you update the mobile app, you must re-run Behat init on the corresponding Moodle installation.&lt;br /&gt;
&lt;br /&gt;
=== Add the mobile app Behat configuration ===&lt;br /&gt;
&lt;br /&gt;
You need to add one or two lines to your config.php to enable app testing. First, put this line in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_approot = &#039;/path/to/app/workspace/moodlemobile2&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This may be sufficient, but you need to be aware of a couple of facts about the Ionic server used for app testing:&lt;br /&gt;
&lt;br /&gt;
* Depending on your computer, it may take about 3 minutes to start up. &lt;br /&gt;
* The server uses about 1GB RAM.&lt;br /&gt;
&lt;br /&gt;
There are two options, covered next.&lt;br /&gt;
&lt;br /&gt;
==== Let Behat launch the app environment ====&lt;br /&gt;
&lt;br /&gt;
If you want Behat to launch the app environment for you, then you don&#039;t need to add anything else to config.php.&lt;br /&gt;
&lt;br /&gt;
When you do this, the Ionic server will be started automatically when Behat runs a test that uses the &#039;I enter the mobile app&#039; step. It will be automatically terminated when the Behat test run finishes. If the test run includes multiple scenarios that use the app, they will all reuse the one server; it won&#039;t restart each time.&lt;br /&gt;
&lt;br /&gt;
This is simple and convenient, but it is probably not a good approach for developers who frequently re-run a short Behat run (as you have to wait for it to start Ionic every time) or for complex systems that run Behat in parallel (as you may end up with multiple copies of Ionic eating up your RAM).&lt;br /&gt;
&lt;br /&gt;
==== Manually launch the app environment yourself ====&lt;br /&gt;
&lt;br /&gt;
The other option is to launch the Ionic server yourself (using &amp;lt;tt&amp;gt;ionic serve -b&amp;lt;/tt&amp;gt;) before running any Behat tests, and also kill it yourself once you are finished with it. In this case, after launching it you will see output like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;&lt;br /&gt;
[OK] Development server running!&lt;br /&gt;
     Local: http://localhost:8100&lt;br /&gt;
     External: http://137.108.5.43:8100, http://192.168.56.1:8100&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To use it in Behat, add this extra line to the Moodle config.php:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
$CFG-&amp;gt;behat_ionicaddress = &#039;http://localhost:8100&#039;;&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The advantage of this approach is that you are in charge of bringing up and taking down the Ionic server, so you can do this efficiently, share a copy between parallel runs, etc. The disadvantage is that you do have to remember to do it; if the server isn&#039;t running, tests which use the app will fail.&lt;br /&gt;
&lt;br /&gt;
The running Ionic server should correspond to the app root setting, above, because the version number from the mobile app installation is used to determine which tests to run.&lt;br /&gt;
&lt;br /&gt;
== Browser profiles ==&lt;br /&gt;
&lt;br /&gt;
Mobile tests only run in Chrome, so you need to make sure you have a Chrome profile set up in your config.php Behat settings.&lt;br /&gt;
&lt;br /&gt;
* See [[Running acceptance test]] for more information on profiles.&lt;br /&gt;
&lt;br /&gt;
Behat will automatically run app tests (those with @app tag) only in a Chrome browser profile. So, if you run multiple browser tests, it won&#039;t waste time trying to run the app tests in each one.&lt;br /&gt;
&lt;br /&gt;
== Behat init ==&lt;br /&gt;
&lt;br /&gt;
After you have set up the config.php, you will need to re-run Behat init:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code php&amp;gt;&lt;br /&gt;
php admin/tool/behat/cli/init.php&lt;br /&gt;
&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is necessary because by default, Behat won&#039;t run app tests (those with @app tag) at all, since you didn&#039;t have it configured.&lt;br /&gt;
&lt;br /&gt;
== Running Behat ==&lt;br /&gt;
&lt;br /&gt;
To run mobile tests in Behat, simply launch Behat in the usual way, but make sure you are using a Chrome profile. (Depending on your setup, this might mean using &amp;lt;tt&amp;gt;--profile=chrome&amp;lt;/tt&amp;gt;.)&lt;br /&gt;
&lt;br /&gt;
You can specify the scenarios to run as normal. The app tests all have the @app tag, so if you want to run all the mobile tests you can specify --tags=app, but you can also run any other set of scenarios. It is OK to combine app and normal tests in the same run.&lt;br /&gt;
&lt;br /&gt;
== Writing tests ==&lt;br /&gt;
&lt;br /&gt;
This page assumes you already know all about [[Writing acceptance tests]] in general.&lt;br /&gt;
&lt;br /&gt;
=== Test structure ===&lt;br /&gt;
&lt;br /&gt;
* Mobile app test scenarios should be marked &amp;lt;tt&amp;gt;@app&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;@javascript&amp;lt;/tt&amp;gt; in addition to any other tags that may be required.&lt;br /&gt;
* If creating a feature file specifically for app tests, call it &amp;lt;tt&amp;gt;app_whatever.feature&amp;lt;/tt&amp;gt; (i.e. use the &amp;lt;tt&amp;gt;app_&amp;lt;/tt&amp;gt; prefix). This is not technically required, it&#039;s just for consistency.&lt;br /&gt;
&lt;br /&gt;
You are writing a normal Behat test and this is likely to require background steps similar to any other Moodle Behat test, for example &amp;lt;tt&amp;gt;the following &amp;quot;courses&amp;quot; exist&amp;lt;/tt&amp;gt;, and so on.&lt;br /&gt;
&lt;br /&gt;
=== Start the app ===&lt;br /&gt;
&lt;br /&gt;
Once all necessary Moodle configuration steps (creating courses, users, groups, etc.) are done, use this Behat step to start the app:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Given &#039;&#039;&#039;I enter the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will:&lt;br /&gt;
&lt;br /&gt;
* Set up all the Moodle server settings to allow the mobile app to connect.&lt;br /&gt;
* Launch Ionic if necessary&lt;br /&gt;
* Restart the browser. This is needed to ensure it doesn&#039;t contain any stored data from previous app testing.&lt;br /&gt;
* Set the browser to a suitable phone size (you can change it later if you want a tablet or other size).&lt;br /&gt;
* Open the Ionic server address in the test Chrome browser.&lt;br /&gt;
* Install necessary JavaScript code in the page that supports Behat testing. &lt;br /&gt;
* Automatically enter the server URL into the app if necessary.&lt;br /&gt;
&lt;br /&gt;
After this step completes, if it is the first time you ran the app inside this scenario, you will be left at the login screen. If you already logged in earlier, then you will be at the start page.&lt;br /&gt;
&lt;br /&gt;
You can use this step even when you are already in the app; this will restart it.&lt;br /&gt;
&lt;br /&gt;
=== Log in to the app ===&lt;br /&gt;
&lt;br /&gt;
All the other app-specific Behat steps end with the words &#039;in the app&#039; to distinguish them from the normal steps. To log in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;When &#039;&#039;&#039;I log in as &amp;quot;student1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This step can only be used on the app login page. It will log in with the given username and password. You will then be left at the start page.&lt;br /&gt;
&lt;br /&gt;
=== Actions ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This finds an element which contains either the visible text, or Aria label, &#039;Course 1&#039; and clicks it. It should work for links, buttons and similar. &lt;br /&gt;
&lt;br /&gt;
You should be able to use this for almost any actions that would be carried out by pressing something - pressing a button, following a link, changing a checkbox, switching a switch, opening a dropdown, selecting something from the popup, etc.&lt;br /&gt;
&lt;br /&gt;
For buttons that are icons with no text, you can usually find them using the Chrome inspector - look for the &#039;aria-label&#039; attribute.&lt;br /&gt;
&lt;br /&gt;
You can press the main (bottom) menu buttons using this step. For example, the home button icon has the label &#039;home&#039;.&lt;br /&gt;
&lt;br /&gt;
* Exact matches (an element which contains only the specified text, or where the Aria label is exactly the specified text) will be preferred. If there are no exact matches, then partial ones (anything containing that text) will be considered.&lt;br /&gt;
* If there are multiple matches, or none, the step will fail. You can avoid this by specifying an exact match (provided there is only one exact match, this will not fail even if there are other partial matches) or by clicking on an icon instead of text.&lt;br /&gt;
* If the item you try to press is a label for some other form field (using &amp;lt;tt&amp;gt;ion-label&amp;lt;/tt&amp;gt; and the &amp;lt;tt&amp;gt;aria-labelled-by&amp;lt;/tt&amp;gt; attribute) then it will actually press the field; this is useful in the settings menus.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press &amp;quot;Course 1&amp;quot; near &amp;quot;Unique text&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is a variant of the above step which is useful when there are multiple elements with the same text on the page. The second value (&#039;Unique text&#039; in this example) should be some text that is unique on the page. The system will press the instance of &#039;Course 1&#039; that is nearest to the supplied unique text.&lt;br /&gt;
&lt;br /&gt;
(This is intended as a simpler alternative to the standard Behat steps that use the word &#039;in&#039;, such as &amp;lt;tt&amp;gt;I click on &amp;quot;X&amp;quot; &amp;quot;thing&amp;quot; in &amp;quot;Y&amp;quot; &amp;quot;css_element&amp;quot;&amp;lt;/tt&amp;gt;. Those steps are complex and can be difficult to use. This one is not as generic but hopefully will handle most circumstances.)&lt;br /&gt;
&lt;br /&gt;
* Nearest is defined in terms of the DOM rather than pixel position; it is based on the number of steps you would have to take up the tree from the candidate item before you get to a shared ancestor with the unique text.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I set the field &amp;quot;field name&amp;quot; to &amp;quot;text value&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This sets a text field to the given value. For the field name, you can use the placeholder text (exact match will be found first, otherwise partial match if any).&lt;br /&gt;
&lt;br /&gt;
This works with single-line text fields, multi-line text fields with rich text editor switched off (textarea) and rich-text-editor fields.&lt;br /&gt;
&lt;br /&gt;
* The normal version of this step supports various form fields, but in the app this only supports text fields at the moment. Use the press step (above) for other types of field.&lt;br /&gt;
* When used with a rich-text editor, you can include HTML tags in the value if necessary.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the back button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the main menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And &#039;&#039;&#039;I press the page menu button in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These steps will press, respectively:&lt;br /&gt;
&lt;br /&gt;
* The back button (the left pointing arrow at top left of the app).&lt;br /&gt;
* The main menu button (the &#039;...&#039; icon at bottom right of the app).&lt;br /&gt;
* The page menu button, if present (the &#039;...&#039; icon at top right of the app).&lt;br /&gt;
&lt;br /&gt;
Note that both the main menu and page menu use a &#039;more&#039; icon so they are annoying to activate with the generic press command.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I switch to the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;And I close the browser tab opened by the app&amp;lt;/tt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
These two steps are necessary if you want to test the transition between the app and browser (e.g. test &#039;Open in browser&#039; links). For example, after pressing &#039;Open in browser&#039; you can use the first step above, and then you will be able to use normal Moodle Behat steps to check the browser tab. Then when finished, use the second step above.&lt;br /&gt;
&lt;br /&gt;
=== Tests ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;tt&amp;gt;Then &#039;&#039;&#039;the header should be &amp;quot;Course 1&amp;quot; in the app&#039;&#039;&#039;&amp;lt;/tt&amp;gt; &lt;br /&gt;
&lt;br /&gt;
This checks the text of the current page header (orange stripe at top of page) in the app. It must be an exact match for the specified text.&lt;br /&gt;
&lt;br /&gt;
For this scenario, &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; would also work, but this allows you to specifically check the header as opposed to the text appearing elsewhere on the page.&lt;br /&gt;
&lt;br /&gt;
=== Standard test steps ===&lt;br /&gt;
&lt;br /&gt;
You can use all the normal Moodle Behat test steps while carrying out app testing, but some of them don&#039;t work very well. The app has a complex DOM and previous pages that are &#039;back&#039; from your current page may still be present in the DOM, which means that any steps that just look for the first matching element in the DOM are likely to look for elements on a page you&#039;re not even on.&lt;br /&gt;
&lt;br /&gt;
==== Useful, working steps ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;tt&amp;gt;I should see&amp;lt;/tt&amp;gt; and &amp;lt;tt&amp;gt;I should not see&amp;lt;/tt&amp;gt; are very useful for checking results.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I change viewport size to &amp;quot;360x640&amp;quot;&amp;lt;/tt&amp;gt; is a useful background step if you want your tests to run in mobile phone (rather than tablet/desktop) format. You can also use this step mid-test to simulate switching between portrait and landscape formats.&lt;br /&gt;
* &amp;lt;tt&amp;gt;I pause&amp;lt;/tt&amp;gt; works and is very useful to debug your scenario.&lt;br /&gt;
&lt;br /&gt;
==== Problematic steps ====&lt;br /&gt;
&lt;br /&gt;
* The &amp;lt;tt&amp;gt;I reload the page&amp;lt;/tt&amp;gt; step does not work correctly in the app and may leave your test in a mess. Use &amp;lt;tt&amp;gt;I enter the app&amp;lt;/tt&amp;gt; if you want to reload the app.&lt;br /&gt;
&lt;br /&gt;
=== Leaving the app ===&lt;br /&gt;
&lt;br /&gt;
If you want to leave the app and go back to Moodle within a scenario, simply use a Moodle step that goes to a page, such as &amp;lt;tt&amp;gt;I am on site homepage&amp;lt;/tt&amp;gt; or &amp;lt;tt&amp;gt;I am on &amp;quot;Course 1&amp;quot; course homepage&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You only need to do this if you want to carry out actions within Moodle after using the app, within the scenario. At the end of your scenario, there is no need to explicitly leave the app; Moodle will automatically start the next scenario on the Moodle start page as usual.&lt;br /&gt;
&lt;br /&gt;
== Limitations ==&lt;br /&gt;
&lt;br /&gt;
These limitations apply to the current system and can be resolved:&lt;br /&gt;
&lt;br /&gt;
* There is no obvious way to attach files.&lt;br /&gt;
&lt;br /&gt;
These limitations are (at least currently) fundamental to the testing approach:&lt;br /&gt;
&lt;br /&gt;
* It is not possible to test behaviour specific to iOS, because tests run in Chrome. (Most iOS-specific failures are caused by problems with the Safari browser used on iOS, which isn&#039;t very good.)&lt;br /&gt;
* Device features such as the camera cannot be tested, because tests run in a browser and not on a device.&lt;br /&gt;
&lt;br /&gt;
It&#039;s my firm belief that even given those restrictions on test coverage, being able to run Behat tests is massively beneficial compared to having to test everything by hand for every app release.&lt;br /&gt;
&lt;br /&gt;
== Advanced ==&lt;br /&gt;
&lt;br /&gt;
=== Versioning ===&lt;br /&gt;
&lt;br /&gt;
The Behat tests are stored in the Moodle codebase, so they always relate to a particular Moodle version, but sometimes it might be necessary to have different tests for different versions of the mobile app. For example, you may be writing a test in Moodle 3.6, where the behaviour in the Moodle 3.6 app is different from behaviour in the Moodle 3.7 app (but both apps can connect to the server).&lt;br /&gt;
&lt;br /&gt;
For these situations:&lt;br /&gt;
&lt;br /&gt;
* In addition to the @app tag, add a version-specific tag to your scenario or feature.&lt;br /&gt;
* There are two types of tag: &#039;&#039;&#039;@app_from3.7&#039;&#039;&#039; (include for every app version from 3.7 and newer) or &#039;&#039;&#039;@app_upto3.6.3&#039;&#039;&#039; (include for every app version up to 3.6.3, but not after that).&lt;br /&gt;
* You can use a two-digit or three-digit version number (3.6 or 3.6.1).&lt;br /&gt;
&lt;br /&gt;
=== Testing against multiple app versions ===&lt;br /&gt;
&lt;br /&gt;
If you need to run tests against multiple versions of the mobile app, you can do this in two ways:&lt;br /&gt;
&lt;br /&gt;
# Update the code in the mobile app workspace (check out a different version). Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
# Maintain multiple copies of the mobile app workspace and switch between them by changing config.php. Then re-run Behat init and run the Behat tests again.&lt;br /&gt;
&lt;br /&gt;
In both cases, because you need to run Behat init and change the Behat configuration, you cannot do this in parallel; if you want to run these tests in parallel you will also need separate Moodle installations with their own config.php, wwwroot, and dataroot.&lt;/div&gt;</summary>
		<author><name>Quen</name></author>
	</entry>
</feed>